From e8fa477793e1395664f79d572800f11994cdd38d Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sat, 13 Jul 2019 19:17:57 +0300 Subject: [PATCH 01/48] Refactor Follows/Followers counter syncronization - Actually sync counters in the database instead of info cache (which got overriden after user update was finished anyway) - Add following count field to user info - Set hide_followers/hide_follows for remote users based on http status codes for the first collection page --- config/test.exs | 3 +- lib/pleroma/object/fetcher.ex | 6 ++- lib/pleroma/user.ex | 4 +- lib/pleroma/user/info.ex | 13 ++++- lib/pleroma/web/activity_pub/activity_pub.ex | 53 ++++++++++++++++++- .../web/activity_pub/transmogrifier.ex | 27 ---------- test/web/activity_pub/transmogrifier_test.exs | 28 ---------- 7 files changed, 73 insertions(+), 61 deletions(-) diff --git a/config/test.exs b/config/test.exs index 96ecf3592..28eea3b00 100644 --- a/config/test.exs +++ b/config/test.exs @@ -29,7 +29,8 @@ email: "admin@example.com", notify_email: "noreply@example.com", skip_thread_containment: false, - federating: false + federating: false, + external_user_synchronization: false # Configure your database config :pleroma, Pleroma.Repo, diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index 101c21f96..bc3e7e5bc 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -76,7 +76,7 @@ def fetch_object_from_id!(id, options \\ []) do end end - def fetch_and_contain_remote_object_from_id(id) do + def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do Logger.info("Fetching object #{id} via AP") with true <- String.starts_with?(id, "http"), @@ -96,4 +96,8 @@ def fetch_and_contain_remote_object_from_id(id) do {:error, e} end end + + def fetch_and_contain_remote_object_from_id(_id) do + {:error, "id must be a string"} + end end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index e5a6c2529..c252e8bff 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -114,7 +114,9 @@ def ap_following(%User{} = user), do: "#{ap_id(user)}/following" def user_info(%User{} = user, args \\ %{}) do following_count = - if args[:following_count], do: args[:following_count], else: following_count(user) + if args[:following_count], + do: args[:following_count], + else: user.info.following_count || following_count(user) follower_count = if args[:follower_count], do: args[:follower_count], else: user.info.follower_count diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index 08e43ff0f..2d8395b73 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -16,6 +16,7 @@ defmodule Pleroma.User.Info do field(:source_data, :map, default: %{}) field(:note_count, :integer, default: 0) field(:follower_count, :integer, default: 0) + field(:following_count, :integer, default: nil) field(:locked, :boolean, default: false) field(:confirmation_pending, :boolean, default: false) field(:confirmation_token, :string, default: nil) @@ -195,7 +196,11 @@ def remote_user_creation(info, params) do :uri, :hub, :topic, - :salmon + :salmon, + :hide_followers, + :hide_follows, + :follower_count, + :following_count ]) end @@ -206,7 +211,11 @@ def user_upgrade(info, params) do :source_data, :banner, :locked, - :magic_key + :magic_key, + :follower_count, + :following_count, + :hide_follows, + :hide_followers ]) end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index a3174a787..0a22fe223 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1013,6 +1013,56 @@ defp object_to_user_data(data) do {:ok, user_data} end + defp maybe_update_follow_information(data) do + with {:enabled, true} <- + {:enabled, Pleroma.Config.get([:instance, :external_user_synchronization])}, + {:ok, following_data} <- + Fetcher.fetch_and_contain_remote_object_from_id(data.following_address), + following_count <- following_data["totalItems"], + hide_follows <- collection_private?(following_data), + {:ok, followers_data} <- + Fetcher.fetch_and_contain_remote_object_from_id(data.follower_address), + followers_count <- followers_data["totalItems"], + hide_followers <- collection_private?(followers_data) do + info = %{ + "hide_follows" => hide_follows, + "follower_count" => followers_count, + "following_count" => following_count, + "hide_followers" => hide_followers + } + + info = Map.merge(data.info, info) + Map.put(data, :info, info) + else + {:enabled, false} -> + data + + e -> + Logger.error( + "Follower/Following counter update for #{data.ap_id} failed.\n" <> inspect(e) + ) + + data + end + end + + defp collection_private?(data) do + if is_map(data["first"]) and + data["first"]["type"] in ["CollectionPage", "OrderedCollectionPage"] do + false + else + with {:ok, _data} <- Fetcher.fetch_and_contain_remote_object_from_id(data["first"]) do + false + else + {:error, {:ok, %{status: code}}} when code in [401, 403] -> + true + + _e -> + false + end + end + end + def user_data_from_user_object(data) do with {:ok, data} <- MRF.filter(data), {:ok, data} <- object_to_user_data(data) do @@ -1024,7 +1074,8 @@ def user_data_from_user_object(data) do def fetch_and_prepare_user_from_ap_id(ap_id) do with {:ok, data} <- Fetcher.fetch_and_contain_remote_object_from_id(ap_id), - {:ok, data} <- user_data_from_user_object(data) do + {:ok, data} <- user_data_from_user_object(data), + data <- maybe_update_follow_information(data) do {:ok, data} else e -> Logger.error("Could not decode user at fetch #{ap_id}, #{inspect(e)}") diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index d14490bb5..e34fe6611 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -1087,10 +1087,6 @@ def upgrade_user_from_ap_id(ap_id) do PleromaJobQueue.enqueue(:transmogrifier, __MODULE__, [:user_upgrade, user]) end - if Pleroma.Config.get([:instance, :external_user_synchronization]) do - update_following_followers_counters(user) - end - {:ok, user} else %User{} = user -> {:ok, user} @@ -1123,27 +1119,4 @@ def maybe_fix_user_object(data) do data |> maybe_fix_user_url end - - def update_following_followers_counters(user) do - info = %{} - - following = fetch_counter(user.following_address) - info = if following, do: Map.put(info, :following_count, following), else: info - - followers = fetch_counter(user.follower_address) - info = if followers, do: Map.put(info, :follower_count, followers), else: info - - User.set_info_cache(user, info) - end - - defp fetch_counter(url) do - with {:ok, %{body: body, status: code}} when code in 200..299 <- - Pleroma.HTTP.get( - url, - [{:Accept, "application/activity+json"}] - ), - {:ok, data} <- Jason.decode(body) do - data["totalItems"] - end - end end diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index b896a532b..6d05138fb 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -1359,32 +1359,4 @@ test "removes recipient's follower collection from cc", %{user: user} do refute recipient.follower_address in fixed_object["to"] end end - - test "update_following_followers_counters/1" do - user1 = - insert(:user, - local: false, - follower_address: "http://localhost:4001/users/masto_closed/followers", - following_address: "http://localhost:4001/users/masto_closed/following" - ) - - user2 = - insert(:user, - local: false, - follower_address: "http://localhost:4001/users/fuser2/followers", - following_address: "http://localhost:4001/users/fuser2/following" - ) - - Transmogrifier.update_following_followers_counters(user1) - Transmogrifier.update_following_followers_counters(user2) - - %{follower_count: followers, following_count: following} = User.get_cached_user_info(user1) - assert followers == 437 - assert following == 152 - - %{follower_count: followers, following_count: following} = User.get_cached_user_info(user2) - - assert followers == 527 - assert following == 267 - end end From e5b850a99115859ceb028c3891f59d5e6ffd5d56 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sat, 13 Jul 2019 23:56:10 +0300 Subject: [PATCH 02/48] Refactor fetching follow information to a separate function --- lib/pleroma/user/info.ex | 1 + lib/pleroma/web/activity_pub/activity_pub.ex | 51 +++++++++++++------- 2 files changed, 34 insertions(+), 18 deletions(-) diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index 2d8395b73..67e8801ea 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -16,6 +16,7 @@ defmodule Pleroma.User.Info do field(:source_data, :map, default: %{}) field(:note_count, :integer, default: 0) field(:follower_count, :integer, default: 0) + # Should be filled in only for remote users field(:following_count, :integer, default: nil) field(:locked, :boolean, default: false) field(:confirmation_pending, :boolean, default: false) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 0a22fe223..eadd335ca 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1013,17 +1013,15 @@ defp object_to_user_data(data) do {:ok, user_data} end - defp maybe_update_follow_information(data) do - with {:enabled, true} <- - {:enabled, Pleroma.Config.get([:instance, :external_user_synchronization])}, - {:ok, following_data} <- - Fetcher.fetch_and_contain_remote_object_from_id(data.following_address), - following_count <- following_data["totalItems"], - hide_follows <- collection_private?(following_data), + def fetch_follow_information_for_user(user) do + with {:ok, following_data} <- + Fetcher.fetch_and_contain_remote_object_from_id(user.following_address), + following_count when is_integer(following_count) <- following_data["totalItems"], + {:ok, hide_follows} <- collection_private(following_data), {:ok, followers_data} <- - Fetcher.fetch_and_contain_remote_object_from_id(data.follower_address), - followers_count <- followers_data["totalItems"], - hide_followers <- collection_private?(followers_data) do + Fetcher.fetch_and_contain_remote_object_from_id(user.follower_address), + followers_count when is_integer(followers_count) <- followers_data["totalItems"], + {:ok, hide_followers} <- collection_private(followers_data) do info = %{ "hide_follows" => hide_follows, "follower_count" => followers_count, @@ -1031,8 +1029,22 @@ defp maybe_update_follow_information(data) do "hide_followers" => hide_followers } - info = Map.merge(data.info, info) - Map.put(data, :info, info) + info = Map.merge(user.info, info) + {:ok, Map.put(user, :info, info)} + else + {:error, _} = e -> + e + + e -> + {:error, e} + end + end + + defp maybe_update_follow_information(data) do + with {:enabled, true} <- + {:enabled, Pleroma.Config.get([:instance, :external_user_synchronization])}, + {:ok, data} <- fetch_follow_information_for_user(data) do + data else {:enabled, false} -> data @@ -1046,19 +1058,22 @@ defp maybe_update_follow_information(data) do end end - defp collection_private?(data) do + defp collection_private(data) do if is_map(data["first"]) and data["first"]["type"] in ["CollectionPage", "OrderedCollectionPage"] do - false + {:ok, false} else with {:ok, _data} <- Fetcher.fetch_and_contain_remote_object_from_id(data["first"]) do - false + {:ok, false} else {:error, {:ok, %{status: code}}} when code in [401, 403] -> - true + {:ok, true} - _e -> - false + {:error, _} = e -> + e + + e -> + {:error, e} end end end From d06d1b751d44802c5c3701f916ae2ce7d3c3be56 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sun, 14 Jul 2019 00:21:35 +0300 Subject: [PATCH 03/48] Use atoms when updating user info --- lib/pleroma/web/activity_pub/activity_pub.ex | 16 ++++++++-------- lib/pleroma/web/activity_pub/transmogrifier.ex | 6 +++--- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index eadd335ca..df4155d21 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -986,10 +986,10 @@ defp object_to_user_data(data) do user_data = %{ ap_id: data["id"], info: %{ - "ap_enabled" => true, - "source_data" => data, - "banner" => banner, - "locked" => locked + ap_enabled: true, + source_data: data, + banner: banner, + locked: locked }, avatar: avatar, name: data["name"], @@ -1023,10 +1023,10 @@ def fetch_follow_information_for_user(user) do followers_count when is_integer(followers_count) <- followers_data["totalItems"], {:ok, hide_followers} <- collection_private(followers_data) do info = %{ - "hide_follows" => hide_follows, - "follower_count" => followers_count, - "following_count" => following_count, - "hide_followers" => hide_followers + hide_follows: hide_follows, + follower_count: followers_count, + following_count: following_count, + hide_followers: hide_followers } info = Map.merge(user.info, info) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index e34fe6611..10b362908 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -609,13 +609,13 @@ def handle_incoming( with %User{ap_id: ^actor_id} = actor <- User.get_cached_by_ap_id(object["id"]) do {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object) - banner = new_user_data[:info]["banner"] - locked = new_user_data[:info]["locked"] || false + banner = new_user_data[:info][:banner] + locked = new_user_data[:info][:locked] || false update_data = new_user_data |> Map.take([:name, :bio, :avatar]) - |> Map.put(:info, %{"banner" => banner, "locked" => locked}) + |> Map.put(:info, %{banner: banner, locked: locked}) actor |> User.upgrade_changeset(update_data) From 183da33e005c8a8e8472350a3b6b36ff6f82d67d Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sun, 14 Jul 2019 00:56:02 +0300 Subject: [PATCH 04/48] Add tests for fetch_follow_information_for_user and check object type when fetching the page --- lib/pleroma/web/activity_pub/activity_pub.ex | 3 +- test/web/activity_pub/activity_pub_test.exs | 81 ++++++++++++++++++++ 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index df4155d21..c821ba45f 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1063,7 +1063,8 @@ defp collection_private(data) do data["first"]["type"] in ["CollectionPage", "OrderedCollectionPage"] do {:ok, false} else - with {:ok, _data} <- Fetcher.fetch_and_contain_remote_object_from_id(data["first"]) do + with {:ok, %{"type" => type}} when type in ["CollectionPage", "OrderedCollectionPage"] <- + Fetcher.fetch_and_contain_remote_object_from_id(data["first"]) do {:ok, false} else {:error, {:ok, %{status: code}}} when code in [401, 403] -> diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 59d56f3a7..448ffbf54 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -1222,4 +1222,85 @@ test "fetches only public posts for other users" do assert result.id == activity.id end end + + describe "fetch_follow_information_for_user" do + test "syncronizes following/followers counters" do + user = + insert(:user, + local: false, + follower_address: "http://localhost:4001/users/fuser2/followers", + following_address: "http://localhost:4001/users/fuser2/following" + ) + + {:ok, user} = ActivityPub.fetch_follow_information_for_user(user) + assert user.info.follower_count == 527 + assert user.info.following_count == 267 + end + + test "detects hidden followers" do + mock(fn env -> + case env.url do + "http://localhost:4001/users/masto_closed/followers?page=1" -> + %Tesla.Env{status: 403, body: ""} + + "http://localhost:4001/users/masto_closed/following?page=1" -> + %Tesla.Env{ + status: 200, + body: + Jason.encode!(%{ + "id" => "http://localhost:4001/users/masto_closed/following?page=1", + "type" => "OrderedCollectionPage" + }) + } + + _ -> + apply(HttpRequestMock, :request, [env]) + end + end) + + user = + insert(:user, + local: false, + follower_address: "http://localhost:4001/users/masto_closed/followers", + following_address: "http://localhost:4001/users/masto_closed/following" + ) + + {:ok, user} = ActivityPub.fetch_follow_information_for_user(user) + assert user.info.hide_followers == true + assert user.info.hide_follows == false + end + + test "detects hidden follows" do + mock(fn env -> + case env.url do + "http://localhost:4001/users/masto_closed/following?page=1" -> + %Tesla.Env{status: 403, body: ""} + + "http://localhost:4001/users/masto_closed/followers?page=1" -> + %Tesla.Env{ + status: 200, + body: + Jason.encode!(%{ + "id" => "http://localhost:4001/users/masto_closed/followers?page=1", + "type" => "OrderedCollectionPage" + }) + } + + _ -> + apply(HttpRequestMock, :request, [env]) + end + end) + + user = + insert(:user, + local: false, + follower_address: "http://localhost:4001/users/masto_closed/followers", + following_address: "http://localhost:4001/users/masto_closed/following" + ) + + {:ok, user} = ActivityPub.fetch_follow_information_for_user(user) + assert user.info.hide_followers == false + assert user.info.hide_follows == true + end + end end From 0c2dcb4c69ed340d02a4b20a4f341f1d9aaaba38 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sun, 14 Jul 2019 01:58:39 +0300 Subject: [PATCH 05/48] Add follow information refetching after following/unfollowing --- lib/pleroma/user.ex | 91 +++++++++++++++----- lib/pleroma/user/info.ex | 10 +++ lib/pleroma/web/activity_pub/activity_pub.ex | 21 +++-- test/web/activity_pub/activity_pub_test.exs | 18 ++-- 4 files changed, 98 insertions(+), 42 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index c252e8bff..2e9b01205 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -406,6 +406,8 @@ def follow(%User{} = follower, %User{info: info} = followed) do {1, [follower]} = Repo.update_all(q, []) + follower = maybe_update_following_count(follower) + {:ok, _} = update_follower_count(followed) set_cache(follower) @@ -425,6 +427,8 @@ def unfollow(%User{} = follower, %User{} = followed) do {1, [follower]} = Repo.update_all(q, []) + follower = maybe_update_following_count(follower) + {:ok, followed} = update_follower_count(followed) set_cache(follower) @@ -698,32 +702,75 @@ def update_note_count(%User{} = user) do |> update_and_set_cache() end - def update_follower_count(%User{} = user) do - follower_count_query = - User.Query.build(%{followers: user, deactivated: false}) - |> select([u], %{count: count(u.id)}) + def maybe_fetch_follow_information(user) do + with {:ok, user} <- fetch_follow_information(user) do + user + else + e -> + Logger.error( + "Follower/Following counter update for #{user.ap_id} failed.\n" <> inspect(e) + ) - User - |> where(id: ^user.id) - |> join(:inner, [u], s in subquery(follower_count_query)) - |> update([u, s], - set: [ - info: - fragment( - "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)", - u.info, - s.count - ) - ] - ) - |> select([u], u) - |> Repo.update_all([]) - |> case do - {1, [user]} -> set_cache(user) - _ -> {:error, user} + user end end + def fetch_follow_information(user) do + with {:ok, info} <- ActivityPub.fetch_follow_information_for_user(user) do + info_cng = User.Info.follow_information_update(user.info, info) + + changeset = + user + |> change() + |> put_embed(:info, info_cng) + + update_and_set_cache(changeset) + else + {:error, _} = e -> e + e -> {:error, e} + end + end + + def update_follower_count(%User{} = user) do + unless user.local == false and Pleroma.Config.get([:instance, :external_user_synchronization]) do + follower_count_query = + User.Query.build(%{followers: user, deactivated: false}) + |> select([u], %{count: count(u.id)}) + + User + |> where(id: ^user.id) + |> join(:inner, [u], s in subquery(follower_count_query)) + |> update([u, s], + set: [ + info: + fragment( + "jsonb_set(?, '{follower_count}', ?::varchar::jsonb, true)", + u.info, + s.count + ) + ] + ) + |> select([u], u) + |> Repo.update_all([]) + |> case do + {1, [user]} -> set_cache(user) + _ -> {:error, user} + end + else + {:ok, maybe_fetch_follow_information(user)} + end + end + + def maybe_update_following_count(%User{local: false} = user) do + if Pleroma.Config.get([:instance, :external_user_synchronization]) do + {:ok, maybe_fetch_follow_information(user)} + else + user + end + end + + def maybe_update_following_count(user), do: user + def remove_duplicated_following(%User{following: following} = user) do uniq_following = Enum.uniq(following) diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index 67e8801ea..4cc3f2f2c 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -330,4 +330,14 @@ def remove_reblog_mute(info, ap_id) do cast(info, params, [:muted_reblogs]) end + + def follow_information_update(info, params) do + info + |> cast(params, [ + :hide_followers, + :hide_follows, + :follower_count, + :following_count + ]) + end end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index c821ba45f..2dd9dbf7f 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -1022,15 +1022,13 @@ def fetch_follow_information_for_user(user) do Fetcher.fetch_and_contain_remote_object_from_id(user.follower_address), followers_count when is_integer(followers_count) <- followers_data["totalItems"], {:ok, hide_followers} <- collection_private(followers_data) do - info = %{ - hide_follows: hide_follows, - follower_count: followers_count, - following_count: following_count, - hide_followers: hide_followers - } - - info = Map.merge(user.info, info) - {:ok, Map.put(user, :info, info)} + {:ok, + %{ + hide_follows: hide_follows, + follower_count: followers_count, + following_count: following_count, + hide_followers: hide_followers + }} else {:error, _} = e -> e @@ -1043,8 +1041,9 @@ def fetch_follow_information_for_user(user) do defp maybe_update_follow_information(data) do with {:enabled, true} <- {:enabled, Pleroma.Config.get([:instance, :external_user_synchronization])}, - {:ok, data} <- fetch_follow_information_for_user(data) do - data + {:ok, info} <- fetch_follow_information_for_user(data) do + info = Map.merge(data.info, info) + Map.put(data, :info, info) else {:enabled, false} -> data diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 448ffbf54..24d8493fe 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -1232,9 +1232,9 @@ test "syncronizes following/followers counters" do following_address: "http://localhost:4001/users/fuser2/following" ) - {:ok, user} = ActivityPub.fetch_follow_information_for_user(user) - assert user.info.follower_count == 527 - assert user.info.following_count == 267 + {:ok, info} = ActivityPub.fetch_follow_information_for_user(user) + assert info.follower_count == 527 + assert info.following_count == 267 end test "detects hidden followers" do @@ -1265,9 +1265,9 @@ test "detects hidden followers" do following_address: "http://localhost:4001/users/masto_closed/following" ) - {:ok, user} = ActivityPub.fetch_follow_information_for_user(user) - assert user.info.hide_followers == true - assert user.info.hide_follows == false + {:ok, info} = ActivityPub.fetch_follow_information_for_user(user) + assert info.hide_followers == true + assert info.hide_follows == false end test "detects hidden follows" do @@ -1298,9 +1298,9 @@ test "detects hidden follows" do following_address: "http://localhost:4001/users/masto_closed/following" ) - {:ok, user} = ActivityPub.fetch_follow_information_for_user(user) - assert user.info.hide_followers == false - assert user.info.hide_follows == true + {:ok, info} = ActivityPub.fetch_follow_information_for_user(user) + assert info.hide_followers == false + assert info.hide_follows == true end end end From d4ee76ab6355db0bed59b5126fe04d3399561798 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sat, 20 Jul 2019 18:52:41 +0000 Subject: [PATCH 06/48] Apply suggestion to lib/pleroma/user.ex --- lib/pleroma/user.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 2e9b01205..956ec6240 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -708,7 +708,7 @@ def maybe_fetch_follow_information(user) do else e -> Logger.error( - "Follower/Following counter update for #{user.ap_id} failed.\n" <> inspect(e) + "Follower/Following counter update for #{user.ap_id} failed.\n#{inspect(e)}" ) user From c3ecaea64dd377b586e3b2a5316e90884ec78fe6 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sat, 20 Jul 2019 18:53:00 +0000 Subject: [PATCH 07/48] Apply suggestion to lib/pleroma/object/fetcher.ex --- lib/pleroma/object/fetcher.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/object/fetcher.ex b/lib/pleroma/object/fetcher.ex index bc3e7e5bc..1e60d0082 100644 --- a/lib/pleroma/object/fetcher.ex +++ b/lib/pleroma/object/fetcher.ex @@ -97,7 +97,8 @@ def fetch_and_contain_remote_object_from_id(id) when is_binary(id) do end end - def fetch_and_contain_remote_object_from_id(_id) do + def fetch_and_contain_remote_object_from_id(%{"id" => id), do: fetch_and_contain_remote_object_from_id(id) + def fetch_and_contain_remote_object_from_id(_id), do: {:error, "id must be a string"} {:error, "id must be a string"} end end From 51b3b6d8164de9196159dc7de8d5abf0c4fa1bce Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 30 Jul 2019 16:36:05 +0000 Subject: [PATCH 08/48] Admin changes --- CHANGELOG.md | 1 + docs/api/admin_api.md | 23 ++++++++++++ lib/mix/tasks/pleroma/config.ex | 2 +- .../web/admin_api/admin_api_controller.ex | 10 +++++ lib/pleroma/web/router.ex | 2 + .../admin_api/admin_api_controller_test.exs | 37 +++++++++++++++++++ 6 files changed, 74 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e77fe4f3d..acd55362d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Admin API: Return avatar and display name when querying users - Admin API: Allow querying user by ID - Admin API: Added support for `tuples`. +- Admin API: Added endpoints to run mix tasks pleroma.config migrate_to_db & pleroma.config migrate_from_db - Added synchronization of following/followers counters for external users - Configuration: `enabled` option for `Pleroma.Emails.Mailer`, defaulting to `false`. - Configuration: Pleroma.Plugs.RateLimiter `bucket_name`, `params` options. diff --git a/docs/api/admin_api.md b/docs/api/admin_api.md index ca9303227..22873dde9 100644 --- a/docs/api/admin_api.md +++ b/docs/api/admin_api.md @@ -575,6 +575,29 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret - 404 Not Found `"Not found"` - On success: 200 OK `{}` + +## `/api/pleroma/admin/config/migrate_to_db` +### Run mix task pleroma.config migrate_to_db +Copy settings on key `:pleroma` to DB. +- Method `GET` +- Params: none +- Response: + +```json +{} +``` + +## `/api/pleroma/admin/config/migrate_from_db` +### Run mix task pleroma.config migrate_from_db +Copy all settings from DB to `config/prod.exported_from_db.secret.exs` with deletion from DB. +- Method `GET` +- Params: none +- Response: + +```json +{} +``` + ## `/api/pleroma/admin/config` ### List config settings List config settings only works with `:pleroma => :instance => :dynamic_configuration` setting to `true`. diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex index a7d0fac5d..462940e7e 100644 --- a/lib/mix/tasks/pleroma/config.ex +++ b/lib/mix/tasks/pleroma/config.ex @@ -15,7 +15,7 @@ defmodule Mix.Tasks.Pleroma.Config do mix pleroma.config migrate_to_db - ## Transfers config from DB to file. + ## Transfers config from DB to file `config/env.exported_from_db.secret.exs` mix pleroma.config migrate_from_db ENV """ diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 1ae5acd91..fcda57b3e 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -379,6 +379,16 @@ def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do end end + def migrate_to_db(conn, _params) do + Mix.Tasks.Pleroma.Config.run(["migrate_to_db"]) + json(conn, %{}) + end + + def migrate_from_db(conn, _params) do + Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "true"]) + json(conn, %{}) + end + def config_show(conn, _params) do configs = Pleroma.Repo.all(Config) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 0689d69fb..d475fc973 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -196,6 +196,8 @@ defmodule Pleroma.Web.Router do get("/config", AdminAPIController, :config_show) post("/config", AdminAPIController, :config_update) + get("/config/migrate_to_db", AdminAPIController, :migrate_to_db) + get("/config/migrate_from_db", AdminAPIController, :migrate_from_db) end scope "/", Pleroma.Web.TwitterAPI do diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 6dda4ae51..824ad23e6 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -1916,6 +1916,43 @@ test "queues key as atom", %{conn: conn} do end end + describe "config mix tasks run" do + setup %{conn: conn} do + admin = insert(:user, info: %{is_admin: true}) + + temp_file = "config/test.exported_from_db.secret.exs" + + on_exit(fn -> + :ok = File.rm(temp_file) + end) + + dynamic = Pleroma.Config.get([:instance, :dynamic_configuration]) + + Pleroma.Config.put([:instance, :dynamic_configuration], true) + + on_exit(fn -> + Pleroma.Config.put([:instance, :dynamic_configuration], dynamic) + end) + + %{conn: assign(conn, :user, admin), admin: admin} + end + + test "transfer settings to DB and to file", %{conn: conn, admin: admin} do + assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) == [] + conn = get(conn, "/api/pleroma/admin/config/migrate_to_db") + assert json_response(conn, 200) == %{} + assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) > 0 + + conn = + build_conn() + |> assign(:user, admin) + |> get("/api/pleroma/admin/config/migrate_from_db") + + assert json_response(conn, 200) == %{} + assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) == [] + end + end + describe "GET /api/pleroma/admin/users/:nickname/statuses" do setup do admin = insert(:user, info: %{is_admin: true}) From f42719506c539a4058c52d3a6e4a828948ac74ce Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 31 Jul 2019 14:20:34 +0300 Subject: [PATCH 09/48] Fix credo issues --- lib/pleroma/user.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index fd1c0a544..7acf1e53c 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -741,7 +741,7 @@ def fetch_follow_information(user) do end def update_follower_count(%User{} = user) do - unless user.local == false and Pleroma.Config.get([:instance, :external_user_synchronization]) do + unless !user.local and Pleroma.Config.get([:instance, :external_user_synchronization]) do follower_count_query = User.Query.build(%{followers: user, deactivated: false}) |> select([u], %{count: count(u.id)}) From 58443d0cd683c227199eb34d660191292e487a14 Mon Sep 17 00:00:00 2001 From: Maksim Date: Wed, 31 Jul 2019 15:14:36 +0000 Subject: [PATCH 10/48] tests for TwitterApi/UtilController --- lib/pleroma/user.ex | 2 + .../controllers/util_controller.ex | 186 ++++---- test/support/http_request_mock.ex | 4 + test/web/twitter_api/util_controller_test.exs | 404 +++++++++++++++++- 4 files changed, 503 insertions(+), 93 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 6e2fd3af8..1adb82f32 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -226,6 +226,7 @@ def password_update_changeset(struct, params) do |> put_password_hash end + @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()} def reset_password(%User{id: user_id} = user, data) do multi = Multi.new() @@ -330,6 +331,7 @@ def needs_update?(%User{local: false} = user) do def needs_update?(_), do: true + @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()} def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do {:ok, follower} end diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 5c73a615d..3405bd3b7 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -18,6 +18,8 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do alias Pleroma.Web.CommonAPI alias Pleroma.Web.WebFinger + plug(Pleroma.Plugs.SetFormatPlug when action in [:config, :version]) + def help_test(conn, _params) do json(conn, "ok") end @@ -58,27 +60,25 @@ def remote_follow(%{assigns: %{user: user}} = conn, %{"acct" => acct}) do %Activity{id: activity_id} = Activity.get_create_by_object_ap_id(object.data["id"]) redirect(conn, to: "/notice/#{activity_id}") else - {err, followee} = User.get_or_fetch(acct) - avatar = User.avatar_url(followee) - name = followee.nickname - id = followee.id - - if !!user do + with {:ok, followee} <- User.get_or_fetch(acct) do conn - |> render("follow.html", %{error: err, acct: acct, avatar: avatar, name: name, id: id}) - else - conn - |> render("follow_login.html", %{ + |> render(follow_template(user), %{ error: false, acct: acct, - avatar: avatar, - name: name, - id: id + avatar: User.avatar_url(followee), + name: followee.nickname, + id: followee.id }) + else + {:error, _reason} -> + render(conn, follow_template(user), %{error: :error}) end end end + defp follow_template(%User{} = _user), do: "follow.html" + defp follow_template(_), do: "follow_login.html" + defp is_status?(acct) do case Pleroma.Object.Fetcher.fetch_and_contain_remote_object_from_id(acct) do {:ok, %{"type" => type}} when type in ["Article", "Note", "Video", "Page", "Question"] -> @@ -92,48 +92,53 @@ defp is_status?(acct) do def do_remote_follow(conn, %{ "authorization" => %{"name" => username, "password" => password, "id" => id} }) do - followee = User.get_cached_by_id(id) - avatar = User.avatar_url(followee) - name = followee.nickname - - with %User{} = user <- User.get_cached_by_nickname(username), - true <- AuthenticationPlug.checkpw(password, user.password_hash), - %User{} = _followed <- User.get_cached_by_id(id), + with %User{} = followee <- User.get_cached_by_id(id), + {_, %User{} = user, _} <- {:auth, User.get_cached_by_nickname(username), followee}, + {_, true, _} <- { + :auth, + AuthenticationPlug.checkpw(password, user.password_hash), + followee + }, {:ok, _follower, _followee, _activity} <- CommonAPI.follow(user, followee) do conn |> render("followed.html", %{error: false}) else # Was already following user {:error, "Could not follow user:" <> _rest} -> - render(conn, "followed.html", %{error: false}) + render(conn, "followed.html", %{error: "Error following account"}) - _e -> + {:auth, _, followee} -> conn |> render("follow_login.html", %{ error: "Wrong username or password", id: id, - name: name, - avatar: avatar + name: followee.nickname, + avatar: User.avatar_url(followee) }) + + e -> + Logger.debug("Remote follow failed with error #{inspect(e)}") + render(conn, "followed.html", %{error: "Something went wrong."}) end end def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}}) do - with %User{} = followee <- User.get_cached_by_id(id), + with {:fetch_user, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)}, {:ok, _follower, _followee, _activity} <- CommonAPI.follow(user, followee) do conn |> render("followed.html", %{error: false}) else # Was already following user {:error, "Could not follow user:" <> _rest} -> - conn - |> render("followed.html", %{error: false}) + render(conn, "followed.html", %{error: "Error following account"}) + + {:fetch_user, error} -> + Logger.debug("Remote follow failed with error #{inspect(error)}") + render(conn, "followed.html", %{error: "Could not find user"}) e -> Logger.debug("Remote follow failed with error #{inspect(e)}") - - conn - |> render("followed.html", %{error: inspect(e)}) + render(conn, "followed.html", %{error: "Something went wrong."}) end end @@ -148,67 +153,70 @@ def notifications_read(%{assigns: %{user: user}} = conn, %{"id" => notification_ end end + def config(%{assigns: %{format: "xml"}} = conn, _params) do + instance = Pleroma.Config.get(:instance) + + response = """ + + + #{Keyword.get(instance, :name)} + #{Web.base_url()} + #{Keyword.get(instance, :limit)} + #{!Keyword.get(instance, :registrations_open)} + + + """ + + conn + |> put_resp_content_type("application/xml") + |> send_resp(200, response) + end + def config(conn, _params) do instance = Pleroma.Config.get(:instance) - case get_format(conn) do - "xml" -> - response = """ - - - #{Keyword.get(instance, :name)} - #{Web.base_url()} - #{Keyword.get(instance, :limit)} - #{!Keyword.get(instance, :registrations_open)} - - - """ + vapid_public_key = Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) - conn - |> put_resp_content_type("application/xml") - |> send_resp(200, response) + uploadlimit = %{ + uploadlimit: to_string(Keyword.get(instance, :upload_limit)), + avatarlimit: to_string(Keyword.get(instance, :avatar_upload_limit)), + backgroundlimit: to_string(Keyword.get(instance, :background_upload_limit)), + bannerlimit: to_string(Keyword.get(instance, :banner_upload_limit)) + } - _ -> - vapid_public_key = Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) + data = %{ + name: Keyword.get(instance, :name), + description: Keyword.get(instance, :description), + server: Web.base_url(), + textlimit: to_string(Keyword.get(instance, :limit)), + uploadlimit: uploadlimit, + closed: bool_to_val(Keyword.get(instance, :registrations_open), "0", "1"), + private: bool_to_val(Keyword.get(instance, :public, true), "0", "1"), + vapidPublicKey: vapid_public_key, + accountActivationRequired: + bool_to_val(Keyword.get(instance, :account_activation_required, false)), + invitesEnabled: bool_to_val(Keyword.get(instance, :invites_enabled, false)), + safeDMMentionsEnabled: bool_to_val(Pleroma.Config.get([:instance, :safe_dm_mentions])) + } - uploadlimit = %{ - uploadlimit: to_string(Keyword.get(instance, :upload_limit)), - avatarlimit: to_string(Keyword.get(instance, :avatar_upload_limit)), - backgroundlimit: to_string(Keyword.get(instance, :background_upload_limit)), - bannerlimit: to_string(Keyword.get(instance, :banner_upload_limit)) - } - - data = %{ - name: Keyword.get(instance, :name), - description: Keyword.get(instance, :description), - server: Web.base_url(), - textlimit: to_string(Keyword.get(instance, :limit)), - uploadlimit: uploadlimit, - closed: if(Keyword.get(instance, :registrations_open), do: "0", else: "1"), - private: if(Keyword.get(instance, :public, true), do: "0", else: "1"), - vapidPublicKey: vapid_public_key, - accountActivationRequired: - if(Keyword.get(instance, :account_activation_required, false), do: "1", else: "0"), - invitesEnabled: if(Keyword.get(instance, :invites_enabled, false), do: "1", else: "0"), - safeDMMentionsEnabled: - if(Pleroma.Config.get([:instance, :safe_dm_mentions]), do: "1", else: "0") - } + managed_config = Keyword.get(instance, :managed_config) + data = + if managed_config do pleroma_fe = Pleroma.Config.get([:frontend_configurations, :pleroma_fe]) + Map.put(data, "pleromafe", pleroma_fe) + else + data + end - managed_config = Keyword.get(instance, :managed_config) - - data = - if managed_config do - data |> Map.put("pleromafe", pleroma_fe) - else - data - end - - json(conn, %{site: data}) - end + json(conn, %{site: data}) end + defp bool_to_val(true), do: "1" + defp bool_to_val(_), do: "0" + defp bool_to_val(true, val, _), do: val + defp bool_to_val(_, _, val), do: val + def frontend_configurations(conn, _params) do config = Pleroma.Config.get(:frontend_configurations, %{}) @@ -217,20 +225,16 @@ def frontend_configurations(conn, _params) do json(conn, config) end - def version(conn, _params) do + def version(%{assigns: %{format: "xml"}} = conn, _params) do version = Pleroma.Application.named_version() - case get_format(conn) do - "xml" -> - response = "#{version}" + conn + |> put_resp_content_type("application/xml") + |> send_resp(200, "#{version}") + end - conn - |> put_resp_content_type("application/xml") - |> send_resp(200, response) - - _ -> - json(conn, version) - end + def version(conn, _params) do + json(conn, Pleroma.Application.named_version()) end def emoji(conn, _params) do diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 2ed5f5042..d767ab9d4 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -51,6 +51,10 @@ def get("https://mastodon.social/users/emelie", _, _, _) do }} end + def get("https://mastodon.social/users/not_found", _, _, _) do + {:ok, %Tesla.Env{status: 404}} + end + def get("https://mastodon.sdf.org/users/rinpatch", _, _, _) do {:ok, %Tesla.Env{ diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs index 3d699e1df..640579c09 100644 --- a/test/web/twitter_api/util_controller_test.exs +++ b/test/web/twitter_api/util_controller_test.exs @@ -14,6 +14,17 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do setup do Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + + instance_config = Pleroma.Config.get([:instance]) + pleroma_fe = Pleroma.Config.get([:frontend_configurations, :pleroma_fe]) + deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked]) + + on_exit(fn -> + Pleroma.Config.put([:instance], instance_config) + Pleroma.Config.put([:frontend_configurations, :pleroma_fe], pleroma_fe) + Pleroma.Config.put([:user, :deny_follow_blocked], deny_follow_blocked) + end) + :ok end @@ -31,6 +42,35 @@ test "it returns HTTP 200", %{conn: conn} do assert response == "job started" end + test "it imports follow lists from file", %{conn: conn} do + user1 = insert(:user) + user2 = insert(:user) + + with_mocks([ + {File, [], + read!: fn "follow_list.txt" -> + "Account address,Show boosts\n#{user2.ap_id},true" + end}, + {PleromaJobQueue, [:passthrough], []} + ]) do + response = + conn + |> assign(:user, user1) + |> post("/api/pleroma/follow_import", %{"list" => %Plug.Upload{path: "follow_list.txt"}}) + |> json_response(:ok) + + assert called( + PleromaJobQueue.enqueue( + :background, + User, + [:follow_import, user1, [user2.ap_id]] + ) + ) + + assert response == "job started" + end + end + test "it imports new-style mastodon follow lists", %{conn: conn} do user1 = insert(:user) user2 = insert(:user) @@ -79,6 +119,33 @@ test "it returns HTTP 200", %{conn: conn} do assert response == "job started" end + + test "it imports blocks users from file", %{conn: conn} do + user1 = insert(:user) + user2 = insert(:user) + user3 = insert(:user) + + with_mocks([ + {File, [], read!: fn "blocks_list.txt" -> "#{user2.ap_id} #{user3.ap_id}" end}, + {PleromaJobQueue, [:passthrough], []} + ]) do + response = + conn + |> assign(:user, user1) + |> post("/api/pleroma/blocks_import", %{"list" => %Plug.Upload{path: "blocks_list.txt"}}) + |> json_response(:ok) + + assert called( + PleromaJobQueue.enqueue( + :background, + User, + [:blocks_import, user1, [user2.ap_id, user3.ap_id]] + ) + ) + + assert response == "job started" + end + end end describe "POST /api/pleroma/notifications/read" do @@ -98,6 +165,18 @@ test "it marks a single notification as read", %{conn: conn} do assert Repo.get(Notification, notification1.id).seen refute Repo.get(Notification, notification2.id).seen end + + test "it returns error when notification not found", %{conn: conn} do + user1 = insert(:user) + + response = + conn + |> assign(:user, user1) + |> post("/api/pleroma/notifications/read", %{"id" => "22222222222222"}) + |> json_response(403) + + assert response == %{"error" => "Cannot get notification"} + end end describe "PUT /api/pleroma/notification_settings" do @@ -123,7 +202,63 @@ test "it updates notification settings", %{conn: conn} do end end - describe "GET /api/statusnet/config.json" do + describe "GET /api/statusnet/config" do + test "it returns config in xml format", %{conn: conn} do + instance = Pleroma.Config.get(:instance) + + response = + conn + |> put_req_header("accept", "application/xml") + |> get("/api/statusnet/config") + |> response(:ok) + + assert response == + "\n\n#{Keyword.get(instance, :name)}\n#{ + Pleroma.Web.base_url() + }\n#{Keyword.get(instance, :limit)}\n#{ + !Keyword.get(instance, :registrations_open) + }\n\n\n" + end + + test "it returns config in json format", %{conn: conn} do + instance = Pleroma.Config.get(:instance) + Pleroma.Config.put([:instance, :managed_config], true) + Pleroma.Config.put([:instance, :registrations_open], false) + Pleroma.Config.put([:instance, :invites_enabled], true) + Pleroma.Config.put([:instance, :public], false) + Pleroma.Config.put([:frontend_configurations, :pleroma_fe], %{theme: "asuka-hospital"}) + + response = + conn + |> put_req_header("accept", "application/json") + |> get("/api/statusnet/config") + |> json_response(:ok) + + expected_data = %{ + "site" => %{ + "accountActivationRequired" => "0", + "closed" => "1", + "description" => Keyword.get(instance, :description), + "invitesEnabled" => "1", + "name" => Keyword.get(instance, :name), + "pleromafe" => %{"theme" => "asuka-hospital"}, + "private" => "1", + "safeDMMentionsEnabled" => "0", + "server" => Pleroma.Web.base_url(), + "textlimit" => to_string(Keyword.get(instance, :limit)), + "uploadlimit" => %{ + "avatarlimit" => to_string(Keyword.get(instance, :avatar_upload_limit)), + "backgroundlimit" => to_string(Keyword.get(instance, :background_upload_limit)), + "bannerlimit" => to_string(Keyword.get(instance, :banner_upload_limit)), + "uploadlimit" => to_string(Keyword.get(instance, :upload_limit)) + }, + "vapidPublicKey" => Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) + } + } + + assert response == expected_data + end + test "returns the state of safe_dm_mentions flag", %{conn: conn} do option = Pleroma.Config.get([:instance, :safe_dm_mentions]) Pleroma.Config.put([:instance, :safe_dm_mentions], true) @@ -210,7 +345,7 @@ test "returns json with custom emoji with tags", %{conn: conn} do end end - describe "GET /ostatus_subscribe?acct=...." do + describe "GET /ostatus_subscribe - remote_follow/2" do test "adds status to pleroma instance if the `acct` is a status", %{conn: conn} do conn = get( @@ -230,6 +365,172 @@ test "show follow account page if the `acct` is a account link", %{conn: conn} d assert html_response(response, 200) =~ "Log in to follow" end + + test "show follow page if the `acct` is a account link", %{conn: conn} do + user = insert(:user) + + response = + conn + |> assign(:user, user) + |> get("/ostatus_subscribe?acct=https://mastodon.social/users/emelie") + + assert html_response(response, 200) =~ "Remote follow" + end + + test "show follow page with error when user cannot fecth by `acct` link", %{conn: conn} do + user = insert(:user) + + response = + conn + |> assign(:user, user) + |> get("/ostatus_subscribe?acct=https://mastodon.social/users/not_found") + + assert html_response(response, 200) =~ "Error fetching user" + end + end + + describe "POST /ostatus_subscribe - do_remote_follow/2 with assigned user " do + test "follows user", %{conn: conn} do + user = insert(:user) + user2 = insert(:user) + + response = + conn + |> assign(:user, user) + |> post("/ostatus_subscribe", %{"user" => %{"id" => user2.id}}) + |> response(200) + + assert response =~ "Account followed!" + assert user2.follower_address in refresh_record(user).following + end + + test "returns error when user is deactivated", %{conn: conn} do + user = insert(:user, info: %{deactivated: true}) + user2 = insert(:user) + + response = + conn + |> assign(:user, user) + |> post("/ostatus_subscribe", %{"user" => %{"id" => user2.id}}) + |> response(200) + + assert response =~ "Error following account" + end + + test "returns error when user is blocked", %{conn: conn} do + Pleroma.Config.put([:user, :deny_follow_blocked], true) + user = insert(:user) + user2 = insert(:user) + + {:ok, _user} = Pleroma.User.block(user2, user) + + response = + conn + |> assign(:user, user) + |> post("/ostatus_subscribe", %{"user" => %{"id" => user2.id}}) + |> response(200) + + assert response =~ "Error following account" + end + + test "returns error when followee not found", %{conn: conn} do + user = insert(:user) + + response = + conn + |> assign(:user, user) + |> post("/ostatus_subscribe", %{"user" => %{"id" => "jimm"}}) + |> response(200) + + assert response =~ "Error following account" + end + + test "returns success result when user already in followers", %{conn: conn} do + user = insert(:user) + user2 = insert(:user) + {:ok, _, _, _} = CommonAPI.follow(user, user2) + + response = + conn + |> assign(:user, refresh_record(user)) + |> post("/ostatus_subscribe", %{"user" => %{"id" => user2.id}}) + |> response(200) + + assert response =~ "Account followed!" + end + end + + describe "POST /ostatus_subscribe - do_remote_follow/2 without assigned user " do + test "follows", %{conn: conn} do + user = insert(:user) + user2 = insert(:user) + + response = + conn + |> post("/ostatus_subscribe", %{ + "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id} + }) + |> response(200) + + assert response =~ "Account followed!" + assert user2.follower_address in refresh_record(user).following + end + + test "returns error when followee not found", %{conn: conn} do + user = insert(:user) + + response = + conn + |> post("/ostatus_subscribe", %{ + "authorization" => %{"name" => user.nickname, "password" => "test", "id" => "jimm"} + }) + |> response(200) + + assert response =~ "Error following account" + end + + test "returns error when login invalid", %{conn: conn} do + user = insert(:user) + + response = + conn + |> post("/ostatus_subscribe", %{ + "authorization" => %{"name" => "jimm", "password" => "test", "id" => user.id} + }) + |> response(200) + + assert response =~ "Wrong username or password" + end + + test "returns error when password invalid", %{conn: conn} do + user = insert(:user) + user2 = insert(:user) + + response = + conn + |> post("/ostatus_subscribe", %{ + "authorization" => %{"name" => user.nickname, "password" => "42", "id" => user2.id} + }) + |> response(200) + + assert response =~ "Wrong username or password" + end + + test "returns error when user is blocked", %{conn: conn} do + Pleroma.Config.put([:user, :deny_follow_blocked], true) + user = insert(:user) + user2 = insert(:user) + {:ok, _user} = Pleroma.User.block(user2, user) + + response = + conn + |> post("/ostatus_subscribe", %{ + "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id} + }) + |> response(200) + + assert response =~ "Error following account" + end end describe "GET /api/pleroma/healthcheck" do @@ -311,5 +612,104 @@ test "it returns HTTP 200", %{conn: conn} do assert user.info.deactivated == true end + + test "it returns returns when password invalid", %{conn: conn} do + user = insert(:user) + + response = + conn + |> assign(:user, user) + |> post("/api/pleroma/disable_account", %{"password" => "test1"}) + |> json_response(:ok) + + assert response == %{"error" => "Invalid password."} + user = User.get_cached_by_id(user.id) + + refute user.info.deactivated + end + end + + describe "GET /api/statusnet/version" do + test "it returns version in xml format", %{conn: conn} do + response = + conn + |> put_req_header("accept", "application/xml") + |> get("/api/statusnet/version") + |> response(:ok) + + assert response == "#{Pleroma.Application.named_version()}" + end + + test "it returns version in json format", %{conn: conn} do + response = + conn + |> put_req_header("accept", "application/json") + |> get("/api/statusnet/version") + |> json_response(:ok) + + assert response == "#{Pleroma.Application.named_version()}" + end + end + + describe "POST /main/ostatus - remote_subscribe/2" do + test "renders subscribe form", %{conn: conn} do + user = insert(:user) + + response = + conn + |> post("/main/ostatus", %{"nickname" => user.nickname, "profile" => ""}) + |> response(:ok) + + refute response =~ "Could not find user" + assert response =~ "Remotely follow #{user.nickname}" + end + + test "renders subscribe form with error when user not found", %{conn: conn} do + response = + conn + |> post("/main/ostatus", %{"nickname" => "nickname", "profile" => ""}) + |> response(:ok) + + assert response =~ "Could not find user" + refute response =~ "Remotely follow" + end + + test "it redirect to webfinger url", %{conn: conn} do + user = insert(:user) + user2 = insert(:user, ap_id: "shp@social.heldscal.la") + + conn = + conn + |> post("/main/ostatus", %{ + "user" => %{"nickname" => user.nickname, "profile" => user2.ap_id} + }) + + assert redirected_to(conn) == + "https://social.heldscal.la/main/ostatussub?profile=#{user.ap_id}" + end + + test "it renders form with error when use not found", %{conn: conn} do + user2 = insert(:user, ap_id: "shp@social.heldscal.la") + + response = + conn + |> post("/main/ostatus", %{"user" => %{"nickname" => "jimm", "profile" => user2.ap_id}}) + |> response(:ok) + + assert response =~ "Something went wrong." + end + end + + test "it returns new captcha", %{conn: conn} do + with_mock Pleroma.Captcha, + new: fn -> "test_captcha" end do + resp = + conn + |> get("/api/pleroma/captcha") + |> response(200) + + assert resp == "\"test_captcha\"" + assert called(Pleroma.Captcha.new()) + end end end From 301ea0dc0466371032f44f3e936d1b951ed9784c Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 31 Jul 2019 19:37:55 +0300 Subject: [PATCH 11/48] Add tests for counters being updated on follow --- lib/pleroma/user.ex | 2 +- .../masto_closed_followers_page.json | 1 + .../masto_closed_following_page.json | 1 + test/support/http_request_mock.ex | 16 ++++ test/user_test.exs | 74 +++++++++++++++++++ test/web/activity_pub/activity_pub_test.exs | 20 ----- 6 files changed, 93 insertions(+), 21 deletions(-) create mode 100644 test/fixtures/users_mock/masto_closed_followers_page.json create mode 100644 test/fixtures/users_mock/masto_closed_following_page.json diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 7acf1e53c..69835f3dd 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -741,7 +741,7 @@ def fetch_follow_information(user) do end def update_follower_count(%User{} = user) do - unless !user.local and Pleroma.Config.get([:instance, :external_user_synchronization]) do + if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do follower_count_query = User.Query.build(%{followers: user, deactivated: false}) |> select([u], %{count: count(u.id)}) diff --git a/test/fixtures/users_mock/masto_closed_followers_page.json b/test/fixtures/users_mock/masto_closed_followers_page.json new file mode 100644 index 000000000..04ab0c4d3 --- /dev/null +++ b/test/fixtures/users_mock/masto_closed_followers_page.json @@ -0,0 +1 @@ +{"@context":"https://www.w3.org/ns/activitystreams","id":"http://localhost:4001/users/masto_closed/followers?page=1","type":"OrderedCollectionPage","totalItems":437,"next":"http://localhost:4001/users/masto_closed/followers?page=2","partOf":"http://localhost:4001/users/masto_closed/followers","orderedItems":["https://testing.uguu.ltd/users/rin","https://patch.cx/users/rin","https://letsalllovela.in/users/xoxo","https://pleroma.site/users/crushv","https://aria.company/users/boris","https://kawen.space/users/crushv","https://freespeech.host/users/cvcvcv","https://pleroma.site/users/picpub","https://pixelfed.social/users/nosleep","https://boopsnoot.gq/users/5c1896d162f7d337f90492a3","https://pikachu.rocks/users/waifu","https://royal.crablettesare.life/users/crablettes"]} diff --git a/test/fixtures/users_mock/masto_closed_following_page.json b/test/fixtures/users_mock/masto_closed_following_page.json new file mode 100644 index 000000000..8d8324699 --- /dev/null +++ b/test/fixtures/users_mock/masto_closed_following_page.json @@ -0,0 +1 @@ +{"@context":"https://www.w3.org/ns/activitystreams","id":"http://localhost:4001/users/masto_closed/following?page=1","type":"OrderedCollectionPage","totalItems":152,"next":"http://localhost:4001/users/masto_closed/following?page=2","partOf":"http://localhost:4001/users/masto_closed/following","orderedItems":["https://testing.uguu.ltd/users/rin","https://patch.cx/users/rin","https://letsalllovela.in/users/xoxo","https://pleroma.site/users/crushv","https://aria.company/users/boris","https://kawen.space/users/crushv","https://freespeech.host/users/cvcvcv","https://pleroma.site/users/picpub","https://pixelfed.social/users/nosleep","https://boopsnoot.gq/users/5c1896d162f7d337f90492a3","https://pikachu.rocks/users/waifu","https://royal.crablettesare.life/users/crablettes"]} diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 2ed5f5042..bdfe43b28 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -792,6 +792,14 @@ def get("http://localhost:4001/users/masto_closed/followers", _, _, _) do }} end + def get("http://localhost:4001/users/masto_closed/followers?page=1", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/users_mock/masto_closed_followers_page.json") + }} + end + def get("http://localhost:4001/users/masto_closed/following", _, _, _) do {:ok, %Tesla.Env{ @@ -800,6 +808,14 @@ def get("http://localhost:4001/users/masto_closed/following", _, _, _) do }} end + def get("http://localhost:4001/users/masto_closed/following?page=1", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/users_mock/masto_closed_following_page.json") + }} + end + def get("http://localhost:4001/users/fuser2/followers", _, _, _) do {:ok, %Tesla.Env{ diff --git a/test/user_test.exs b/test/user_test.exs index 556df45fd..7ec241c25 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -1393,4 +1393,78 @@ test "performs update cache if user updated" do assert %User{bio: "test-bio"} = User.get_cached_by_ap_id(user.ap_id) end end + + describe "following/followers synchronization" do + setup do + sync = Pleroma.Config.get([:instance, :external_user_synchronization]) + on_exit(fn -> Pleroma.Config.put([:instance, :external_user_synchronization], sync) end) + end + + test "updates the counters normally on following/getting a follow when disabled" do + Pleroma.Config.put([:instance, :external_user_synchronization], false) + user = insert(:user) + + other_user = + insert(:user, + local: false, + follower_address: "http://localhost:4001/users/masto_closed/followers", + following_address: "http://localhost:4001/users/masto_closed/following", + info: %{ap_enabled: true} + ) + + assert User.user_info(other_user).following_count == 0 + assert User.user_info(other_user).follower_count == 0 + + {:ok, user} = Pleroma.User.follow(user, other_user) + other_user = Pleroma.User.get_by_id(other_user.id) + + assert User.user_info(user).following_count == 1 + assert User.user_info(other_user).follower_count == 1 + end + + test "syncronizes the counters with the remote instance for the followed when enabled" do + Pleroma.Config.put([:instance, :external_user_synchronization], false) + + user = insert(:user) + + other_user = + insert(:user, + local: false, + follower_address: "http://localhost:4001/users/masto_closed/followers", + following_address: "http://localhost:4001/users/masto_closed/following", + info: %{ap_enabled: true} + ) + + assert User.user_info(other_user).following_count == 0 + assert User.user_info(other_user).follower_count == 0 + + Pleroma.Config.put([:instance, :external_user_synchronization], true) + {:ok, _user} = User.follow(user, other_user) + other_user = User.get_by_id(other_user.id) + + assert User.user_info(other_user).follower_count == 437 + end + + test "syncronizes the counters with the remote instance for the follower when enabled" do + Pleroma.Config.put([:instance, :external_user_synchronization], false) + + user = insert(:user) + + other_user = + insert(:user, + local: false, + follower_address: "http://localhost:4001/users/masto_closed/followers", + following_address: "http://localhost:4001/users/masto_closed/following", + info: %{ap_enabled: true} + ) + + assert User.user_info(other_user).following_count == 0 + assert User.user_info(other_user).follower_count == 0 + + Pleroma.Config.put([:instance, :external_user_synchronization], true) + {:ok, other_user} = User.follow(other_user, user) + + assert User.user_info(other_user).following_count == 152 + end + end end diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 853c93ab5..3d9a678dd 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -1149,16 +1149,6 @@ test "detects hidden followers" do "http://localhost:4001/users/masto_closed/followers?page=1" -> %Tesla.Env{status: 403, body: ""} - "http://localhost:4001/users/masto_closed/following?page=1" -> - %Tesla.Env{ - status: 200, - body: - Jason.encode!(%{ - "id" => "http://localhost:4001/users/masto_closed/following?page=1", - "type" => "OrderedCollectionPage" - }) - } - _ -> apply(HttpRequestMock, :request, [env]) end @@ -1182,16 +1172,6 @@ test "detects hidden follows" do "http://localhost:4001/users/masto_closed/following?page=1" -> %Tesla.Env{status: 403, body: ""} - "http://localhost:4001/users/masto_closed/followers?page=1" -> - %Tesla.Env{ - status: 200, - body: - Jason.encode!(%{ - "id" => "http://localhost:4001/users/masto_closed/followers?page=1", - "type" => "OrderedCollectionPage" - }) - } - _ -> apply(HttpRequestMock, :request, [env]) end From f72e0b7caddd96da67269552db3102733e4a2581 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Wed, 31 Jul 2019 17:23:16 +0000 Subject: [PATCH 12/48] ostatus: explicitly disallow protocol downgrade from activitypub This closes embargoed bug #1135. --- CHANGELOG.md | 3 ++ .../web/ostatus/handlers/follow_handler.ex | 2 +- .../web/ostatus/handlers/note_handler.ex | 2 +- .../web/ostatus/handlers/unfollow_handler.ex | 2 +- lib/pleroma/web/ostatus/ostatus.ex | 17 +++++-- test/web/ostatus/ostatus_test.exs | 48 +++++++++++++++++-- 6 files changed, 63 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acd55362d..b02ed243b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ 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] +### Security +- OStatus: eliminate the possibility of a protocol downgrade attack. + ### Changed - **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 diff --git a/lib/pleroma/web/ostatus/handlers/follow_handler.ex b/lib/pleroma/web/ostatus/handlers/follow_handler.ex index 263d3b2dc..03e4cbbb0 100644 --- a/lib/pleroma/web/ostatus/handlers/follow_handler.ex +++ b/lib/pleroma/web/ostatus/handlers/follow_handler.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Web.OStatus.FollowHandler do alias Pleroma.Web.XML def handle(entry, doc) do - with {:ok, actor} <- OStatus.find_make_or_update_user(doc), + with {:ok, actor} <- OStatus.find_make_or_update_actor(doc), id when not is_nil(id) <- XML.string_from_xpath("/entry/id", entry), followed_uri when not is_nil(followed_uri) <- XML.string_from_xpath("/entry/activity:object/id", entry), diff --git a/lib/pleroma/web/ostatus/handlers/note_handler.ex b/lib/pleroma/web/ostatus/handlers/note_handler.ex index 3005e8f57..7fae14f7b 100644 --- a/lib/pleroma/web/ostatus/handlers/note_handler.ex +++ b/lib/pleroma/web/ostatus/handlers/note_handler.ex @@ -111,7 +111,7 @@ def handle_note(entry, doc \\ nil, options \\ []) do with id <- XML.string_from_xpath("//id", entry), activity when is_nil(activity) <- Activity.get_create_by_object_ap_id_with_object(id), [author] <- :xmerl_xpath.string('//author[1]', doc), - {:ok, actor} <- OStatus.find_make_or_update_user(author), + {:ok, actor} <- OStatus.find_make_or_update_actor(author), content_html <- OStatus.get_content(entry), cw <- OStatus.get_cw(entry), in_reply_to <- XML.string_from_xpath("//thr:in-reply-to[1]/@ref", entry), diff --git a/lib/pleroma/web/ostatus/handlers/unfollow_handler.ex b/lib/pleroma/web/ostatus/handlers/unfollow_handler.ex index 6596ada3b..2062432e3 100644 --- a/lib/pleroma/web/ostatus/handlers/unfollow_handler.ex +++ b/lib/pleroma/web/ostatus/handlers/unfollow_handler.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Web.OStatus.UnfollowHandler do alias Pleroma.Web.XML def handle(entry, doc) do - with {:ok, actor} <- OStatus.find_make_or_update_user(doc), + with {:ok, actor} <- OStatus.find_make_or_update_actor(doc), id when not is_nil(id) <- XML.string_from_xpath("/entry/id", entry), followed_uri when not is_nil(followed_uri) <- XML.string_from_xpath("/entry/activity:object/id", entry), diff --git a/lib/pleroma/web/ostatus/ostatus.ex b/lib/pleroma/web/ostatus/ostatus.ex index 502410c83..331cbc0b7 100644 --- a/lib/pleroma/web/ostatus/ostatus.ex +++ b/lib/pleroma/web/ostatus/ostatus.ex @@ -56,7 +56,7 @@ def remote_follow_path do def handle_incoming(xml_string, options \\ []) do with doc when doc != :error <- parse_document(xml_string) do - with {:ok, actor_user} <- find_make_or_update_user(doc), + with {:ok, actor_user} <- find_make_or_update_actor(doc), do: Pleroma.Instances.set_reachable(actor_user.ap_id) entries = :xmerl_xpath.string('//entry', doc) @@ -120,7 +120,7 @@ def handle_incoming(xml_string, options \\ []) do end def make_share(entry, doc, retweeted_activity) do - with {:ok, actor} <- find_make_or_update_user(doc), + with {:ok, actor} <- find_make_or_update_actor(doc), %Object{} = object <- Object.normalize(retweeted_activity), id when not is_nil(id) <- string_from_xpath("/entry/id", entry), {:ok, activity, _object} = ActivityPub.announce(actor, object, id, false) do @@ -138,7 +138,7 @@ def handle_share(entry, doc) do end def make_favorite(entry, doc, favorited_activity) do - with {:ok, actor} <- find_make_or_update_user(doc), + with {:ok, actor} <- find_make_or_update_actor(doc), %Object{} = object <- Object.normalize(favorited_activity), id when not is_nil(id) <- string_from_xpath("/entry/id", entry), {:ok, activity, _object} = ActivityPub.like(actor, object, id, false) do @@ -264,11 +264,18 @@ def maybe_update_ostatus(doc, user) do end end - def find_make_or_update_user(doc) do + def find_make_or_update_actor(doc) do uri = string_from_xpath("//author/uri[1]", doc) - with {:ok, user} <- find_or_make_user(uri) do + with {:ok, %User{} = user} <- find_or_make_user(uri), + {:ap_enabled, false} <- {:ap_enabled, User.ap_enabled?(user)} do maybe_update(doc, user) + else + {:ap_enabled, true} -> + {:error, :invalid_protocol} + + _ -> + {:error, :unknown_user} end end diff --git a/test/web/ostatus/ostatus_test.exs b/test/web/ostatus/ostatus_test.exs index 4e8f3a0fc..d244dbcf7 100644 --- a/test/web/ostatus/ostatus_test.exs +++ b/test/web/ostatus/ostatus_test.exs @@ -426,7 +426,7 @@ test "find_or_make_user sets all the nessary input fields" do } end - test "find_make_or_update_user takes an author element and returns an updated user" do + test "find_make_or_update_actor takes an author element and returns an updated user" do uri = "https://social.heldscal.la/user/23211" {:ok, user} = OStatus.find_or_make_user(uri) @@ -439,14 +439,56 @@ test "find_make_or_update_user takes an author element and returns an updated us doc = XML.parse_document(File.read!("test/fixtures/23211.atom")) [author] = :xmerl_xpath.string('//author[1]', doc) - {:ok, user} = OStatus.find_make_or_update_user(author) + {:ok, user} = OStatus.find_make_or_update_actor(author) assert user.avatar["type"] == "Image" assert user.name == old_name assert user.bio == old_bio - {:ok, user_again} = OStatus.find_make_or_update_user(author) + {:ok, user_again} = OStatus.find_make_or_update_actor(author) assert user_again == user end + + test "find_or_make_user disallows protocol downgrade" do + user = insert(:user, %{local: true}) + {:ok, user} = OStatus.find_or_make_user(user.ap_id) + + assert User.ap_enabled?(user) + + user = + insert(:user, %{ + ap_id: "https://social.heldscal.la/user/23211", + info: %{ap_enabled: true}, + local: false + }) + + assert User.ap_enabled?(user) + + {:ok, user} = OStatus.find_or_make_user(user.ap_id) + assert User.ap_enabled?(user) + end + + test "find_make_or_update_actor disallows protocol downgrade" do + user = insert(:user, %{local: true}) + {:ok, user} = OStatus.find_or_make_user(user.ap_id) + + assert User.ap_enabled?(user) + + user = + insert(:user, %{ + ap_id: "https://social.heldscal.la/user/23211", + info: %{ap_enabled: true}, + local: false + }) + + assert User.ap_enabled?(user) + + {:ok, user} = OStatus.find_or_make_user(user.ap_id) + assert User.ap_enabled?(user) + + doc = XML.parse_document(File.read!("test/fixtures/23211.atom")) + [author] = :xmerl_xpath.string('//author[1]', doc) + {:error, :invalid_protocol} = OStatus.find_make_or_update_actor(author) + end end describe "gathering user info from a user id" do From 6eb33e73035789fd9160e697617feb30a3070589 Mon Sep 17 00:00:00 2001 From: Maksim Date: Wed, 31 Jul 2019 18:35:15 +0000 Subject: [PATCH 13/48] test for Pleroma.Web.CommonAPI.Utils.get_by_id_or_ap_id --- lib/pleroma/flake_id.ex | 10 ++++++++++ lib/pleroma/web/common_api/utils.ex | 7 ++++++- test/flake_id_test.exs | 5 +++++ test/web/common_api/common_api_utils_test.exs | 20 +++++++++++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/flake_id.ex b/lib/pleroma/flake_id.ex index 58ab3650d..ca0610abc 100644 --- a/lib/pleroma/flake_id.ex +++ b/lib/pleroma/flake_id.ex @@ -66,6 +66,16 @@ def from_integer(integer) do @spec get :: binary def get, do: to_string(:gen_server.call(:flake, :get)) + # checks that ID is is valid FlakeID + # + @spec is_flake_id?(String.t()) :: boolean + def is_flake_id?(id), do: is_flake_id?(String.to_charlist(id), true) + defp is_flake_id?([c | cs], true) when c >= ?0 and c <= ?9, do: is_flake_id?(cs, true) + defp is_flake_id?([c | cs], true) when c >= ?A and c <= ?Z, do: is_flake_id?(cs, true) + defp is_flake_id?([c | cs], true) when c >= ?a and c <= ?z, do: is_flake_id?(cs, true) + defp is_flake_id?([], true), do: true + defp is_flake_id?(_, _), do: false + # -- Ecto.Type API @impl Ecto.Type def type, do: :uuid diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index d80fffa26..c8a743e8e 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -24,7 +24,12 @@ defmodule Pleroma.Web.CommonAPI.Utils do # This is a hack for twidere. def get_by_id_or_ap_id(id) do activity = - Activity.get_by_id_with_object(id) || Activity.get_create_by_object_ap_id_with_object(id) + with true <- Pleroma.FlakeId.is_flake_id?(id), + %Activity{} = activity <- Activity.get_by_id_with_object(id) do + activity + else + _ -> Activity.get_create_by_object_ap_id_with_object(id) + end activity && if activity.data["type"] == "Create" do diff --git a/test/flake_id_test.exs b/test/flake_id_test.exs index ca2338041..85ed5bbdf 100644 --- a/test/flake_id_test.exs +++ b/test/flake_id_test.exs @@ -39,4 +39,9 @@ test "ecto type behaviour" do assert dump(flake_s) == {:ok, flake} assert dump(flake) == {:ok, flake} end + + test "is_flake_id?/1" do + assert is_flake_id?("9eoozpwTul5mjSEDRI") + refute is_flake_id?("http://example.com/activities/3ebbadd1-eb14-4e20-8118-b6f79c0c7b0b") + end end diff --git a/test/web/common_api/common_api_utils_test.exs b/test/web/common_api/common_api_utils_test.exs index af320f31f..4b5666c29 100644 --- a/test/web/common_api/common_api_utils_test.exs +++ b/test/web/common_api/common_api_utils_test.exs @@ -360,4 +360,24 @@ test "for direct posts, a reply" do assert third_user.ap_id in to end end + + describe "get_by_id_or_ap_id/1" do + test "get activity by id" do + activity = insert(:note_activity) + %Pleroma.Activity{} = note = Utils.get_by_id_or_ap_id(activity.id) + assert note.id == activity.id + end + + test "get activity by ap_id" do + activity = insert(:note_activity) + %Pleroma.Activity{} = note = Utils.get_by_id_or_ap_id(activity.data["object"]) + assert note.id == activity.id + end + + test "get activity by object when type isn't `Create` " do + activity = insert(:like_activity) + %Pleroma.Activity{} = like = Utils.get_by_id_or_ap_id(activity.id) + assert like.data["object"] == activity.data["object"] + end + end end From 813c686dd77e6d441c235b2f7a57ac7911e249af Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 31 Jul 2019 22:05:12 +0300 Subject: [PATCH 14/48] Disallow following locked accounts over OStatus --- lib/pleroma/web/ostatus/handlers/follow_handler.ex | 4 ++++ test/web/ostatus/ostatus_test.exs | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/lib/pleroma/web/ostatus/handlers/follow_handler.ex b/lib/pleroma/web/ostatus/handlers/follow_handler.ex index 03e4cbbb0..24513972e 100644 --- a/lib/pleroma/web/ostatus/handlers/follow_handler.ex +++ b/lib/pleroma/web/ostatus/handlers/follow_handler.ex @@ -14,9 +14,13 @@ def handle(entry, doc) do followed_uri when not is_nil(followed_uri) <- XML.string_from_xpath("/entry/activity:object/id", entry), {:ok, followed} <- OStatus.find_or_make_user(followed_uri), + {:locked, false} <- {:locked, followed.info.locked}, {:ok, activity} <- ActivityPub.follow(actor, followed, id, false) do User.follow(actor, followed) {:ok, activity} + else + {:locked, true} -> + {:error, "It's not possible to follow locked accounts over OStatus"} end end end diff --git a/test/web/ostatus/ostatus_test.exs b/test/web/ostatus/ostatus_test.exs index d244dbcf7..f8d389020 100644 --- a/test/web/ostatus/ostatus_test.exs +++ b/test/web/ostatus/ostatus_test.exs @@ -326,6 +326,14 @@ test "handle incoming follows" do assert User.following?(follower, followed) end + test "refuse following over OStatus if the followed's account is locked" do + incoming = File.read!("test/fixtures/follow.xml") + _user = insert(:user, info: %{locked: true}, ap_id: "https://pawoo.net/users/pekorino") + + {:ok, [{:error, "It's not possible to follow locked accounts over OStatus"}]} = + OStatus.handle_incoming(incoming) + end + test "handle incoming unfollows with existing follow" do incoming_follow = File.read!("test/fixtures/follow.xml") {:ok, [_activity]} = OStatus.handle_incoming(incoming_follow) From def0c49ead94d21a63bdc7323521b6d73ad4c0b2 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 31 Jul 2019 23:03:06 +0300 Subject: [PATCH 15/48] Add a changelog entry for disallowing locked accounts follows over OStatus --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b02ed243b..bd64b2259 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] ### Security - OStatus: eliminate the possibility of a protocol downgrade attack. +- OStatus: prevent following locked accounts, bypassing the approval process. ### Changed - **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 From f98235f2adfff290d95c7353c63225c07e5f86ff Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 1 Aug 2019 16:33:36 +0700 Subject: [PATCH 16/48] Clean up tests output --- test/web/admin_api/admin_api_controller_test.exs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 824ad23e6..f61499a22 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -1922,7 +1922,10 @@ test "queues key as atom", %{conn: conn} do temp_file = "config/test.exported_from_db.secret.exs" + Mix.shell(Mix.Shell.Quiet) + on_exit(fn -> + Mix.shell(Mix.Shell.IO) :ok = File.rm(temp_file) end) From 81412240e6e6ca60a7fcece5eff056722d868d2e Mon Sep 17 00:00:00 2001 From: rinpatch Date: Thu, 1 Aug 2019 14:15:18 +0300 Subject: [PATCH 17/48] Fix Invalid SemVer version generation when the current branch does not have commits ahead of tag/checked out on a tag --- CHANGELOG.md | 1 + mix.exs | 23 ++++++++++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd64b2259..6fdc432ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - 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. - 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 ### Added - MRF: Support for priming the mediaproxy cache (`Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`) diff --git a/mix.exs b/mix.exs index 2a8fe2e9d..dfff53d57 100644 --- a/mix.exs +++ b/mix.exs @@ -190,12 +190,13 @@ defp version(version) do tag = String.trim(tag), {describe, 0} <- System.cmd("git", ["describe", "--tags", "--abbrev=8"]), describe = String.trim(describe), - ahead <- String.replace(describe, tag, "") do + ahead <- String.replace(describe, tag, ""), + ahead <- String.trim_leading(ahead, "-") do {String.replace_prefix(tag, "v", ""), if(ahead != "", do: String.trim(ahead))} else _ -> {commit_hash, 0} = System.cmd("git", ["rev-parse", "--short", "HEAD"]) - {nil, "-0-g" <> String.trim(commit_hash)} + {nil, "0-g" <> String.trim(commit_hash)} end if git_tag && version != git_tag do @@ -207,14 +208,15 @@ defp version(version) do # Branch name as pre-release version component, denoted with a dot branch_name = with {branch_name, 0} <- System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]), + branch_name <- String.trim(branch_name), branch_name <- System.get_env("PLEROMA_BUILD_BRANCH") || branch_name, - true <- branch_name != "master" do + true <- branch_name not in ["master", "HEAD"] do branch_name = branch_name |> String.trim() |> String.replace(identifier_filter, "-") - "." <> branch_name + branch_name end build_name = @@ -234,6 +236,17 @@ defp version(version) do env_override -> env_override end + # Pre-release version, denoted by appending a hyphen + # and a series of dot separated identifiers + pre_release = + [git_pre_release, branch_name] + |> Enum.filter(fn string -> string && string != "" end) + |> Enum.join(".") + |> (fn + "" -> nil + string -> "-" <> String.replace(string, identifier_filter, "-") + end).() + # Build metadata, denoted with a plus sign build_metadata = [build_name, env_name] @@ -244,7 +257,7 @@ defp version(version) do string -> "+" <> String.replace(string, identifier_filter, "-") end).() - [version, git_pre_release, branch_name, build_metadata] + [version, pre_release, build_metadata] |> Enum.filter(fn string -> string && string != "" end) |> Enum.join() end From d93d7779151c811e991e99098e64c1da2c783d68 Mon Sep 17 00:00:00 2001 From: feld Date: Fri, 2 Aug 2019 17:07:09 +0000 Subject: [PATCH 18/48] Fix/mediaproxy whitelist base url --- CHANGELOG.md | 1 + lib/pleroma/web/media_proxy/media_proxy.ex | 14 ++++- .../mastodon_api_controller_test.exs | 34 ----------- test/web/media_proxy/media_proxy_test.exs | 58 ++++++++++++------- 4 files changed, 51 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fdc432ef..4fa9ffd9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - ActivityPub S2S: remote user deletions now work the same as local user deletions. - 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. ### Added - MRF: Support for priming the mediaproxy cache (`Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`) diff --git a/lib/pleroma/web/media_proxy/media_proxy.ex b/lib/pleroma/web/media_proxy/media_proxy.ex index a661e9bb7..1725ab071 100644 --- a/lib/pleroma/web/media_proxy/media_proxy.ex +++ b/lib/pleroma/web/media_proxy/media_proxy.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.MediaProxy do alias Pleroma.Config + alias Pleroma.Upload alias Pleroma.Web @base64_opts [padding: false] @@ -26,7 +27,18 @@ defp local?(url), do: String.starts_with?(url, Pleroma.Web.base_url()) defp whitelisted?(url) do %{host: domain} = URI.parse(url) - Enum.any?(Config.get([:media_proxy, :whitelist]), fn pattern -> + mediaproxy_whitelist = Config.get([:media_proxy, :whitelist]) + + upload_base_url_domain = + if !is_nil(Config.get([Upload, :base_url])) do + [URI.parse(Config.get([Upload, :base_url])).host] + else + [] + end + + whitelist = mediaproxy_whitelist ++ upload_base_url_domain + + Enum.any?(whitelist, fn pattern -> String.equivalent?(domain, pattern) 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 66016c886..e49c4cc22 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -1671,40 +1671,6 @@ test "returns uploaded image", %{conn: conn, image: image} do object = Repo.get(Object, media["id"]) assert object.data["actor"] == User.ap_id(conn.assigns[:user]) end - - test "returns proxied url when media proxy is enabled", %{conn: conn, image: image} do - Pleroma.Config.put([Pleroma.Upload, :base_url], "https://media.pleroma.social") - - proxy_url = "https://cache.pleroma.social" - Pleroma.Config.put([:media_proxy, :enabled], true) - Pleroma.Config.put([:media_proxy, :base_url], proxy_url) - - media = - conn - |> post("/api/v1/media", %{"file" => image}) - |> json_response(:ok) - - assert String.starts_with?(media["url"], proxy_url) - end - - test "returns media url when proxy is enabled but media url is whitelisted", %{ - conn: conn, - image: image - } do - media_url = "https://media.pleroma.social" - Pleroma.Config.put([Pleroma.Upload, :base_url], media_url) - - Pleroma.Config.put([:media_proxy, :enabled], true) - Pleroma.Config.put([:media_proxy, :base_url], "https://cache.pleroma.social") - Pleroma.Config.put([:media_proxy, :whitelist], ["media.pleroma.social"]) - - media = - conn - |> post("/api/v1/media", %{"file" => image}) - |> json_response(:ok) - - assert String.starts_with?(media["url"], media_url) - end end describe "locked accounts" do diff --git a/test/web/media_proxy/media_proxy_test.exs b/test/web/media_proxy/media_proxy_test.exs index edbbf9b66..0c94755df 100644 --- a/test/web/media_proxy/media_proxy_test.exs +++ b/test/web/media_proxy/media_proxy_test.exs @@ -171,21 +171,6 @@ test "preserve unicode characters" do encoded = url(url) assert decode_result(encoded) == url end - - test "does not change whitelisted urls" do - upload_config = Pleroma.Config.get([Pleroma.Upload]) - media_url = "https://media.pleroma.social" - Pleroma.Config.put([Pleroma.Upload, :base_url], media_url) - Pleroma.Config.put([:media_proxy, :whitelist], ["media.pleroma.social"]) - Pleroma.Config.put([:media_proxy, :base_url], "https://cache.pleroma.social") - - url = "#{media_url}/static/logo.png" - encoded = url(url) - - assert String.starts_with?(encoded, media_url) - - Pleroma.Config.put([Pleroma.Upload], upload_config) - end end describe "when disabled" do @@ -215,12 +200,43 @@ defp decode_result(encoded) do decoded end - test "mediaproxy whitelist" do - Pleroma.Config.put([:media_proxy, :enabled], true) - Pleroma.Config.put([:media_proxy, :whitelist], ["google.com", "feld.me"]) - url = "https://feld.me/foo.png" + describe "whitelist" do + setup do + Pleroma.Config.put([:media_proxy, :enabled], true) + :ok + end - unencoded = url(url) - assert unencoded == url + test "mediaproxy whitelist" do + Pleroma.Config.put([:media_proxy, :whitelist], ["google.com", "feld.me"]) + url = "https://feld.me/foo.png" + + unencoded = url(url) + assert unencoded == url + end + + test "does not change whitelisted urls" do + Pleroma.Config.put([:media_proxy, :whitelist], ["mycdn.akamai.com"]) + Pleroma.Config.put([:media_proxy, :base_url], "https://cache.pleroma.social") + + media_url = "https://mycdn.akamai.com" + + url = "#{media_url}/static/logo.png" + encoded = url(url) + + assert String.starts_with?(encoded, media_url) + end + + test "ensure Pleroma.Upload base_url is always whitelisted" do + upload_config = Pleroma.Config.get([Pleroma.Upload]) + media_url = "https://media.pleroma.social" + Pleroma.Config.put([Pleroma.Upload, :base_url], media_url) + + url = "#{media_url}/static/logo.png" + encoded = url(url) + + assert String.starts_with?(encoded, media_url) + + Pleroma.Config.put([Pleroma.Upload], upload_config) + end end end From 8815f07058f4bdf61355758cbe740288e9551435 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 2 Aug 2019 23:30:47 +0200 Subject: [PATCH 19/48] tasks/pleroma/user.ex: Fix documentation of --max-use and --expire-at Closes: https://git.pleroma.social/pleroma/pleroma/issues/1155 [ci skip] --- lib/mix/tasks/pleroma/user.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index c9b84b8f9..a3f8bc945 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -31,8 +31,8 @@ defmodule Mix.Tasks.Pleroma.User do mix pleroma.user invite [OPTION...] Options: - - `--expires_at DATE` - last day on which token is active (e.g. "2019-04-05") - - `--max_use NUMBER` - maximum numbers of token uses + - `--expires-at DATE` - last day on which token is active (e.g. "2019-04-05") + - `--max-use NUMBER` - maximum numbers of token uses ## List generated invites From 7efca4317b568c408a10b71799f9b8261ac5e8e6 Mon Sep 17 00:00:00 2001 From: Ashlynn Anderson Date: Wed, 31 Jul 2019 19:35:14 -0400 Subject: [PATCH 20/48] Basic working Dockerfile No fancy script or minit automatic migration, etc, but if you start the docker image and go in and manually do everything, it works. --- Dockerfile | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..667c01b39 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +FROM rinpatch/elixir:1.9.0-rc.0-alpine as build + +COPY . . + +ENV MIX_ENV prod + +RUN apk add git gcc g++ musl-dev make &&\ + echo "import Mix.Config" > config/prod.secret.exs &&\ + mix local.hex --force &&\ + mix local.rebar --force + +RUN mix deps.get --only prod &&\ + mkdir release &&\ + mix release --path release + +FROM alpine:latest + +RUN echo "http://nl.alpinelinux.org/alpine/latest-stable/community" >> /etc/apk/repositories &&\ + apk update &&\ + apk add ncurses postgresql-client + +RUN adduser --system --shell /bin/false --home /opt/pleroma pleroma &&\ + mkdir -p /var/lib/pleroma/uploads &&\ + chown -R pleroma /var/lib/pleroma &&\ + mkdir -p /var/lib/pleroma/static &&\ + chown -R pleroma /var/lib/pleroma &&\ + mkdir -p /etc/pleroma &&\ + chown -R pleroma /etc/pleroma + +USER pleroma + +COPY --from=build --chown=pleroma:0 /release/ /opt/pleroma/ From 4a418698db71016447f2f246f7c5579b3dc0b08c Mon Sep 17 00:00:00 2001 From: Ashlynn Anderson Date: Fri, 2 Aug 2019 22:28:48 -0400 Subject: [PATCH 21/48] Create docker.exs and docker-entrypoint + round out Dockerfile At this point, the implementation is completely working and has been tested running live and federating with other instances. --- Dockerfile | 23 ++++++++++----- config/docker.exs | 67 ++++++++++++++++++++++++++++++++++++++++++++ docker-entrypoint.sh | 14 +++++++++ 3 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 config/docker.exs create mode 100755 docker-entrypoint.sh diff --git a/Dockerfile b/Dockerfile index 667c01b39..2f438c952 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM rinpatch/elixir:1.9.0-rc.0-alpine as build COPY . . -ENV MIX_ENV prod +ENV MIX_ENV=prod RUN apk add git gcc g++ musl-dev make &&\ echo "import Mix.Config" > config/prod.secret.exs &&\ @@ -15,18 +15,27 @@ RUN mix deps.get --only prod &&\ FROM alpine:latest +ARG HOME=/opt/pleroma +ARG DATA=/var/lib/pleroma + RUN echo "http://nl.alpinelinux.org/alpine/latest-stable/community" >> /etc/apk/repositories &&\ apk update &&\ apk add ncurses postgresql-client -RUN adduser --system --shell /bin/false --home /opt/pleroma pleroma &&\ - mkdir -p /var/lib/pleroma/uploads &&\ - chown -R pleroma /var/lib/pleroma &&\ - mkdir -p /var/lib/pleroma/static &&\ - chown -R pleroma /var/lib/pleroma &&\ +RUN adduser --system --shell /bin/false --home ${HOME} pleroma &&\ + mkdir -p ${DATA}/uploads &&\ + mkdir -p ${DATA}/static &&\ + chown -R pleroma ${DATA} &&\ mkdir -p /etc/pleroma &&\ chown -R pleroma /etc/pleroma USER pleroma -COPY --from=build --chown=pleroma:0 /release/ /opt/pleroma/ +COPY --from=build --chown=pleroma:0 /release ${HOME} + +COPY ./config/docker.exs /etc/pleroma/config.exs +COPY ./docker-entrypoint.sh ${HOME} + +EXPOSE 4000 + +ENTRYPOINT ["/opt/pleroma/docker-entrypoint.sh"] diff --git a/config/docker.exs b/config/docker.exs new file mode 100644 index 000000000..c07f0b753 --- /dev/null +++ b/config/docker.exs @@ -0,0 +1,67 @@ +import Config + +config :pleroma, Pleroma.Web.Endpoint, + url: [host: System.get_env("DOMAIN", "localhost"), scheme: "https", port: 443], + http: [ip: {0, 0, 0, 0}, port: 4000] + +config :pleroma, :instance, + name: System.get_env("INSTANCE_NAME", "Pleroma"), + email: System.get_env("ADMIN_EMAIL"), + notify_email: System.get_env("NOTIFY_EMAIL"), + limit: 5000, + registrations_open: false, + dynamic_configuration: true + +config :pleroma, Pleroma.Repo, + adapter: Ecto.Adapters.Postgres, + username: System.get_env("DB_USER", "pleroma"), + password: System.fetch_env!("DB_PASS"), + database: System.get_env("DB_NAME", "pleroma"), + hostname: System.get_env("DB_HOST", "db"), + pool_size: 10 + +# Configure web push notifications +config :web_push_encryption, :vapid_details, + subject: "mailto:#{System.get_env("NOTIFY_EMAIL")}" + +config :pleroma, :database, rum_enabled: false +config :pleroma, :instance, static_dir: "/var/lib/pleroma/static" +config :pleroma, Pleroma.Uploaders.Local, uploads: "/var/lib/pleroma/uploads" + +# We can't store the secrets in this file, since this is baked into the docker image +if not File.exists?("/var/lib/pleroma/secret.exs") do + secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64) + signing_salt = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8) + {web_push_public_key, web_push_private_key} = :crypto.generate_key(:ecdh, :prime256v1) + + secret_file = EEx.eval_string( + """ + import Config + + config :pleroma, Pleroma.Web.Endpoint, + secret_key_base: "<%= secret %>", + signing_salt: "<%= signing_salt %>" + + config :web_push_encryption, :vapid_details, + public_key: "<%= web_push_public_key %>", + private_key: "<%= web_push_private_key %>" + """, + secret: secret, + signing_salt: signing_salt, + web_push_public_key: Base.url_encode64(web_push_public_key, padding: false), + web_push_private_key: Base.url_encode64(web_push_private_key, padding: false) + ) + + File.write("/var/lib/pleroma/secret.exs", secret_file) +end + +import_config("/var/lib/pleroma/secret.exs") + +# For additional user config +if File.exists?("/var/lib/pleroma/config.exs"), + do: import_config("/var/lib/pleroma/config.exs"), + else: File.write("/var/lib/pleroma/config.exs", """ + import Config + + # For additional configuration outside of environmental variables + """) diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100755 index 000000000..f56f8c50a --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,14 @@ +#!/bin/ash + +set -e + +echo "-- Waiting for database..." +while ! pg_isready -U ${DB_USER:-pleroma} -d postgres://${DB_HOST:-db}:5432/${DB_NAME:-pleroma} -t 1; do + sleep 1s +done + +echo "-- Running migrations..." +$HOME/bin/pleroma_ctl migrate + +echo "-- Starting!" +exec $HOME/bin/pleroma start From c86db959cb3a3f4a4f79833747d5fa8ecff0d0c7 Mon Sep 17 00:00:00 2001 From: Ashlynn Anderson Date: Fri, 2 Aug 2019 22:40:31 -0400 Subject: [PATCH 22/48] Optimize Dockerfile Just merging RUNs to decrease the number of layers --- Dockerfile | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 2f438c952..268ec61dc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,9 +7,8 @@ ENV MIX_ENV=prod RUN apk add git gcc g++ musl-dev make &&\ echo "import Mix.Config" > config/prod.secret.exs &&\ mix local.hex --force &&\ - mix local.rebar --force - -RUN mix deps.get --only prod &&\ + mix local.rebar --force &&\ + mix deps.get --only prod &&\ mkdir release &&\ mix release --path release @@ -20,9 +19,8 @@ ARG DATA=/var/lib/pleroma RUN echo "http://nl.alpinelinux.org/alpine/latest-stable/community" >> /etc/apk/repositories &&\ apk update &&\ - apk add ncurses postgresql-client - -RUN adduser --system --shell /bin/false --home ${HOME} pleroma &&\ + apk add ncurses postgresql-client &&\ + adduser --system --shell /bin/false --home ${HOME} pleroma &&\ mkdir -p ${DATA}/uploads &&\ mkdir -p ${DATA}/static &&\ chown -R pleroma ${DATA} &&\ From 04327721d73733c1052d284adca12b949ce61045 Mon Sep 17 00:00:00 2001 From: Ashlynn Anderson Date: Fri, 2 Aug 2019 23:33:47 -0400 Subject: [PATCH 23/48] Add .dockerignore --- .dockerignore | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .dockerignore diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..c5ef89b86 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.* +*.md +AGPL-3 +CC-BY-SA-4.0 +COPYING +*file +elixir_buildpack.config +docs/ +test/ + +# Required to get version +!.git From a187dbb326f8fa3dfe19a113f4db5ed0a95435cb Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Sat, 3 Aug 2019 17:24:57 +0000 Subject: [PATCH 24/48] Add preferredUsername to service actors so Mastodon can resolve them --- lib/pleroma/web/activity_pub/views/user_view.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 639519e0a..4a83ac980 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -45,6 +45,7 @@ def render("service.json", %{user: user}) do "following" => "#{user.ap_id}/following", "followers" => "#{user.ap_id}/followers", "inbox" => "#{user.ap_id}/inbox", + "preferredUsername" => user.nickname, "name" => "Pleroma", "summary" => "An internal service actor for this Pleroma instance. No user-serviceable parts inside.", From 4007717534f9cc880b808b91ba6be5801afb71a0 Mon Sep 17 00:00:00 2001 From: Ashlynn Anderson Date: Sat, 3 Aug 2019 13:42:57 -0400 Subject: [PATCH 25/48] Run mix format --- config/docker.exs | 53 ++++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/config/docker.exs b/config/docker.exs index c07f0b753..63ab4cdee 100644 --- a/config/docker.exs +++ b/config/docker.exs @@ -1,8 +1,8 @@ import Config config :pleroma, Pleroma.Web.Endpoint, - url: [host: System.get_env("DOMAIN", "localhost"), scheme: "https", port: 443], - http: [ip: {0, 0, 0, 0}, port: 4000] + url: [host: System.get_env("DOMAIN", "localhost"), scheme: "https", port: 443], + http: [ip: {0, 0, 0, 0}, port: 4000] config :pleroma, :instance, name: System.get_env("INSTANCE_NAME", "Pleroma"), @@ -21,8 +21,7 @@ pool_size: 10 # Configure web push notifications -config :web_push_encryption, :vapid_details, - subject: "mailto:#{System.get_env("NOTIFY_EMAIL")}" +config :web_push_encryption, :vapid_details, subject: "mailto:#{System.get_env("NOTIFY_EMAIL")}" config :pleroma, :database, rum_enabled: false config :pleroma, :instance, static_dir: "/var/lib/pleroma/static" @@ -34,23 +33,24 @@ signing_salt = :crypto.strong_rand_bytes(8) |> Base.encode64() |> binary_part(0, 8) {web_push_public_key, web_push_private_key} = :crypto.generate_key(:ecdh, :prime256v1) - secret_file = EEx.eval_string( - """ - import Config - - config :pleroma, Pleroma.Web.Endpoint, - secret_key_base: "<%= secret %>", - signing_salt: "<%= signing_salt %>" - - config :web_push_encryption, :vapid_details, - public_key: "<%= web_push_public_key %>", - private_key: "<%= web_push_private_key %>" - """, - secret: secret, - signing_salt: signing_salt, - web_push_public_key: Base.url_encode64(web_push_public_key, padding: false), - web_push_private_key: Base.url_encode64(web_push_private_key, padding: false) - ) + secret_file = + EEx.eval_string( + """ + import Config + + config :pleroma, Pleroma.Web.Endpoint, + secret_key_base: "<%= secret %>", + signing_salt: "<%= signing_salt %>" + + config :web_push_encryption, :vapid_details, + public_key: "<%= web_push_public_key %>", + private_key: "<%= web_push_private_key %>" + """, + secret: secret, + signing_salt: signing_salt, + web_push_public_key: Base.url_encode64(web_push_public_key, padding: false), + web_push_private_key: Base.url_encode64(web_push_private_key, padding: false) + ) File.write("/var/lib/pleroma/secret.exs", secret_file) end @@ -60,8 +60,9 @@ # For additional user config if File.exists?("/var/lib/pleroma/config.exs"), do: import_config("/var/lib/pleroma/config.exs"), - else: File.write("/var/lib/pleroma/config.exs", """ - import Config - - # For additional configuration outside of environmental variables - """) + else: + File.write("/var/lib/pleroma/config.exs", """ + import Config + + # For additional configuration outside of environmental variables + """) From 8b2fa31fed1a970c75e077d419dc78be7fc73a93 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Sat, 3 Aug 2019 18:12:38 +0000 Subject: [PATCH 26/48] Handle MRF rejections of incoming AP activities --- lib/pleroma/web/activity_pub/activity_pub.ex | 3 +++ test/web/federator_test.exs | 16 ++++++++++++++++ 2 files changed, 19 insertions(+) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 07a65127b..2877c029e 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -267,6 +267,9 @@ def create(%{to: to, actor: actor, context: context, object: object} = params, f else {:fake, true, activity} -> {:ok, activity} + + {:error, message} -> + {:error, message} end end diff --git a/test/web/federator_test.exs b/test/web/federator_test.exs index 6e143eee4..73cfaa8f1 100644 --- a/test/web/federator_test.exs +++ b/test/web/federator_test.exs @@ -229,5 +229,21 @@ test "rejects incoming AP docs with incorrect origin" do :error = Federator.incoming_ap_doc(params) end + + test "it does not crash if MRF rejects the post" do + policies = Pleroma.Config.get([:instance, :rewrite_policy]) + mrf_keyword_policy = Pleroma.Config.get(:mrf_keyword) + Pleroma.Config.put([:mrf_keyword, :reject], ["lain"]) + Pleroma.Config.put([:instance, :rewrite_policy], Pleroma.Web.ActivityPub.MRF.KeywordPolicy) + + params = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + + assert Federator.incoming_ap_doc(params) == :error + + Pleroma.Config.put([:instance, :rewrite_policy], policies) + Pleroma.Config.put(:mrf_keyword, mrf_keyword_policy) + end end end From 040347b24820e2773c45a38d4cb6a184d6b14366 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Sat, 3 Aug 2019 18:13:20 +0000 Subject: [PATCH 27/48] Remove spaces from the domain search --- lib/pleroma/user/search.ex | 2 +- test/web/mastodon_api/search_controller_test.exs | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/user/search.ex b/lib/pleroma/user/search.ex index 46620b89a..6fb2c2352 100644 --- a/lib/pleroma/user/search.ex +++ b/lib/pleroma/user/search.ex @@ -44,7 +44,7 @@ defp format_query(query_string) do query_string = String.trim_leading(query_string, "@") with [name, domain] <- String.split(query_string, "@"), - formatted_domain <- String.replace(domain, ~r/[!-\-|@|[-`|{-~|\/|:]+/, "") do + formatted_domain <- String.replace(domain, ~r/[!-\-|@|[-`|{-~|\/|:|\s]+/, "") do name <> "@" <> to_string(:idna.encode(formatted_domain)) else _ -> query_string diff --git a/test/web/mastodon_api/search_controller_test.exs b/test/web/mastodon_api/search_controller_test.exs index 043b96c14..49c79ff0a 100644 --- a/test/web/mastodon_api/search_controller_test.exs +++ b/test/web/mastodon_api/search_controller_test.exs @@ -95,6 +95,18 @@ test "account search", %{conn: conn} do assert user_three.nickname in result_ids end + + test "returns account if query contains a space", %{conn: conn} do + user = insert(:user, %{nickname: "shp@shitposter.club"}) + + results = + conn + |> assign(:user, user) + |> get("/api/v1/accounts/search", %{"q" => "shp@shitposter.club xxx "}) + |> json_response(200) + + assert length(results) == 1 + end end describe ".search" do From de0f3b73dd7c76b6b19b38804f98f6e7ccba7222 Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Sat, 3 Aug 2019 18:16:09 +0000 Subject: [PATCH 28/48] Admin fixes --- docs/api/admin_api.md | 4 +++ .../web/admin_api/admin_api_controller.ex | 6 ++-- lib/pleroma/web/admin_api/config.ex | 15 +++++++-- .../admin_api/admin_api_controller_test.exs | 32 +++++++++++++++++++ 4 files changed, 52 insertions(+), 5 deletions(-) diff --git a/docs/api/admin_api.md b/docs/api/admin_api.md index 22873dde9..7ccb90836 100644 --- a/docs/api/admin_api.md +++ b/docs/api/admin_api.md @@ -627,6 +627,9 @@ Tuples can be passed as `{"tuple": ["first_val", Pleroma.Module, []]}`. Keywords can be passed as lists with 2 child tuples, e.g. `[{"tuple": ["first_val", Pleroma.Module]}, {"tuple": ["second_val", true]}]`. +If value contains list of settings `[subkey: val1, subkey2: val2, subkey3: val3]`, it's possible to remove only subkeys instead of all settings passing `subkeys` parameter. E.g.: +{"group": "pleroma", "key": "some_key", "delete": "true", "subkeys": [":subkey", ":subkey3"]}. + Compile time settings (need instance reboot): - all settings by this keys: - `:hackney_pools` @@ -645,6 +648,7 @@ Compile time settings (need instance reboot): - `key` (string or string with leading `:` for atoms) - `value` (string, [], {} or {"tuple": []}) - `delete` = true (optional, if parameter must be deleted) + - `subkeys` [(string with leading `:` for atoms)] (optional, works only if `delete=true` parameter is passed, otherwise will be ignored) ] - Request (example): diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index fcda57b3e..2d3d0adc4 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -402,9 +402,9 @@ def config_update(conn, %{"configs" => configs}) do if Pleroma.Config.get([:instance, :dynamic_configuration]) do updated = Enum.map(configs, fn - %{"group" => group, "key" => key, "delete" => "true"} -> - {:ok, _} = Config.delete(%{group: group, key: key}) - nil + %{"group" => group, "key" => key, "delete" => "true"} = params -> + {:ok, config} = Config.delete(%{group: group, key: key, subkeys: params["subkeys"]}) + config %{"group" => group, "key" => key, "value" => value} -> {:ok, config} = Config.update_or_create(%{group: group, key: key, value: value}) diff --git a/lib/pleroma/web/admin_api/config.ex b/lib/pleroma/web/admin_api/config.ex index dde05ea7b..a10cc779b 100644 --- a/lib/pleroma/web/admin_api/config.ex +++ b/lib/pleroma/web/admin_api/config.ex @@ -55,8 +55,19 @@ def update_or_create(params) do @spec delete(map()) :: {:ok, Config.t()} | {:error, Changeset.t()} def delete(params) do - with %Config{} = config <- Config.get_by_params(params) do - Repo.delete(config) + with %Config{} = config <- Config.get_by_params(Map.delete(params, :subkeys)) do + if params[:subkeys] do + updated_value = + Keyword.drop( + :erlang.binary_to_term(config.value), + Enum.map(params[:subkeys], &do_transform_string(&1)) + ) + + Config.update(config, %{value: updated_value}) + else + Repo.delete(config) + {:ok, nil} + end else nil -> err = diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index f61499a22..bcbc18639 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -1914,6 +1914,38 @@ test "queues key as atom", %{conn: conn} do ] } end + + test "delete part of settings by atom subkeys", %{conn: conn} do + config = + insert(:config, + key: "keyaa1", + value: :erlang.term_to_binary(subkey1: "val1", subkey2: "val2", subkey3: "val3") + ) + + conn = + post(conn, "/api/pleroma/admin/config", %{ + configs: [ + %{ + group: config.group, + key: config.key, + subkeys: [":subkey1", ":subkey3"], + delete: "true" + } + ] + }) + + assert( + json_response(conn, 200) == %{ + "configs" => [ + %{ + "group" => "pleroma", + "key" => "keyaa1", + "value" => [%{"tuple" => [":subkey2", "val2"]}] + } + ] + } + ) + end end describe "config mix tasks run" do From 16cfb89240f9f56752ba8d91d84ce81a70f8d6cf Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Sat, 3 Aug 2019 18:28:08 +0000 Subject: [PATCH 29/48] Only add `preferredUsername` to service actor json when the underlying user actually has a username --- lib/pleroma/web/activity_pub/views/user_view.ex | 2 +- 1 file changed, 1 insertion(+), 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 4a83ac980..8fe38927f 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -45,7 +45,6 @@ def render("service.json", %{user: user}) do "following" => "#{user.ap_id}/following", "followers" => "#{user.ap_id}/followers", "inbox" => "#{user.ap_id}/inbox", - "preferredUsername" => user.nickname, "name" => "Pleroma", "summary" => "An internal service actor for this Pleroma instance. No user-serviceable parts inside.", @@ -58,6 +57,7 @@ def render("service.json", %{user: user}) do }, "endpoints" => endpoints } + |> Map.merge(if user.nickname == nil do %{} else %{ "preferredUsername" => user.nickname}) |> Map.merge(Utils.make_json_ld_header()) end From 1fce56c7dffb84917b6943cc5919ed76e06932a5 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Sat, 3 Aug 2019 18:37:20 +0000 Subject: [PATCH 30/48] Refactor --- lib/pleroma/web/activity_pub/views/user_view.ex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/pleroma/web/activity_pub/views/user_view.ex b/lib/pleroma/web/activity_pub/views/user_view.ex index 8fe38927f..06c9e1c71 100644 --- a/lib/pleroma/web/activity_pub/views/user_view.ex +++ b/lib/pleroma/web/activity_pub/views/user_view.ex @@ -57,7 +57,6 @@ def render("service.json", %{user: user}) do }, "endpoints" => endpoints } - |> Map.merge(if user.nickname == nil do %{} else %{ "preferredUsername" => user.nickname}) |> Map.merge(Utils.make_json_ld_header()) end @@ -66,7 +65,7 @@ def render("user.json", %{user: %User{nickname: nil} = user}), do: render("service.json", %{user: user}) def render("user.json", %{user: %User{nickname: "internal." <> _} = user}), - do: render("service.json", %{user: user}) + do: render("service.json", %{user: user}) |> Map.put("preferredUsername", user.nickname) def render("user.json", %{user: user}) do {:ok, user} = User.ensure_keys_present(user) From a035ab8c1d1589a97816d17dac8d60fb4b2275b2 Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Sat, 3 Aug 2019 22:56:20 +0200 Subject: [PATCH 31/48] templates/layout/app.html.eex: Style anchors [ci skip] --- lib/pleroma/web/templates/layout/app.html.eex | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/pleroma/web/templates/layout/app.html.eex b/lib/pleroma/web/templates/layout/app.html.eex index b3cf9ed11..5836ec1e0 100644 --- a/lib/pleroma/web/templates/layout/app.html.eex +++ b/lib/pleroma/web/templates/layout/app.html.eex @@ -36,6 +36,11 @@ margin-bottom: 20px; } + a { + color: color: #d8a070; + text-decoration: none; + } + form { width: 100%; } From cef3af5536c16ff357fe2e0ed8c560aff16c62de Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Sat, 3 Aug 2019 23:17:17 +0000 Subject: [PATCH 32/48] tasks: relay: add list task --- CHANGELOG.md | 1 + lib/mix/tasks/pleroma/relay.ex | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fa9ffd9b..2b0a6189a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,6 +69,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - ActivityPub: Optional signing of ActivityPub object fetches. - Admin API: Endpoint for fetching latest user's statuses - Pleroma API: Add `/api/v1/pleroma/accounts/confirmation_resend?email=` for resending account confirmation. +- Relays: Added a task to list relay subscriptions. ### Changed - Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text diff --git a/lib/mix/tasks/pleroma/relay.ex b/lib/mix/tasks/pleroma/relay.ex index 83ed0ed02..c7324fff6 100644 --- a/lib/mix/tasks/pleroma/relay.ex +++ b/lib/mix/tasks/pleroma/relay.ex @@ -5,6 +5,7 @@ defmodule Mix.Tasks.Pleroma.Relay do use Mix.Task import Mix.Pleroma + alias Pleroma.User alias Pleroma.Web.ActivityPub.Relay @shortdoc "Manages remote relays" @@ -22,6 +23,10 @@ defmodule Mix.Tasks.Pleroma.Relay do ``mix pleroma.relay unfollow `` Example: ``mix pleroma.relay unfollow https://example.org/relay`` + + ## List relay subscriptions + + ``mix pleroma.relay list`` """ def run(["follow", target]) do start_pleroma() @@ -44,4 +49,19 @@ def run(["unfollow", target]) do {:error, e} -> shell_error("Error while following #{target}: #{inspect(e)}") end end + + def run(["list"]) do + start_pleroma() + + with %User{} = user <- Relay.get_actor() do + user.following + |> Enum.each(fn entry -> + URI.parse(entry) + |> Map.get(:host) + |> shell_info() + end) + else + e -> shell_error("Error while fetching relay subscription list: #{inspect(e)}") + end + end end From c10a3e035b2761b1d8419d39b8392d499abe9aae Mon Sep 17 00:00:00 2001 From: Pierce McGoran Date: Sun, 4 Aug 2019 03:01:21 +0000 Subject: [PATCH 33/48] Update link in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 41d454a03..5aad34ccc 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ If you want to run your own server, feel free to contact us at @lain@pleroma.soy Currently Pleroma is not packaged by any OS/Distros, but feel free to reach out to us at [#pleroma-dev on freenode](https://webchat.freenode.net/?channels=%23pleroma-dev) or via matrix at for assistance. If you want to change default options in your Pleroma package, please **discuss it with us first**. ### Docker -While we don’t provide docker files, other people have written very good ones. Take a look at or . +While we don’t provide docker files, other people have written very good ones. Take a look at or . ### Dependencies From 9b9453ceaf492ef3af18c12ce67e144a718fd65a Mon Sep 17 00:00:00 2001 From: Pierce McGoran Date: Sun, 4 Aug 2019 03:12:38 +0000 Subject: [PATCH 34/48] Fix some typos and rework a few lines in howto_mediaproxy.md --- docs/config/howto_mediaproxy.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/config/howto_mediaproxy.md b/docs/config/howto_mediaproxy.md index ed70c3ed4..16c40c5db 100644 --- a/docs/config/howto_mediaproxy.md +++ b/docs/config/howto_mediaproxy.md @@ -1,8 +1,8 @@ # How to activate mediaproxy ## Explanation -Without the `mediaproxy` function, Pleroma don't store any remote content like pictures, video etc. locally. So every time you open Pleroma, the content is loaded from the source server, from where the post is coming. This can result in slowly loading content or/and increased bandwidth usage on the source server. -With the `mediaproxy` function you can use the cache ability of nginx, to cache these content, so user can access it faster, cause it's loaded from your server. +Without the `mediaproxy` function, Pleroma doesn't store any remote content like pictures, video etc. locally. So every time you open Pleroma, the content is loaded from the source server, from where the post is coming. This can result in slowly loading content or/and increased bandwidth usage on the source server. +With the `mediaproxy` function you can use nginx to cache this content, so users can access it faster, because it's loaded from your server. ## Activate it From 39c7bbe18fcbff608bbc0b72ec6134872a1c946a Mon Sep 17 00:00:00 2001 From: Hakaba Hitoyo Date: Sun, 4 Aug 2019 04:32:45 +0000 Subject: [PATCH 35/48] Remove longfox emoji set --- CHANGELOG.md | 3 +++ config/emoji.txt | 28 ---------------------------- priv/static/emoji/f_00b.png | Bin 371 -> 0 bytes priv/static/emoji/f_00b11b.png | Bin 661 -> 0 bytes priv/static/emoji/f_00b33b.png | Bin 662 -> 0 bytes priv/static/emoji/f_00h.png | Bin 7522 -> 0 bytes priv/static/emoji/f_00t.png | Bin 541 -> 0 bytes priv/static/emoji/f_01b.png | Bin 4510 -> 0 bytes priv/static/emoji/f_03b.png | Bin 2872 -> 0 bytes priv/static/emoji/f_10b.png | Bin 2849 -> 0 bytes priv/static/emoji/f_11b.png | Bin 447 -> 0 bytes priv/static/emoji/f_11b00b.png | Bin 615 -> 0 bytes priv/static/emoji/f_11b22b.png | Bin 618 -> 0 bytes priv/static/emoji/f_11h.png | Bin 7314 -> 0 bytes priv/static/emoji/f_11t.png | Bin 559 -> 0 bytes priv/static/emoji/f_12b.png | Bin 4352 -> 0 bytes priv/static/emoji/f_21b.png | Bin 2900 -> 0 bytes priv/static/emoji/f_22b.png | Bin 386 -> 0 bytes priv/static/emoji/f_22b11b.png | Bin 666 -> 0 bytes priv/static/emoji/f_22b33b.png | Bin 663 -> 0 bytes priv/static/emoji/f_22h.png | Bin 7448 -> 0 bytes priv/static/emoji/f_22t.png | Bin 549 -> 0 bytes priv/static/emoji/f_23b.png | Bin 4334 -> 0 bytes priv/static/emoji/f_30b.png | Bin 4379 -> 0 bytes priv/static/emoji/f_32b.png | Bin 2921 -> 0 bytes priv/static/emoji/f_33b.png | Bin 459 -> 0 bytes priv/static/emoji/f_33b00b.png | Bin 611 -> 0 bytes priv/static/emoji/f_33b22b.png | Bin 623 -> 0 bytes priv/static/emoji/f_33h.png | Bin 7246 -> 0 bytes priv/static/emoji/f_33t.png | Bin 563 -> 0 bytes 30 files changed, 3 insertions(+), 28 deletions(-) delete mode 100644 priv/static/emoji/f_00b.png delete mode 100644 priv/static/emoji/f_00b11b.png delete mode 100644 priv/static/emoji/f_00b33b.png delete mode 100644 priv/static/emoji/f_00h.png delete mode 100644 priv/static/emoji/f_00t.png delete mode 100644 priv/static/emoji/f_01b.png delete mode 100644 priv/static/emoji/f_03b.png delete mode 100644 priv/static/emoji/f_10b.png delete mode 100644 priv/static/emoji/f_11b.png delete mode 100644 priv/static/emoji/f_11b00b.png delete mode 100644 priv/static/emoji/f_11b22b.png delete mode 100644 priv/static/emoji/f_11h.png delete mode 100644 priv/static/emoji/f_11t.png delete mode 100644 priv/static/emoji/f_12b.png delete mode 100644 priv/static/emoji/f_21b.png delete mode 100644 priv/static/emoji/f_22b.png delete mode 100644 priv/static/emoji/f_22b11b.png delete mode 100644 priv/static/emoji/f_22b33b.png delete mode 100644 priv/static/emoji/f_22h.png delete mode 100644 priv/static/emoji/f_22t.png delete mode 100644 priv/static/emoji/f_23b.png delete mode 100644 priv/static/emoji/f_30b.png delete mode 100644 priv/static/emoji/f_32b.png delete mode 100644 priv/static/emoji/f_33b.png delete mode 100644 priv/static/emoji/f_33b00b.png delete mode 100644 priv/static/emoji/f_33b22b.png delete mode 100644 priv/static/emoji/f_33h.png delete mode 100644 priv/static/emoji/f_33t.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fa9ffd9b..fc4d08aaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,9 @@ 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. +### Removed +- Emoji: Remove longfox emojis. + ## [1.0.1] - 2019-07-14 ### Security - OStatus: fix an object spoofing vulnerability. diff --git a/config/emoji.txt b/config/emoji.txt index 79246f239..200768ad1 100644 --- a/config/emoji.txt +++ b/config/emoji.txt @@ -1,30 +1,2 @@ firefox, /emoji/Firefox.gif, Gif,Fun blank, /emoji/blank.png, Fun -f_00b, /emoji/f_00b.png -f_00b11b, /emoji/f_00b11b.png -f_00b33b, /emoji/f_00b33b.png -f_00h, /emoji/f_00h.png -f_00t, /emoji/f_00t.png -f_01b, /emoji/f_01b.png -f_03b, /emoji/f_03b.png -f_10b, /emoji/f_10b.png -f_11b, /emoji/f_11b.png -f_11b00b, /emoji/f_11b00b.png -f_11b22b, /emoji/f_11b22b.png -f_11h, /emoji/f_11h.png -f_11t, /emoji/f_11t.png -f_12b, /emoji/f_12b.png -f_21b, /emoji/f_21b.png -f_22b, /emoji/f_22b.png -f_22b11b, /emoji/f_22b11b.png -f_22b33b, /emoji/f_22b33b.png -f_22h, /emoji/f_22h.png -f_22t, /emoji/f_22t.png -f_23b, /emoji/f_23b.png -f_30b, /emoji/f_30b.png -f_32b, /emoji/f_32b.png -f_33b, /emoji/f_33b.png -f_33b00b, /emoji/f_33b00b.png -f_33b22b, /emoji/f_33b22b.png -f_33h, /emoji/f_33h.png -f_33t, /emoji/f_33t.png diff --git a/priv/static/emoji/f_00b.png b/priv/static/emoji/f_00b.png deleted file mode 100644 index 3d00b89b02acbcf8cd3b4ff388e2f09f06c0aee1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 371 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrV6^gdaSW+oe0!bI&BamX_{aZe zX6tr}a(3}uiBn;ST_nxo^!x?ST;&xR;T&I=Y5KF@5L*AU=Eu*^3^kJaKZ=3oU;`6w zt-JN@_OS)(J9FoTt==ppyU6XFv-Q@zto0Qdv!5@2A)YF8{Drt>8YiOy14{#g00WZ) z0|x^mE>6Ry;sTMsi)`-2?%!(d_nO`LcJru6{1-oD!Mef_(Y8l8#ae0+Qj4GnKrsQ0BxFf%h37Z<-?u0A16 z0%&7iUS4i)Zd_bkLPEluhbQMeKD+2luf*|Y=HtyQr@90$%rZJNQStnAoeQ&!pRKZZvPkXO61C?m4Odqy zFDsK-P$bR6!;_w#o}ZszQBjeeo_cp*b52f9R%XV>_irB@={wym`gWb=lx$fcA))H( z>TJ8SnLw8@mIV0)GdMiEkp^U2d%8G=R4~3d7hKfBD8TSA^7h8u+uKV2Z+>FA`PXu_ zd+(S_YLj$D`dpGryVWPH+_lvB=A0d;+m$yu-R)Xhv@|bw<~P2=>o+AXT)oS2;W9VS zcr0Ko?~B*+5;?gCE_^ugqMzy5F?F@2t5&|04CXuV=FXp{UB3FKE`93KR8MeK7fkkZ za8X|!812_^Z|$k5gVTfq0*=PK`$wDhZOlBy8L)kA?#-Kbji0itxczRQ zjLfYfrZ4Y2`n0#rY3R~oMbTJPly#cZB6#lUqf2$wA9$%>7X~W&+3(A9;>FoTn(7Xv z)#}&gFch*wHJ!L`&SEn~no&*e=-n^DPb$~ueVl&%@vgml^-XkS4%;yBPFPsT{N%@E OkaAB~KbLh*2~7Y>h#0s4 diff --git a/priv/static/emoji/f_00b33b.png b/priv/static/emoji/f_00b33b.png deleted file mode 100644 index 8f4929297e401d8fc0b150c0080b989d974e00db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 662 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 diff --git a/priv/static/emoji/f_00t.png b/priv/static/emoji/f_00t.png deleted file mode 100644 index 31d98b4333b253da7c08b7925928d3750b816125..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 541 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 diff --git a/priv/static/emoji/f_03b.png b/priv/static/emoji/f_03b.png deleted file mode 100644 index 9e4ff1bf77074361c97d1ef8fcdc205575d03916..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2872 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 Wnd|l2N8F|W00003RzX=`6xmTY)Ni*_) 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 diff --git a/priv/static/emoji/f_11b00b.png b/priv/static/emoji/f_11b00b.png deleted file mode 100644 index c4c30e11f5c16d81c05217d2a109d27d89a73e4d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 615 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD~={fsrG?C&U%V&CSjA_4WDx|No~C zZ$G?y^X>2dC+C)&>6N%J%jm)^<7cZZR#z+Y@bDBD7stiLd3$?)eE;VC+gD%z{(pFK z_VH%sGZPihOjLTZNNrh}4A8Xn^z?*;1aEJzho|NqZ)VBN%#4bP3J3@Y3=GUjPfbrx z&C1O9@b2};_iyhX>b|?b<=+0*yZf3S9O*mR!FQ~Y;aDT%>2A^U({b4j%g!Y6&P4IPREY^`68&isQ?g~Hq@=jHxp{ec`S|#lnVA_G8HI#| z+S=M`YisN4>l+#x^78Tu3JP*^a;mGVpDj^aP$d2E*?YcQo;DgTe~DWM4f2cZE% diff --git a/priv/static/emoji/f_11b22b.png b/priv/static/emoji/f_11b22b.png deleted file mode 100644 index 47425e06e2713254552d9081069ed7553f6181e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 618 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD~={fsrG?C&U%V&CJa7_V)V!|No~C zZ$G?y{q67n$7dIv>JmIZUFZCC-RCO}7ZgeJ@$pquRK&%_eSH7s{o7Yx|Neh?a?bH) z<}(u&&rDQ$wnPnRPJVuVLPEmBQ*)0uvoJC;78De;wYAmO*4Ee8H#9U97Z>N{<>ln$ zR99C^Nl9^YbMx}@GBYy^2?=#3iFYQ7_oYfqNR#MKlbDh%`);lA+m#ycR%*Oiq5gWg z`kUqIZ`WB~m}PvjgYQ@)!?8xj)7_%?4|U(&-*Rt%>)n0L503PGc=zVx`?neCsp;vd zS(zD8QBeT_0fB*mPZp^yE0gi|_I!A9c6xexZf>rxug{ZnOV0F4T$p9_Y?a08YGocC z9wpI7o^R1%uzC3;cb(>KnV&Jc({El|VyP>`{b5yL zO8V3$P^`g0LrJ3cx_w@nA_>(OH8!n#mAreMhzwj7#8|Q`IOUtq#--7&Y0GD>-SNwP z^@hjd@)60E_vc^p2#?$U?T!G7dIaZB{ef?OJN4|RypB6OiEH=9U17<#ORk-l%6wY3 aHu3)RS+gW`A6wr7srPjCb6Mw<&;$S)^#2(E diff --git a/priv/static/emoji/f_11h.png b/priv/static/emoji/f_11h.png deleted file mode 100644 index 28342363a20b6dbdeb34a2fc4d30a9def0c3201f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7314 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 diff --git a/priv/static/emoji/f_11t.png b/priv/static/emoji/f_11t.png deleted file mode 100644 index dca67dc70ac3d8259ab7a62acea3d9298751abb6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 559 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD`S&H3s;ExB}^fgamJIuWx_AKHX8uIb+Ra8`*4}EtCXeDDwkY6x^!?PP{Kz59$i(^Oy zgGn3nCI%jj7yPUO32_(CH#Bj(JZw-%5Ln=%AT^s8Xk(I5gQeJo9tQoF`i2VJ0S#UM zf|$>2Y)E6NaID|S_^kUKKf4LjgN8~@4guS^NTAvRrvotzix^&hXT1qM2Ur>mdKI;Vst0M7f_6#xJL diff --git a/priv/static/emoji/f_12b.png b/priv/static/emoji/f_12b.png deleted file mode 100644 index 9925adb7cf6dc3af9218a7e2f258840c4df7b423..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4352 zcmV+b5&!OqP)~Q@=i~`sxK@~{qHv?_o$w13D((&IbY54WoRDAt-GTy&05ii{mj|Z=e z!Hwre;(S{ea#Dg385W=dQ$$N?5dBRr)8&*%rhK8ccX1T%=<3AIZcgm#?!+G2+tY>p zy<9ldhiGpP@BcTnw_|ZLD<=PzhrXX>qv>nu`1GM9Ja$bit~fmcImyAWSY$9tXb!ce z7wK#|70c9|e+vo;^_lx$0p`)dCRQwMVZ-V+c5Lb7z;1?x0g++Z*4ZJ$G2;7N8H~5@ zPGr}JLS|yH>J}TQJH1M$lMj>$K%>q7zd(dTc8NXg7=;YRE_RKDO|9trc{aXzJOy`M z9F44`ia;!;zvyNPCf_X+fZB;FF)qgaoF_-cHD^YuF|m@qCL8HgCIHpLz;Pkh59|zb$1#$N$#?Hbgf%@xg`tm@ zUBLvP_ArQDMsYv4+;OygGXpoB7o~zQhhCsS(ydGYDx1YHENO1Vf!^XQ!iAyV=HQtd z;t(DhpthACP$cPCCIFR#AkHFqXoVtnh3S9h;~kENgy=vKgq`%MxvS7P0F}qXED~#l zlKyTkBCYuF{v;KI?es2L$drx&P-fH2zw@!Stv3h@npp83&lMuPqhU2YMW%EOKy5)N zWe{fmU4Z9ridT!p$#g53(lr2;M+1YvGX;n22x1F)%lQ?%hJIuxQ@RGA`Wiu;Bkb$x zl1I+xSi?jJj?rgiO6LG5vp5Ro@s#l(w~yPpIPooyngar4H<(M8ktv-6pop=sw1pK% z2YWD<$INHu$!;LLL8f#MK;`v;c)PH*vlAcOm!v{4gwn|bfS|h>f8}FEOB>1udNAah z95}N=MFd*SegH}VnUS!s zmrLF5XeLZ$)bHtBm zkAyMZkQmh`XhagrML_Qe)WZJoHEl@*2*^Hzy!dD(H+C|P$NYg~d&c6(_94jL{53My zyoW!RK8v)ucO!Y~6>4pLWP2fseLpcS7>2mvt&uRcJNA$7AF!5ytN>LHKX{;{E`oPi zAi+}$g%dixC)EGlRe<6HoJs}kmJ*Z~WS}H-8}j$h!I5o)aA?IVNSSd1w+ej`JEVyw zibjYZ)malnNWeTO(J)Ije^C1BiIeXLAF)7z-CCw_;_qm)z+tn(;c##fc;WKak|;Eh zr~+{~9VpLBK|%5oE{b1}vG_?G_#;vk#E%Sd!&+)?Bc$GE2sF}0uZllB(?y~PNBnyotvT?MMt1O9*SR|F_V3TREgb*Awf{J6#0t z1+4)7o~2wAR=C_%x*&w*D9zb}quU1|b>_|7BD4@*aE>W}f6G!K2A8KA733mZ zPUI)fKJ!n(ff1}W#eR10RJs5CBi*;p{ABa>8p^w;6V}sxdw_4h<;r$q`_6{ zEzlZEmYV|jA7~>X*d0O$o?6u+Bu8I^y+1HfiazjrxL=EaM%h<>-$#!JelZ2`e+j{^ zx`v8c9N#w{iDP?Eu&C7$!u&Qcq|9LW%iaE;%)k_YPFr~~oGz~j7VYJEIK1K&N*$@7 zq*;Ryzk5nf{yjvLjGwu=oi2htrU3rmV6nUe?n($D6sE7l0d4{I{lNUPsz_EKYg!m} z9qU6V*`@%_R-kCNX{n>!&V(-V9FbH11*w;4BxC zMBhRskh%J85f>zk>g+czAz?hl1EFFjaiF^DF4h!4UAP3VC@WJ(6P1$bfr*PC!f#vw z?J3wgw4ZJTJ~jnV7j*-NQ;4AW&?f3Xptd!?u>|o{A6RoPExpu5aJeafI(TigTg!zA zEQRS54^SzYMBo?o0ct6u)G(r#m`E-(1yE;kK`Fcd|F{qbCY(p9qF-bP^m0hYIChTc zk0$Cu>|v$=>M$;#;E?JCDKl>&5omvi5X0jE>K~wG*qSRCU<#lPMc}T6VEX)rga{58 zoFXQ2QYR6)g7f={Dq)N%fV$2QDj~X%QfJ>qBA}SSXjee@m`;B+e83byUAu#7>PK|r z;KU0kF!YNQk#-W$JtbOrZ3;kAOklSaV}DdX#13jmA~3W-L|sI?M)lXN!0V;}>RbdR zB0I1Y9irwEYEL)R9VA2z5M$`C+#$-u3TH!R6dbm4g%iYp7J{Wz)Co$mwj*Xx1H_N& zWLR?v)dr5v2%}pF`Vr2bm{9fiKDsBZn ze^gWms|`DR@z$Gf;QjaC#pj=Wg297+!Gu44!;-}dv2V{V96y$Wnkp~OL^TwrRja<$ zKPy19j${e4)_sh<--|+#VQFFJv{2DN(xJ7ucQBe6ghkrc%pKY;-J_f?PPA^_8t0vN zE*^d4Vf_5lk662UB?|I$aXLIMJ5}K7)!Tu~Qz<%-Q)k>vZDte?7-j`%^!#ge%&gX* z<_g+zMsnV*O~KCKJjhwowp}~idFP!NF=99l9!SJ#AXhfy8mW zDIPH7wUDg9=Z^_1;Nxt{>Je__{Ef4yapT5#{PD-JY2yay@Vfn32&E_c3^Uh$Ai9bU z7-R)#MIH4JYMaSEVf7HrIG^NP&xxCEyb-Hbt~k>*_@fXkC8D9k<@VsqA- zI-N@bK%lIvy_9nmCtiB#MHCj~X>P&ke{R91yMp)06&R8gBy!SC#Jkj6NAWCxa35zz zn>MWx9X$;?tYyXioQJ!1?SgIF{^DmYp%hN1q85l|-%cII z(gQ*Y2I;Bzn;{~(IS~~#4uqO2A1r?05^R=Yctt?CLp&JmkLoMk3LKje zjyV)MDXXb=MnRhF>eK~Ws!Qr$QBmhc!m=% zzWBTrlZv|rW zY0S^^tpHU9W5*3db9Lp+3oHn5Gbf&Z?pY8{)KnQ86Ie>bI^5#S9oh=JeulUK?f&sb z79_Zl6R*AY3J6s`uc4NJ_7!=kt$e}5`UtvwH}JY}12w`81PSgIB6w95fyc-ck*t9B zS||kY>j3#~VC(=iYa+6Nr&$o;IZliiKE&`I1EeiCGu*`h@YSjtNceLE1h z65N$Yo_Zy{5l9t5{+w{E{i&~>4qn585INUhcP$8BkBeM^VeRB}N@w4Xa)ZIt2_d3= zJfw#hEB~1VAwoGZK57gIRIfMUjeyqBlpRk(!l+KvFCf(lxR;-UXKxmbBD-0T;%iRi z=N=FXmJOe~=7A3C;cG`Rt(aR8Nt zwM%6sEXXl^K>z*#YT>JJOSb~mz8AyM@X{pFrlCx22Ybsaf>c zj4*u#{Vy!YaTO<$4kUmew>qfC0l6^~qzZr@hG^yNgI9?Yb&DA@k@wY?U&t4L76;H2 zQT&Lu6bDER1m9B+RqkiZ#E-pu_eM>%50yTTQ~}UwTw7T_4o*Cu*33u~0PPnX{i>)4 zI>iKWkYfNRwr$;{Spk{#06P@#391KBkAO4*6weFCf>W75IAbPKKl<1eu<2Xqz+Q)l|gD4&5fNztzyyyaA-;>lK%)n&rVIW-GU~r z>#ZcK$(DclyV(s zf5uDOf>@>Ox1x{UKF z#!R&3tXR5uAqcW-0Y|p|EVUM3Sr~x{@Ad%w%$#pB69U;$`7ub4Nn>diM^2SMh|1BwCi z56naCkfyX*Oo{+#m*9$1_lAZu69TiJe)=h61dzOp3!s@a0p#*Z;F=##j*5+ACIsd@ z^w5L)B(YQh6r`>~?9dic1+ebtlLCllCIm*`ckew40LA67()}YTbXP+uN?)t+fl-B* ugcbe=s&n$TFlE!=eoCW;4V1HG0saacHj7w7EdKrg0000pLZ?*8x#P1Ia;6q$l0?R>4Tagb7&jz1(iE{#&<#b zu#RZbpc?q!eGf5VhMjTx>3}FfIOx|VK%f}N+n+2lhP0#rt+fcSeN1QU9NPt5TGf;S zyg*Et_xkkdjaW$pk&vZNfCx}`Jld&K zM^*kyqCsr}SfNrBS{MsE8pKl`tkhCSP&!x3$)x!S0yTND^z~}SA3fW@&FV~6G zPikrtV8__9KA>sC>QaEWi0kve1X&qr8U-Mp0t90ad*(f9r+k>8O@NHyoe-Jb6AxeB zSPGCsT%RN&7B8BwR{#7x(d*DgRHB2ajAo5&j0R?Y+TRRIdHe_~f0 znA8=G>#L#AOycUye(1pmAmh_0fX{Ei3PiE>yNm3!&+2FsK$14JBdi%cG3E6nXDN7z zxHf|Z4CoJ;pykoK3<7{9|XGG@o#5;HXt`bZlwTWxefJCSK`m=TcI1NN72JD)7pGcA0!I7QgAc0c} zdg8~IlhC}88WJ5LcN5nn>&-V_hm5B%PoDt%XN3VA%l=8|90>iTt)cvDU}g_&`=&D< zyfo3N2HE6E(vno_jJ@pAOCS^Q7wKIFg7OTaKy2i}Q2LjR;wFK!J2>=hS9NH#`(!lKh;QjaX_W=@55yC<> zgJm;;kzM6WsBmogx-)iD2)Rrg{l1%(z(=Kl8PmUCU&sV4ujXYSSDg*y?n_o@1DV5; zDxv^?ij}}erGdG&di82JcH{s6Tdx2Tk4II3oipxHRUmtm?kZpes6(QYCysZP0sU2= z6v#iaS)_edPh<{mp>sTJ1duTEuD||T$XMP&y#go-5CO1n@oQqkJMpLjMu59&*RGAc zoZ}F7OuqoE0)a>ge7+))J*u0m1e&*nMu7T6eE;1f$P^XiYL~!MkgKY|v8#J0)f}@Ayv>PCv!W=am z@)n+uw}nL7r*(Ao14aM|}G^;9#M6CVk*u3=$Hwdd(PQloS%-z1rO!3!XT7F|zz85=QZKpWBFF^%>c((-U$P8Wgpc)xZc)UNC9>`}h0PW8%0k2f-4ET{G@gjiLI;M7hlgi~y$-tXi=Q zGDZ106+(hQB&K!;2UdQlj*sQJ0Vjq=fbtp-_v+OPGLcZgIVz631b!=o2!NB@W{Hfi zno1Gmiz$ceA9$ z4G;|Y)P&G!2;)zl8XE^o%ld*NX=4N%Lm_V%XAWr%{_4mG@SlS5K5+kikgReSjkKKekga0$fe7-Wd)#4Pk{LudG3gLWz*Ke;tpMub00% z5-C!IVQv2%5sUy5W`0sq5+H1pmPDPUpkkqVlmdiosCxw`cgz>tf4xDZep*}IFUT0w z?B5t783F2%;wK(|6f%}`X`})on^m9Jt8N zzrpdxc>eqn`$6am2~g#2Xfk<`&Pf0bl!8h1K>pxRjW43N!Z>c`c& zrEH*<3|T3Z9K+j_4iYPY4_Ct+>DTuH$V4L{&Hcc?FN3TKfe6ZED24DA<#L%=YadA) zYwuh9s=Z_Ct@ak4K4yR2!{%4b?es4b)%T22Kdr@IFxB!W@ZoBhE(EW>@{)66sBcPG z4!4i|R#?swRL>ByAt;BSBzDoseRkf#3_EAna{I)#IpX-HsrIprzjzY(a6QZ&1XF(b z2{PUyB|xQsFP~Bb)u(I%Q7Ab=F*sNWd>9Bbh+t!OroJm=@yANw;}iyFL1Lpu$jv$F z42g^YZiCt0xpOC!mJ~ZX10#T&V49Q2rI%a`8DW=735)=4!nsU(*PXXRM#M^_1V#Wi z!Sp3?P7dKz0waKXUSa#OV=?04{*J#S!7QTW`gRFiCr3X#>|yf z00VOxd7P{v=+L19o_g{L%%3+0UcUT4Of+Osh)OGff$2tGa%5=Tx;5^&;|`1+I|f_0 zi-?s(;l$}3*htDMQAq_b7f~K1(?||MpFVx?zytSV*FEv-YLdR>H8O)7BuGq5#F=NFiQ8|#4R62oCMHf8hb4;^ zVAIA-^@nEoeMPJq5&VT%2>^b(7@XzeCh`{fiKLR_Bu3D%VMDZU-=0;YH~RLy0P+vm zzLs|wZn^m;+ zoOz2@Td%v_$8EXr`ri418>7CjTX)2FLaFoJcPz{|&g{3}C&Qb(r$hSl+kVwYKbFMU z8ffnP^Z#{_TTPGj!Yiq6B{QdgS5Ms?A7OPaLoC)uh{=J0gMm?jfu(^#fPo1o=kLX1 zIi1|98J^ds{`@KV)`x4Z9iGhhb~4$ft&4>PXWK6P&lNQI}XpUXO@ GgeCxGV1L2@ diff --git a/priv/static/emoji/f_22b11b.png b/priv/static/emoji/f_22b11b.png deleted file mode 100644 index 4bdfb3107c69afa25b605a62a11f18e05155fe9b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 666 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD~={fsrG?C&U%Vjf;!R%FOux|NqDL zZ$G?y^X>2d2S@r&cZ*(_W&C!X<&gs@ifb{g#5AR;TfBWj|-~V^_H6Lqa zI5Sb{&2shrGzp+NIXO9jfq@z6srUA`9&2O-f`WpA;^N};^z_Wk%>4ZPii!#z9v(hE zzSY&r%gSUH6iGi@W$|Q@+Os8U&sQ3rpRRjhmeHAsisz^6oavP~-pqWwndMZM;FEJp z9-f^2@YLLgC+9pqyXe!0w;$iX@%8ob_V)Dl_R7u8O-M+1y+A3CZ{gDVK&-h$X`|rSJR_BK&HoGQ&Y?{8tt&ik5e-xey#csp;?JFM)z3^+I@ZW=cVF|@ zzyIIAef8np>-6;0fPjF4f&xZH#$%0)_x87Dq^AZ323Ay5;PGbWD z-mFk>XlUq66o0o;qrSeLmzTFQN&M|fjoR8;Zf@?o`&-)D+N7kU-mNu0*}-@JPR zRD99qzd-LXmIV0)GdMiEkp^Vjdb&7s*6nS_=iM;-zJF%S|M<=G zx<1bnm63e>nZ0S@%0m+~a#mF+%@o>PqBi@qf2hW^Q^BD^S7*(*D!rNM?6orvyPX>t z7@1f&1O(RRt$(+H^_N!j!X6`D{bYs3pFQn%_cZET#Xr4W-fsB8`T6ocr>uJ#_xN={ zbSOB0wLbYS50rI(zW&ee{ZCSPbXNX}pEmWXseW>TZ)&acBGq=ofJ>cTOWzoAalAK7Tdy5K}Va0fs7rr>mdKI;Vst0HhxxN&o-= diff --git a/priv/static/emoji/f_22h.png b/priv/static/emoji/f_22h.png deleted file mode 100644 index 3b27e2de8cd26a53d5e84c4f8e3ce7c469bb7abb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7448 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 WbP@pQG7Dn>0000z?{ diff --git a/priv/static/emoji/f_22t.png b/priv/static/emoji/f_22t.png deleted file mode 100644 index addd9fec78839c418422e4a56a402ee003dcd8c5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 549 zcmV+=0^0qFP)C0001iP)t-s0001K zX=!L^XliO|adB~QaBy#LZ+Lik000020s;X60S*ohrgj{sb{wU39H4R=pL{r~m-|4U0s z@9paF?(1M+VDazl^6>5d|NmZIUU^uDuK)l50d!JMQvg8b*k%9#0a8gsK~#7F?bG2h z!$25?;SEI!Ko$kmN8tW1&}4r&{A)LvzB8QX2F|RejRrK6k4pv~ zzD9t?WitUp*}W&je^42+%wf&@@^n0yGZ<0MA!|=79j>+mlSrD*!pZ0<;gv z+Gk{NUjZE`Yo7{e@qkOzRk~i>fEKsu?*0Mv_>`8sBdppO9y z&_96$h<*toK)*yV0g8wL5TFDdf(TGW46p!Q0t!$?6mR3ar+R%6gO7S?ELGG=ljv)G nHtF9pGcz+YGcz+YGc&Uqr?$opU;qFB0000006^c_w+ARUw>ApE8QZpJlGL_s+qP}nwr$&PliIeK zjOy-x_x!rI%JQ9jbKbR{cyYf6`Cw1}q@g@if~rv?YDfKOIE^4PFImo{rQ+0>`qDI7 zP5bE_-K6IfPoD{YAEU(0-5LecJE2t>PceG84h+TlsZn^mI2LbLiEu@zPnN{u_S_g; zof$2f;Oyi`oT1Z`OiEDkLHdWV^3ib85queVi7d|OUBeqIdjx|)ijkVYpcyzGEQ}Q) zpPLeeGbY1LkqscRiqI6gOUMvukJcRFi@F3c@F+1wC#=(evjVQoisok~LQMG$AhGh( zG`dg7rEx^B>fYEkC=~ZOsu?(5GWhEZoKrXS6MqQG{2D-F1yENyPskDXJ#-&zXXsCt z#vzfRKjK<`VQQ3#z|E=wBvxKpNFM^dY|yf_CpPsD#hT90p*InP z88(2#YD0JIZ7tBOqzCqo2p9j$xr;YF+GPWnFIHo^$>7ndvu_pc6{+@2RB zE}-~fomV2+0IWiE-`~R;OS=W*!$uA6umO}k>SY56tp0?Gxm<8ziBQ*3KTdvwsNg9v+MP z$EM=JiJ5qCYHosx4~oF5N~mAN9q%`4aC@GTPUJ*U1axD3IcjZiY=BCPVEy<&oS&Nu zSJ&3X?LGbQP&XgXuJ6IChnMj7)l(SblacTqNc;pOeWh>W`L7aGd@y_e318d}{b8dP zH)cnh|4x5$6sPkrF~S+R80Gv7mseC|*nRQn>{7hEe+GtkZ;|*BNd6u!F>J*1jgiS4 z3`Tzc2Jha!#=AGK=;etf%-gtg}F9SCGtG#Q0ZsptFr`P4R*;Thc+qP}nHm_~lwr#6!uTd*$(qxiUY4`tp zCz=1V-#atu%(vD~mi2A#ef9|JhB)0`Sp451)IbWraWxlu@}_W%aRbopgZOBXw87e8 z`KQ-@L`B)_$>CIdR_+xR?kB5MCCyX-|3t6qy0~C$=%U;U81iB5_&+qD1?~TpQS!JL zy0~{J*~)T#q}TaLx0e1L(oF?O$2}aJLqVw*2-!ZY9sh9gu=uoT*Hg~Hab&lZP$dy5 zo_ZfC*1t=-sQ?`7!&)?=lyOnMCeMh-fFK@a8;DPzc`M~7&mpI~62$ZW8cur2KV$8t z0&tAvLL2+^#3BcpJUWl@F+7tG7Q|=FzKaS{7jjFl6~sH7ZnA;+e~u_q0sQCDXxxXL zsb+1Wu48`kxLA?Z@Bf8S%EU`3|M+aw>Vo(Rhw`ryZz=%Cm0akfN7_)``Xu#?iay=_ z=txn{9p>5n`5nEctc;@7N4qTh>d+=>Lqf>iKML^)^c$tR6l1|Ipo=3ZO1^(C;sIpjwgi=_|tr`C_Ktzd0YTjwX+P z5M#6a$0ePq0340DSns+#3~gSlZX@Z_m&^Z+QS$f%D$6-SRcewaE7lu80T#6iZA@o* zi#Jb4aX*>Y^~XkZMtR3w{?p`ixyVC^^%6RGMo|EcC%Mq zLb=pnt&27$>B91n@FCu(OuB;XmOLbT5@z(qRe%K@dAARpK4kvKMr&{RAU|#D4dirt z$XzcX*)ysFa9qZP#(&ru+dZ1Qe#HEPyMz*DRudl7!$MOcUeQQA`c?jf7$n*`a0O#(#GEBVW zN!GNz8-pefdi%W8+l_2rLZ)wU1>ks*i#W4hJU>pe%pbjdw0F_l&rhDOmU>(ot^!op zEUGAs|7ao{CC}d$LDg#$LB6JM?$Fymx9xYe$rQ1nVUrT=n3L_%v!trjS zMnSl>k3|oZecF|KzhSWRjPK=*;tt3TpN%Ul2(R~6(;uU*wu%b!bLhyS1GI9*Qkpbz zJPqjIm%jbxYx?w)kLmsQ-X+F=qzi}Tl{;JV=UlM_`HsuO zT#~r7VLCzH3%9}gwTt~~+{dAWDv|N)o%oVJvG)ADi!^2OB)a$BdsV^<{P&Ju;S$Mo zj7Pc9wgEly!DCpzW^q4j;gcfnI~-2kCVl0WKP7(uzPdT6ezY~yr{1`p)#1ksF%8lJALuR7b@|)xZJ@494)!f;5R#B&rgHo4;MJv3-@m= zJms74S@q-kt$$?ot5&X{Zr!?q_(Co(vj&1MTZT5KtclTdYHG9=%N@x2aVAYySwCVu zGJK%6rkdV;=WW&MCyFnbG$5SA`L?Rn@~t0FH?{ zA0G$eOGVw@z;F#0e257jRn{c=6N8@PV$cJN(*gO#qE8PjT)05B`i)##vj&LCNj;lW z(Y!cH8y_Xz`mx}VG5c}%HtxSU*A3|`GZ*lhUKd#+iBqbAyLBm3sRTr z!unzPD)~SE^n*(NH>?5UW-he$_Z~RrB>fr}di}V`3^510* z91~Id7tcrSA0=G@Q1|C2FVNijh2>Ye{+#@mm;q4w^;7Mz?JGI?i^5Ybaf;39s=`4g zO)cN!a{js7e~}pg*K?sYzxBZ2M|$#ymT%6%37T3z>U_1->(l!;mHc;@K`Cr`={Dr@LCi*k4M(qJ{HR^1o&VLMEPixn;Y5SpV;gF76$n>2@!M zI>3pn43+#L%s}YMg{FVrm8^^7q`H6H=fkNsjCqwxFRW0LxbLm4ruw>C>e0QsI?yo` z-UttF40nv)zf|i-=6`zK$K+AhyEWO>vk~y}OD}+Yg-Z)&FwDL4vQT0D$zx?^|GmU9 z=A!IP@7&g3;Qlp<{Z6};fcM@NW8DfaLCk=7Eh3~bNRb==NB0!p*eN-^ zgWO*E3IOW|vNBRt*Z&kVC??*Nz=>EOclGP=V88o!xbV3&>DKR3DhRmqj@vgn1S(j z`<6`*`|%AwX$pYaKYP)Wq*RLM-O`_71J6GDG{`^B4344K#RiMzkF@d958%Humpl3 zP|7%29l%cFyZe^>OmceUD?q=#y+QtI#!PhPvizx=BXIpg+7=MbyQWULnrzl0awt{O zyMIqV^&}Kv9y1{-fL(9h*NU9W<&}VV_MJKRK2ClmE`CT;fNGCIb>13EOic8zdYib3 zOX`;99gaF}xfJ+@yd*pA3BHQxu#jdArZ#SrbsygNFEf;%xdFHycz3r@h~P zj~f?Ri5saI{~4uMskv6WQaoQ|;2dRrZK6gYs6d1J%7ghpA8F;}`f!`p$A`(j-T`9V ze%Ge8-|X;awu3ZD~-Sl-ZH5+r9vUM;p7uL*st4 zlYr0;VWmz$@A~*FS(tbnD8nuu6Eg3I8Mwm^mRL&nwzG^@r}kuFXrKE%gFOmGcjFcI)KT)JX8{axOD?SBq?5 zwb5EXaR1_aPXeX1YV5?t`AdG5iL#<=5zwu`xA)E}jpZXCgp5*P@%}v4{&)ktF?o}4 z&GXyh(**l3X<0L_PujRwe}8=1OXEZsCg^AljvXDH3B}$wegM27tI?-mOUbg1v2lrd zG7AuzdgcM9Ft`8Z_Bf6;JuD zBE)ZEkjnAYAw)t$Ga)e}X-^P2L@}&(z1^M+M_q%Ko+CKLe|orh)_h&PlY!J}{LV}U zw6zgZ>TlkFi>Td$bE%N+yfV})Z^c?&r@i16woB$1epTS6%y!dv1iZhM>IkRN$tazZ zh3tmb!y@SsI5EH5pAV6dyNBN(76ZtGDm8Q|< zCJtsh(!tR?4T4BLbn=z?m_N-QMaWfzn9Cr+6%gj)%eUY~&HxipssVeX-t8L=*#p!o zDj>Jt(FV2&byE$UDrSMA_+NAz+yHn&;kj>*P6Ml!3BZ52CP2Y{Fc` zZ^1V_LlcR?P;gV`Ot!i8tOsTpWO_FztJs1@qv1$22~5NKoOQ08XKwo%((*KYv9Ish^hjTp zn7S4*C;FV$3H15#JN2dD=qMn+U; zyTInaayZ@B!xewTk(DNN{%QgBUnp`GvN|MwX>HL+nFaj})X%Kkr+k@C*gWc_KWlw* z5#IsjVg||%8oG9O?u$?7y5U(|v`v3mb-}>+!TPDgA2l2YOLK3xsly;sELrTE6I!}g+GE3HmX6VK0uD2u%dUpn zXIqhBhoP$ed#jm)O#hO_6lp7eCgpD#U--mV8qe1m7F-`c9Cp*|Y{t;e>K4a|b*aTW zu9zRMS1z|*MTd>nw{084s~wbbtly&~dH>q>T4H{+&fO)&3A4KYxJ$a|$d_Xwx#aC?Vt=n*dN1qKgU-{C&ha(Ju|g>@haSQzWtHiP@Yn*Ksd3B;ce@lgKu-7;ZPju zI;(F#Lp2gNV9e*uoiY#Dyrd}NHsUMbWa()p1@ps4G#XMRsr}Da#b1nsgGz^|X@l(Y z3^o*RC}a}@wrfWLFLg+X5Dn{!mzNrum0og5i-{t}n83S}uHqo9OZs)mAKsR^y-4EJ#( z5!t3mCS6QjgrDL^`MsrYD57d-DuKW4gGC{BWlM_Oa{eyM*LfK@cA1|JO93KQzkP)d z;t`CYGTb|s%TfJBW=Z+_66OPS_hY3|kP5a{u$!7oy(2nlqgOmW_|+k!JG!5e%Ph%T z^nd6t46NaNIy8R;8lDp9{R%eW`=xFC6E)Q8IB?RiP8Aabpm01Bk}>Z3QP9D|it`iL z#rt0Ewi}c=GICM<$(jQE1h@kSKVdGK1o*;KyBOH4bcf>Oyx@M#8zp%++40T9Uw}`# zR9JfY8Ygqdb&1C=mrEKsIcRL>y?3lSoyXk1w{{{3#P*dB=G^%vV{6)Hr2V9)(Jkfw z@YvjrGInAqAuaZ^r2wl4txp51td5|}2noj!@P0xsDm+H=SA)s{>+Q0)6KsC{ImRT_ z&SlUY=xDwE&m&CoI~pTGflLGmS%%4-Fa&);L$3YeT?j@n@RACTVDI12W;Hn%8QUTY zNrl1kvE+AcnLLpL7k1VPhh7LGz^Kv8)jqEy0l~Nws{ll^R_?F81^{b?GY%p{5!JGi$_6Bj;IyWY$z?j z&!OC!z-<$wbcrE<{Ko7*rtlqWNk0P|BVX@x`+*#lcWgLdpG|dJ$`z$4zYO=`x~6|C za2lt5vvB{!vF4*QpXJ`a;6DB>l{q5C!MpRdPEzqv_xf8;3m!Mf?dn|b!Sq5C#<&Wm;-m0H!ltE;3ui>`VOyB`v4s*djM)lJEg~XT)0X{PRVS$El$*Ihy1>p zz9czaxf1a&Y^e(CMRybbh+srq92Ma*a2}G;LN(+RU<3^H!|gKZkU3n|!oqRmK+c^t zq>J`@_PXNGWM(%rRd1CNT!OYN$FY7~!--rKbSm~1% z-d;54`!Ua+nLDItf)yeV=Rgx;GPVntWu`J7mL5*eA#&!s&LpS(k@mv0z&okRxFV9( zqU)eVGeN4(cK>XvX>g=tBK)kr_0tE@97x9w;D$!m8=J|j{6fEzRT~~qvl4lZ$S&qmw*_F0<&}Hl@eeV5o?4zf7xN_X zv!<4wrqNH>F-NUbxWx5o{d)ET#LLHw@1@5h2k$m4lK5v6zB)wcH2+l6+19}g%AQ_@mBe&jj;IYl*LCcA2_mJ@u9J7lC*U^3Tf9Y!d~^RqtE)F71)*%4=o zivvP}eb}jDK3vrNtx9X(ei+`>7g_M1x=TPu#lk!2Fx@Mpzrqw_PXYmWAH<^I4DF*Z z!DGc~+Rq?;03wryknI+We}7ljUannT2tvq{oePE0!<^&Hk5R9de+Kap*SsZ(05h@L zY8^Ul%E@>RT8J1WqajdX>H*+~alSzc`$`FLOJTs zqgH}G-Ivor-8G*`1s;o5uttg%v%+B#abS;+NvW&vA6sZAhlaZ|4HUE?JWmC-&fX55 zcoC&7I9<~iBdZ$XsL||XzB^p1mY_;70EdCHF=__c2diO@KK3KO>I#;~0>!8v`G+1O zQs+)P=0UkEvz;;-4sG^Oy|_Q|1;h~MI6KPeSuN#xI{s`0N2Dl@X|9UTc(o)g*9V`j zv-g@IQ>=&w4J?rUqJ_Fg;8?4HS}g1y%Y;m;uwKM1bhA#3rmkCI=h~3SA%RWvk*y07 zhX5b)*~+~XU}u^KKcK_TnXI^04JMA?PZt^xxVyV6iE870=aCR!r+Nr2Zk0-SHaf!= z(uwQ?#NkU8lt(#nptFFWlhH1_G*M%tffcbg9ro#|;wxt((-)6PQ0=$f;=Lrp$e)Z& z+}zYxd7rtl)_PQyGIAa(Nhvz(0gGWT1IP*L!XTtvB>xkTg0sxSm*`oT*Gd~j!LDhh z%3kcP&iMnuK*1ID2^1O0f2D9`R4&+ z>?huR9tCbfXA5v+XWeT^2+|8e>;@Kk;38^`ooG>GI&efCaxP% zX$A-yI)E95F{4M30q$T-;i7Ji1X%ifGHiPf~1598{Va3S@>;=B>hDj5jSn%Y|u-2z_r1vgXU6^V z4+&r2w;wcrF$6nvc?QUTl)>idx0;4Y_1`JY0E9KGR*?9_gIS!T{P;0@g7G~@|I6~k zC+0uKpep}W9sn{xlT-uDnKeTgAd$r}UZCMmF-CpeCmHmAc?QTy#2#R8-mr1L)FmL1 z@cw&mllV^L&T%{VLBb1@214-1;ZT&5EH(Ozp6dlGUmwLi&U&uV%#ULh)C~3}a8{bNU;v9+M6JJh+yhj*REX|c(|AT%F2nQd0U|bG?o<+jslY4>w?JyeqN>Ba7 zlI(q<`1wA(xx9q?!;CK5g|_tH zA}{^B@%JLU{_4vlelya+Y=UvfquNv2BgkKn+TOc6-~z1m-#xfLXx|Kl+K)4!FgsZ^ zd@*5h)<7tJaR7JLY~cQoNy11aVd8{q$N)2#jQ}*u@3VI|7(IA5xbUd%@WR9akoRJ; zwY%5!`H;4Md?9?Sh|y2Y--hq5r0E}*+5Hp4K_38*!iW*W$p8;AyWoe1_K(bXJP8Ug zy0>Cu=lW_Wcu;uyvek6OTKHd|0D9a3_3LJGgH!r%lpg&X*O!Ud_`>1(NS_VYUs{RK}ru=zU)JAadFnogofumA78YfS(RklbR&sh z%49(qAeBbGMDD8JVr~1nCD(ppBou(wa1i$2Z$C1?1IhprJO8EkGbms9tQoW#zvh@l z|HAomMEcKA29T8c>BP_KcMjpcK$z=(LyUdpvfThT-FO3uzaGhy0VG5|9r!4GYBz9J zud#+cZl`Gci_)>phFKmgv9@r5tISCvhJrt-b0M``l;jRx)Gu^ ze@vslq$pp;@o!}SYWir@yYh{()bMeCBzATG*H^&t4S=p*`zr&G_%!KT@p1;&JWg&z zV=2B^`f;e(wpm2~*~$Q2cETqe@-gz4qxc?oy&21n|Mwd|lP66e@oQM;Lm7aYJ!<-> z=2P0&Jk2~1bMZy(=TK8!0SO5SWPr<*0lHwFZ!Ps6WKjB1Ce{3yr+q=M#|eb1uDpW8 zuVn;ffX=P=G~~&9kd}JaQ@0=UhVS>C|K^SBMD0IM8K6_w_-QoWrFVUJb>Y~NYE(%Rh7G)?U8#hk9@1|ZCzGplpf{mKBM z)+bu~`Nhxo<>d>XK zHy3kP{YFkv@AU__o0Ko8Aqh`B{-}ul-pT+S6+U+`3h$t)o^^(=e%&v$yrV{s4swd9 z-)`8_p8CtTZXx0S>v=zA0MXnPzc`RYr^B1P?Bf%8?W!+Ke;C?M@Vea{IK?X`^#=_f z5Dq%Xy6s!W{wS45JniWf)H=Q-5)ZB4{lki#xBVK;jzpiYJ5dU@-ub@}AkwByg zU2)bRD1M3ZB3Sf0RK!65r+HE8iGpVpC^UEX_8X%L`3C5VfMc?WN9^4%T@2 z10ORQ`E}R7n8h;+p4`=ouWoZ3(4fdYI6l8>(hHpU2&0Mb@R^&Z-)2_kziKvYoM*b7 zwWdD;ZK8((G>;}h^iHk&;-H6k<|T;So5eF~R(|frC)Z7Sb-Q81Y?I#JZK>a6dfZO# z3z$}FC&5v8@M5TY+$~-3EQ{17pSa^sJeI{fj40?xAd<+avGon~{Ho@0OL&A)4&Cju z{=8*Ng!uO%71}=u1ME!E^wrnZ!k&BXN#cLQ;seS6V%QUHjX-)@Dv7_E#RrrDT+RkC zQFy|UM;uP#w;=ao3idYuE%(GG&%|r47Il9NQ?S1QNc>RH4}`n#xp0VwT@ z$bayG`$+h>c+^oDKs0>3wJq}RzEg;QDO0e&0Z4p-Yo|_8ksnV6ptMhjghO!LvBwDU z&tMAnHh{Z*AA(R@Qw_t14HL(?4`mAWHUM?{Vq<6NlEu)oXAcs;SpGF`Wq=sfdlLw6 zzVW(9`JXcddmDgKKG6b?!la24g!uO}1v?u+tnd9mSiNc`j2bzTgm)oNW(sySfQb4= z5XjET5>Y=3*`F!c*8sGQ8;t~kuyMnBIN^ljh4^4~2gH`icmzK~6(HggkKvyKSpLFJhm6{&{fbnPPQ$pdK4rlCqvI3)+rmohV~l%HS#g!g~$=eZcJf^#v91@k*_1O+Qq#Cc@6S( zGdzN+2?dj>^`O~DtGgC5ik8#?$IJS)E zdO6ZxAbuD{xN`k){KU-!N>bN!R2 z@7yXQSKhGF>`QFXj_da`AI4wbcX}Q<7Ew))f!@d3U2EdQr8hCY{r|a};a=8x=lQ3n q?OFG+KXYrPuprOh4*~w~Zx+hOtkv_ZW~xwP00K`}KbLh*2~7YMsEX16 diff --git a/priv/static/emoji/f_33b00b.png b/priv/static/emoji/f_33b00b.png deleted file mode 100644 index 65b6e24b8fb55f67817d8508ccae91b85b5acfd7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 611 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD~={fsrG?C&U%VEiNwR;o(_bt^912 z#f4eM7iJlq>6LhLZppX5|3AEY^XbFe|NsB{`ugPN=BB5o1Jy4plXm=J95hho|OxdwV4$Bm@Km1O^61MMY(0W~8U5 zW~8TneE;^tyVnnn^xfUpd~bj2-Tf{14|SjJ7CqL;aIBH>WC!2*>AG*%S-x4W{(8Ck zn-%KsR%*Omsqt>D@sw=Y{xpdRX%c;@5}k?Sok`+CLPCs;jLgi;e0+Soyu94p+)`3f z)z#HGIXMLd1$lXS4Gj(T_4T#2wQX%}3yP$lEm6zQ&wqGwPDMq<^Oc6@r|X>R5`27i z(e0Nd&wzeoED7=pW^j0RBMr#r^mK6ysbGA2xzUX!QKI!>@iFt;IkyBE#kw5?LcdSg z^-ymC_YJ*;C9H1`srfL?IJ5pfpZp&E1C8u`rswu&E_$SK``*fR7k2#PI8YbLn_-f- zG$?j?Q10p~hdiK7SV6;eZte98JD09KQ?u+&Qf}(EmzU#a&V7Dg07Z4%oqM&Z+WOYF zLJnNm@pkIXWr0N~@&ebdt+_ev>w6jXoeW1GeRV6|ws$S#jk*n{>2-Chm^1D?UiiIR z?bD_BYa#dIfAmVr@j`X)TFWK-F7!?G)T8m2<|Zxf)O&mP$qN3{tKP83a!#$YosoVE Oq|npV&t;ucLK6T9k_2G@ diff --git a/priv/static/emoji/f_33b22b.png b/priv/static/emoji/f_33b22b.png deleted file mode 100644 index d71a8ddd484664537af3fbd9b3f2703f78193d71..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 623 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD~={fsrG?C&U%Vt*EHrjA) z;rZ#h=cntO>Jof>cG0)L|3AEY{prKo|NsAcdwXSOX6EPT1JyrUqIPDY(wT{h$D5fS zo}Bab@BjC2UwwT4CN3_nprC+}k@0vl%fnN16A}`ttE+Qza`N)>ii?XI8XD^B>uYOk z+uGWMgoK!xnR$76xw*Neq@<=~%l4;9Oh}XHOO@zM6z@zDf4k1|&2shE%hlhkP=B{l z zjP%rifPlcjz^JIGWo0r?7OADDr$0P7+uPfdhlgi%wequ778hn2o#~Z$a&C#QuTO4n z?gf|5a-iQBOM?7@862M7NCUD(JzX3_Dj46~Xmn#K6lwifd~EGZ)3*Wvjt!y{8FDpj zYpNQY7@3$Z>Z}T2l1O`Fe^~TBd%;6?@jlZt`!koET%7aUH?_5`^xgD&)|@NTn6!Aa zqkXTK%*t_p+HePG7Z5a*H1z*nX}LN#@^+g0=Sx}V;`rxn(PH>uMoV?-wMze;M_bpPsi~N@ zn?K_4u2+|~FTU4mf9&ABivK_V?^i@okHjfhy7Jz6)2nBvb8+)l@7NYD_435K>r$`J geLepsY|;w`hHaZ=?5^gR>;`G^boFyt=akR{02Y=Ag#Z8m diff --git a/priv/static/emoji/f_33h.png b/priv/static/emoji/f_33h.png deleted file mode 100644 index e141c51846c2072fb6722c83ef2c998e3978005d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7246 zcmV-U9I@kxP)I&|pJQH5Bjbc7|U5<&b=g&lM}Z7>*MwOT>@3su-a zM*?vy5t*5p@Z{ux_L%{tBjnMVxH}>u0y$nUs@150U|t@~W-~iL52Yid@c%Jn?+*mv_4!a(wJI!*I0RCS=-Duezt1qG)_ zY6i{9&en|9^DG?h>~uPvTC=B#ucdxO#wN?N|MqMa@N+)Ce?l?oLNVS z&2HyN;s#1HCLdQEqCBq7Jkp74oel?!&thxhYn13$hG@8x4A}8~9+tf4$HcdDFul|Z zZ;DI=ZzWcOJ~i8#lA6lKt87qdtP&Fw*>GEk$?D9(L7c-&I(EzoY5$-O>oP06d}S1t ze;C9ba)gy11yIu~%fV-f6`7dxM6Wvi z=*R1P9_@0E!q1hSks;1dG5RwyvqbPuKP(1g_NedBpkWI+SH-1}lPC`HB~eE?su7Vv z#^jo+3C^{(@-b%uqtVC)?5|EQZY9F)c1t-bB*rI5Ns@H0l>)xT=L+(C!0rXWrpW+S z61QQ|#Glc!MSCqHY$sl>l#b@a9h_D<{c|+2A=#iDU9uu5AFS{;EH0Aj`1R zgL*{R9g!q(HPotI3u)<@FdG%DL|0UdWG8k|w?}F}`j_+pum!u8jwFu4-j$<)y-Oh2 zzZ5;&H`N?rIWakG*ZeN01NI0D>~@D1?cxY|wC`}(A!#!;9kuJ!MwZ(n$3<5pqDKQ6 zseYnPJ+gLm(iNu^BtYSgL)dEVp%>|Qq&*g6Bdmydxs zz>3iT)=M_{X3)!UXGtlWOYE+~5Iu;?;$ouk^QUj4PW3#{WK@fEiKfIV>4jXgl&V%)noT_~Zd?qQ=pAN>Q2^ZN{_`FUG#%!DTI+^0{dV7V7K+*!5i=Jrl3% zBgKLF+JpDq1Yj$6lkmHijKtoxlK^bS^xvn6;y^?5CVTQIu*lZEF<;vc`{>50a zY^oR}sY(i}Rj-a<`2>&x58dk7fq$#w#tzao3qOA-16#fdVg(t$NxqDTzfq?Sorvgn zdn&enlZ!>~`muusI)C-a#*i1?m^Z+Ob)N>&?>2T$1C%jBo^kT1?*S~u?q#Ea4O4(k z)6lzX8@bjbXhG5C@nQQuz(AbbvS|Zo81Tm9Vwg-yp+=Kc(tl!#bkX?X2@#u&o`;K@ zN27P+D6Ic1h*7V3u;KF{KDXzZo>W>3xoacZD+_)})CBW+OlK!!7 zlCaNXx5I38BGIAHwXS55wK7u(Lo^{`!r(8!xZ|cv!N(@ERhm18!zs~!eq$H5ew9mN zyD^E3yzG5HM<5$qM40fF2kXcgT2&WgepQ}MuP1&^oI~74M2yn}Z>kk_{C2dad%M?< z#6``baYgGGT-z>&vP-P+2IAFiV{mz^7+gr#+tqTRS)r3gw;uW2(d2Yo0xDB;#t-gE zmG4pf`m+EwQS^_}>Oc0Z8Vg_paX0$C_B7w8u~3C6W*5Kk5E!kRHxOfVtM9@e&t=1A zG05@XlxCNI;1{vSh~POQKJT;PSrB%B&mNRsU@vh`e3S|8Ye(YVUI{3Ud>gh@hkADhrSNC{y zsTav2u=s~H{5VR>v-|mQZ_juuN zrz|sXV1TlSA0vvK7*-UGF|TD}LP;T}4r+>BL$Ai;SJptPQ_1(kuLYP|l7lL-;t(Y& z%#cRhGj-V4m_GV9ae$wmNaZNvi067wk({GN|2f5RynWp*;*WkM6(e6v#E3r8_~!{D zro5RzWv38xsd*gwl3N_U>^A8em~B-Up-*;px=hJRUVi~}OzBqYG#fD_Ifgj?O)f!N zR- z8VC#Vd8jpafRDrvqN>{_;t#wl4fwZEekl}4mr?S+D#Y>n&DURwAHMrY4DeS`1{)>h z#pmX4@;f;)-E3RaCExwK$1<_^hdk*AI3xV`LN*KrDFrX95XSjc^L+65YoLz%*y}$J zXr`8Uc^=Pi;u0HWh6>Zp#!kk)`n0g)fxCZu%i`b+mH}t;%vl!BT(7#<#T7!dCpp`w(Ik_JJ^MfV&i5s$(-+OW)w|Y?F*9f1bIv`w6wIMsJVppS`}cJe^tpI z7GjVb46Sn4FyX@&nK^`J(CNw%0?>po)J(#0ck%c0L>Q2ASje}lRy)!5UVGhzJvZOvyapgd=7!OE}|TPyQ2%ghhRJN>=Xi3Eq7O2&&~Og z%B4WLn^HSOEh}O$mCGt0s%WV;ioqF!TC~l{{k^3qC647_T^^_+3F!AMmwJR?KShG& zj!uB-(ZOy^-?B}LyWg&kXbeRc2pQ;vQR_UQIbyWM)EpVgSLu2Z@Yu-5N596ACyc!z zU~fF1kX~S6chF0|NYdJWY)7=IZ^Y@R1fp9=z>>6?!<###e{?kAUnRisTf?bp9VX37 z4MqQAqgMOZ-H(MN_@QOCz$ix%{y75NdqEJLK00at!mB<4{WQYB(R&Xx2a4`>1Oj}( zntJhmc|;Q?9}#AINWef*B4y~!DFQW$ zaDuRGmks7sjMN8aOt5DH zJliMMgHbm|{~TWE6b6M(AuoB&An(19@!>$^{BjL8&3p;lmw$rYS^uDL(_)x*tcGb< z4oo|<+l7+dxj=m$h7TWxo_$l$r*|@3TwK(g1Roz|2G-a!0Uk<8`oZQl`i-tj!;B8?!>;{sFp|| zt8kc#rLdTuO%o!_8evpfxQ`!>!k9b4@$wC!cuGp8S>>a;b!DQ~ZD+NBpWktI0K0jm zk~Ua1g1#cZGvc-|ynSzk>bBwVi0Q+Om_H_-VO)|;Lb}N$no0wkwGuLmMkJ<|NgU?s zNt(5v_QtZ|?pX4M0qZ9|0CVAX)BvccYZAejDELkjdRb{P8tSUhdezlbVdCF^;KB=< z=L>BXTwrjJ_1(2nuEhe2j~2;O9);W@vQ* z?BC~3pE=y9Ov+UG8-o)(G(xz-#XA&E9wG8^fS`xKOW-bW5$vCYF#!fqwVB{5HJ6Cs z3p#wKH`Z5S-P*MnJ^nqs@X83>_sH|O;>HKi@0j!8?4=@otx+%dQgE6!nS6kw+@avE zwLs6&M!_w8Nb>AhFi=c^O%1aSuR!cS)icckRrNZt+iS&c=X`Wv(5|x*h&G<~p8f;N z#DaS`DdC2MhI8Bu=lOBwkV1cfHq=?Z*AG2=_d!z6UWiXjLS&2)LE)TCVk1v_#wu`i zma6Ca?Wb?|5VX4>%tf@7XsqLs4LE(ECr*`exZ`?zV?ewoVnf~FPk$`;K!0Y-YgC3qytuZ9TeuALt|L2z@RQ@AcuR zTK9!I;bGWc@{@hL_ z03AvmCwFE3)DlJ}-q7%-GnS3;kVvBsR*d$SSVSnce`&<;FY5k*C(T>)WOb6ApY!A3 zkPvZ{m4A~i2(cFeyfh@ylXjk}tWBv;#t1k%hIT$(P3dZzHo%nrc3W8FXZA@{eNk^ST4So8H+SoP_# zNPoW%a^H`@aygzJ9cA+wj0p*QPFC+}d#;0q4B|LY*#bB&dch zlw8>;sD}zImsd8RtgKv-r8sW`@-{3(&DMz!Re%f5I$j&EXKa1B2EmV;9bRAG<^MKC!Tdt2KYPyEXYsM}KEiz*UVXSW$q^7>bB?=P6Xd>lT}Yc} z0i;XDEp2Q77X3N0Y3ygSkaE(cFu0Kr1_d9%`F#x-bE_9-kBAl1(o1U+?W=12F}*)- zJk6VD{(WvBInb$qs*`F}%`KYz54a8ypBKTEVDD8eB-puq3)bdjAuV+U7B8HS`SWHg zEM2@18EGpe&X9w=UE5GoRek_+H1hB0A>n<3NI(;~PB`AC3F719De!$p?32{$?jk=m zie{(4r@L<1Fu!$g1ke3#_Utnz1Tl1LMeR1L9wAFlOU0;h@8Oj5Zh)Jgb`z(D-T?;O zeyRZVyfW+G1M>#An>4n6v4^p_WbYO$Xd2Ozj>_%%9;j1z=M*a*fQvt`qIO5vac zsI9J08M*Oe$KdkIE=6x45%crDe!d6`3)3C3{etP-pr9ZmCnw>=6Hbu${?&Nq>8J4N zCm$)I6y)u|-mAW*lK+=lMo@~czW59Qw!4&&l$5kzU#HcWeO_DJZLQ}<0lt0e@M;6o z-|HnFvYRAj;;dz*`BrdQoAywlYjbn(`Iq0}raK=;{D3oHa1A|hnAp`#?Sue1F8Pa) zJ-xj55mHYP$lBa&aB+tpJebpnZCf^iSH|&oQ&NWD;fEf?i6@?*r)Y-?`fC+CJvH>x zzJ9uPan${SVS;b9+8%Ds+1?l|0k-@W+cqYtq6z~C0 zJN!CLfp!O@S#V}o6X0k8{&_Wyl|dBlVUd7^7puQK&05*O9D5VYraV|g0Cm+B`>Nl{ zGNh)a;*-z6$E&Zsj$3cJQC`SnVKl~pcM)pmWCiZlVA+L(MFZS^+pU;9@h|AhTwEY1 zY*WV0RG5dRR>B!IY9u&+xE)hedTRX?qqQii#YWC#+;{KY`0baU!1euiDw0UQ<)A?? zqK|g56$!5h*Tw66hAM>Dat+y-K$NR_LpyszP>t{$ZqRO=yz;NpoNkM~>^2mtW+IysU%o zEI2{%l13~mFVLd42@MJ1{~`f+$>sqAU+OL<`JQ$({IaVFa4V-`GJ(S|=D(}Gi^t*@ z4cIv2`KB5GmeSHT<>DzI7E;vLRw|RR=dy}gt))%vz!*OX@RA}xC*MBsxc7F|<*gVUAjKM09PUY*9y5lJL9;bZdf8XvlYwcvse_tBNCKLIHD90IRMh}0i!vEjJwkv3u2{ik zg5HL1TK`;ssy{Dy=&(r}^a&RKzrAySm8=P(@c*xE+qP|+!P>TM+qNC7ZQC}VvTwY) zJzM8l@$0$0={iYeV$GNCy46*u#*zK{q5dKuDfG7>Ctv^+)!A-!L*D_S3xJ@mUA&|j z`1nH&9S|@8>gjs_CMn>{7u~}eL1S65#n)#iaT1_wm%>Xi09a2(woWBwLKykL^;t;* zA$WRBI0cfzZ;AoHdOf*qYP{CrPm=-x`uMNUP9glf-Uz&<<0?=C09I^@Z?ih5MG`0h zfKUi7jYU$}&p>^EPXKk4n8La!^eI?cpm*-h@7@$t@1!(lK0swi8KH9She((PH$ zV*TOfW;X+%j`9M4og*^Ak5|JJfK?Cx>>ZOC?%tAffGkJ^sUC1#p&Wo!Ja8l|GX_9q z0sspa{2IQWo)}igA4LFQm7##`I{E?R8Y`1`&%-Ts4U5es&{MAl0IUiybT~343ycn> z0)Y9t7e|JK-v3Gfz^Ve*4j4FKE>Pwd06hBMUtM81gh~Lgs>b#N0E@C=npS+ezYm?m z3IV{XjqP1$XJwPQATr3jG5(*=4-WiR3jkJ~_;F*n&jEmSa!&x%06yK_#W{d#00f)> zGjRgQPXgbbpJh1RDm@3Y>SHSCV(3(mv_Kj7@$Oaz*|Z9{d#M2ct0^{KVPz}!_DU@T z&Cfu^pmf6L<2`{{dNlxGHBKBqhWoN~077R7U!R>CdJQlDx&VO42~#EV)G2LPEEZw0 z*ckwg00?~%oDx&e=U~v+fS1Vtu$p)jfF(I`4Pam-v?9m==mP)(*TDVt6>$v+e~Zmz z09bVspb#`Wib9aIuYvTK8WP^`?;6So5J&`L zh!5A-A`vth09LJdv>f8h%xtiJ^%wwGrchb<;zD6Q0IaIA`vIZ|7%q!9XJ`IV9{}zL zkOsi(von6H0syy!O-4(Y$4>wS0AQ7+a37k4rceezH-&BrnUEPSp$veADgprZkIjr# zF${o)DnLPN;b=5$3j?5|asz-JBQjwF76YK`bnuqY>0kyxGi9JK)`$87FaVk<1%+#M z|MXb+!~kfhBoy`~y~rbo0nk!$DZJ-&A(yaS41kV`Me${3r!=sMV*uDEkO9z5aVWe@ z0Kom`>C_?EnA(07*qoM6N<$f(Co`hyVZp diff --git a/priv/static/emoji/f_33t.png b/priv/static/emoji/f_33t.png deleted file mode 100644 index d5a23073d9ebd7fee72e78191940db5fdf482ff9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 563 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD`S&H3s;ExB}^liV8kHz6C|n&sQ3r zpRRj;y3VOC!N+G8ef#_W!@Jj?KD_<^|G&4lS7v5rettes{j()%XC^A0nW%WYnfc+# zIbZ+&fB*K?$M@&R*PtGl|&lY6`8p>D_gTe~DWM4fVxrt9 From 3411f506b3bf2cbeeb7f3b9e746eab652f93d530 Mon Sep 17 00:00:00 2001 From: x0rz3q Date: Sun, 4 Aug 2019 14:35:45 +0000 Subject: [PATCH 36/48] Replace "impode" with "implode" for --- docs/config.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config.md b/docs/config.md index 02f86dc16..50d6bfed4 100644 --- a/docs/config.md +++ b/docs/config.md @@ -25,7 +25,7 @@ At this time, write CNAME to CDN in public_endpoint. ## Pleroma.Upload.Filter.Mogrify -* `args`: List of actions for the `mogrify` command like `"strip"` or `["strip", "auto-orient", {"impode", "1"}]`. +* `args`: List of actions for the `mogrify` command like `"strip"` or `["strip", "auto-orient", {"implode", "1"}]`. ## Pleroma.Upload.Filter.Dedupe From e8ad116c2a5a166613f9609c8fe2559af2b97505 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Sun, 4 Aug 2019 17:13:06 +0000 Subject: [PATCH 37/48] Do not add the "next" key to likes.json if there is no more items --- .../web/activity_pub/views/object_view.ex | 4 +- test/support/factory.ex | 4 +- .../activity_pub_controller_test.exs | 53 +++++++++++++++++-- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/web/activity_pub/views/object_view.ex b/lib/pleroma/web/activity_pub/views/object_view.ex index 6028b773c..94d05f49b 100644 --- a/lib/pleroma/web/activity_pub/views/object_view.ex +++ b/lib/pleroma/web/activity_pub/views/object_view.ex @@ -66,8 +66,10 @@ def collection(collection, iri, page) do "orderedItems" => items } - if offset < total do + if offset + length(items) < total do Map.put(map, "next", "#{iri}?page=#{page + 1}") + else + map end end end diff --git a/test/support/factory.ex b/test/support/factory.ex index c751546ce..8f638b98f 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -182,8 +182,8 @@ def announce_activity_factory(attrs \\ %{}) do } end - def like_activity_factory do - note_activity = insert(:note_activity) + def like_activity_factory(attrs \\ %{}) do + note_activity = attrs[:note_activity] || insert(:note_activity) object = Object.normalize(note_activity) user = insert(:user) diff --git a/test/web/activity_pub/activity_pub_controller_test.exs b/test/web/activity_pub/activity_pub_controller_test.exs index 40344f17e..251055ee1 100644 --- a/test/web/activity_pub/activity_pub_controller_test.exs +++ b/test/web/activity_pub/activity_pub_controller_test.exs @@ -180,18 +180,65 @@ test "it returns 404 for tombstone objects", %{conn: conn} do end describe "/object/:uuid/likes" do - test "it returns the like activities in a collection", %{conn: conn} do + setup do like = insert(:like_activity) like_object_ap_id = Object.normalize(like).data["id"] - uuid = String.split(like_object_ap_id, "/") |> List.last() + uuid = + like_object_ap_id + |> String.split("/") + |> List.last() + + [id: like.data["id"], uuid: uuid] + end + + test "it returns the like activities in a collection", %{conn: conn, id: id, uuid: uuid} do result = conn |> put_req_header("accept", "application/activity+json") |> get("/objects/#{uuid}/likes") |> json_response(200) - assert List.first(result["first"]["orderedItems"])["id"] == like.data["id"] + assert List.first(result["first"]["orderedItems"])["id"] == id + assert result["type"] == "OrderedCollection" + assert result["totalItems"] == 1 + refute result["first"]["next"] + end + + test "it does not crash when page number is exceeded total pages", %{conn: conn, uuid: uuid} do + result = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/objects/#{uuid}/likes?page=2") + |> json_response(200) + + assert result["type"] == "OrderedCollectionPage" + assert result["totalItems"] == 1 + refute result["next"] + assert Enum.empty?(result["orderedItems"]) + end + + test "it contains the next key when likes count is more than 10", %{conn: conn} do + note = insert(:note_activity) + insert_list(11, :like_activity, note_activity: note) + + uuid = + note + |> Object.normalize() + |> Map.get(:data) + |> Map.get("id") + |> String.split("/") + |> List.last() + + result = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/objects/#{uuid}/likes?page=1") + |> json_response(200) + + assert result["totalItems"] == 11 + assert length(result["orderedItems"]) == 10 + assert result["next"] end end From 96028cd585ac23a4233f41a6307d80979dd0e3a7 Mon Sep 17 00:00:00 2001 From: Eugenij Date: Sun, 4 Aug 2019 22:24:50 +0000 Subject: [PATCH 38/48] Remove Reply-To from report emails --- CHANGELOG.md | 2 ++ lib/pleroma/emails/admin_email.ex | 1 - test/emails/admin_email_test.exs | 14 +++++++++++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e2cbbb88c..2713461ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - 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 ### Added - MRF: Support for priming the mediaproxy cache (`Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`) @@ -79,6 +80,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Removed - Emoji: Remove longfox emojis. +- Remove `Reply-To` header from report emails for admins. ## [1.0.1] - 2019-07-14 ### Security diff --git a/lib/pleroma/emails/admin_email.ex b/lib/pleroma/emails/admin_email.ex index d0e254362..c14be02dd 100644 --- a/lib/pleroma/emails/admin_email.ex +++ b/lib/pleroma/emails/admin_email.ex @@ -63,7 +63,6 @@ def report(to, reporter, account, statuses, comment) do new() |> to({to.name, to.email}) |> from({instance_name(), instance_notify_email()}) - |> reply_to({reporter.name, reporter.email}) |> subject("#{instance_name()} Report") |> html_body(html_body) end diff --git a/test/emails/admin_email_test.exs b/test/emails/admin_email_test.exs index 4bf54b0c2..9e83c73c6 100644 --- a/test/emails/admin_email_test.exs +++ b/test/emails/admin_email_test.exs @@ -24,7 +24,6 @@ test "build report email" do assert res.to == [{to_user.name, to_user.email}] assert res.from == {config[:name], config[:notify_email]} - assert res.reply_to == {reporter.name, reporter.email} assert res.subject == "#{config[:name]} Report" assert res.html_body == @@ -34,4 +33,17 @@ test "build report email" do status_url }\">#{status_url}\n \n

\n\n" end + + test "it works when the reporter is a remote user without email" do + config = Pleroma.Config.get(:instance) + to_user = insert(:user) + reporter = insert(:user, email: nil, local: false) + account = insert(:user) + + res = + AdminEmail.report(to_user, reporter, account, [%{name: "Test", id: "12"}], "Test comment") + + assert res.to == [{to_user.name, to_user.email}] + assert res.from == {config[:name], config[:notify_email]} + end end From bdc9a7222cca9988a238cbe76d0e51125a016f8d Mon Sep 17 00:00:00 2001 From: Maksim Date: Mon, 5 Aug 2019 15:37:05 +0000 Subject: [PATCH 39/48] tests for CommonApi/Utils --- lib/pleroma/web/common_api/utils.ex | 104 +++++---- test/web/common_api/common_api_utils_test.exs | 219 +++++++++++++++++- 2 files changed, 278 insertions(+), 45 deletions(-) diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index c8a743e8e..22c44a0a3 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -47,26 +47,43 @@ def get_replied_to_activity(id) when not is_nil(id) do def get_replied_to_activity(_), do: nil - def attachments_from_ids(data) do - if Map.has_key?(data, "descriptions") do - attachments_from_ids_descs(data["media_ids"], data["descriptions"]) - else - attachments_from_ids_no_descs(data["media_ids"]) - end + def attachments_from_ids(%{"media_ids" => ids, "descriptions" => desc} = _) do + attachments_from_ids_descs(ids, desc) end - def attachments_from_ids_no_descs(ids) do - Enum.map(ids || [], fn media_id -> - Repo.get(Object, media_id).data - end) + def attachments_from_ids(%{"media_ids" => ids} = _) do + attachments_from_ids_no_descs(ids) end + def attachments_from_ids(_), do: [] + + def attachments_from_ids_no_descs([]), do: [] + + def attachments_from_ids_no_descs(ids) do + Enum.map(ids, fn media_id -> + case Repo.get(Object, media_id) do + %Object{data: data} = _ -> data + _ -> nil + end + end) + |> Enum.filter(& &1) + end + + def attachments_from_ids_descs([], _), do: [] + def attachments_from_ids_descs(ids, descs_str) do {_, descs} = Jason.decode(descs_str) - Enum.map(ids || [], fn media_id -> - Map.put(Repo.get(Object, media_id).data, "name", descs[media_id]) + Enum.map(ids, fn media_id -> + case Repo.get(Object, media_id) do + %Object{data: data} = _ -> + Map.put(data, "name", descs[media_id]) + + _ -> + nil + end end) + |> Enum.filter(& &1) end @spec get_to_and_cc(User.t(), list(String.t()), Activity.t() | nil, String.t()) :: @@ -247,20 +264,18 @@ def maybe_add_attachments({text, mentions, tags}, attachments, _no_links) do end def add_attachments(text, attachments) do - attachment_text = - Enum.map(attachments, fn - %{"url" => [%{"href" => href} | _]} = attachment -> - name = attachment["name"] || URI.decode(Path.basename(href)) - href = MediaProxy.url(href) - "
#{shortname(name)}" - - _ -> - "" - end) - + attachment_text = Enum.map(attachments, &build_attachment_link/1) Enum.join([text | attachment_text], "
") end + defp build_attachment_link(%{"url" => [%{"href" => href} | _]} = attachment) do + name = attachment["name"] || URI.decode(Path.basename(href)) + href = MediaProxy.url(href) + "#{shortname(name)}" + end + + defp build_attachment_link(_), do: "" + def format_input(text, format, options \\ []) @doc """ @@ -320,7 +335,7 @@ def make_note_data( sensitive \\ false, merge \\ %{} ) do - object = %{ + %{ "type" => "Note", "to" => to, "cc" => cc, @@ -330,18 +345,20 @@ def make_note_data( "context" => context, "attachment" => attachments, "actor" => actor, - "tag" => tags |> Enum.map(fn {_, tag} -> tag end) |> Enum.uniq() + "tag" => Keyword.values(tags) |> Enum.uniq() } + |> add_in_reply_to(in_reply_to) + |> Map.merge(merge) + end - object = - with false <- is_nil(in_reply_to), - %Object{} = in_reply_to_object <- Object.normalize(in_reply_to) do - Map.put(object, "inReplyTo", in_reply_to_object.data["id"]) - else - _ -> object - end + defp add_in_reply_to(object, nil), do: object - Map.merge(object, merge) + defp add_in_reply_to(object, in_reply_to) do + with %Object{} = in_reply_to_object <- Object.normalize(in_reply_to) do + Map.put(object, "inReplyTo", in_reply_to_object.data["id"]) + else + _ -> object + end end def format_naive_asctime(date) do @@ -373,17 +390,16 @@ def to_masto_date(%NaiveDateTime{} = date) do |> String.replace(~r/(\.\d+)?$/, ".000Z", global: false) end - def to_masto_date(date) do - try do - date - |> NaiveDateTime.from_iso8601!() - |> NaiveDateTime.to_iso8601() - |> String.replace(~r/(\.\d+)?$/, ".000Z", global: false) - rescue - _e -> "" + def to_masto_date(date) when is_binary(date) do + with {:ok, date} <- NaiveDateTime.from_iso8601(date) do + to_masto_date(date) + else + _ -> "" end end + def to_masto_date(_), do: "" + defp shortname(name) do if String.length(name) < 30 do name @@ -428,7 +444,7 @@ def maybe_notify_mentioned_recipients( object_data = cond do - !is_nil(object) -> + not is_nil(object) -> object.data is_map(data["object"]) -> @@ -472,9 +488,9 @@ def maybe_notify_subscribers(recipients, _), do: recipients def maybe_extract_mentions(%{"tag" => tag}) do tag - |> Enum.filter(fn x -> is_map(x) end) - |> Enum.filter(fn x -> x["type"] == "Mention" end) + |> Enum.filter(fn x -> is_map(x) && x["type"] == "Mention" end) |> Enum.map(fn x -> x["href"] end) + |> Enum.uniq() end def maybe_extract_mentions(_), do: [] diff --git a/test/web/common_api/common_api_utils_test.exs b/test/web/common_api/common_api_utils_test.exs index 4b5666c29..5989d7d29 100644 --- a/test/web/common_api/common_api_utils_test.exs +++ b/test/web/common_api/common_api_utils_test.exs @@ -306,7 +306,6 @@ test "for private posts, not a reply" do mentions = [mentioned_user.ap_id] {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "private") - assert length(to) == 2 assert length(cc) == 0 @@ -380,4 +379,222 @@ test "get activity by object when type isn't `Create` " do assert like.data["object"] == activity.data["object"] end end + + describe "to_master_date/1" do + test "removes microseconds from date (NaiveDateTime)" do + assert Utils.to_masto_date(~N[2015-01-23 23:50:07.123]) == "2015-01-23T23:50:07.000Z" + end + + test "removes microseconds from date (String)" do + assert Utils.to_masto_date("2015-01-23T23:50:07.123Z") == "2015-01-23T23:50:07.000Z" + end + + test "returns empty string when date invalid" do + assert Utils.to_masto_date("2015-01?23T23:50:07.123Z") == "" + end + end + + describe "conversation_id_to_context/1" do + test "returns id" do + object = insert(:note) + assert Utils.conversation_id_to_context(object.id) == object.data["id"] + end + + test "returns error if object not found" do + assert Utils.conversation_id_to_context("123") == {:error, "No such conversation"} + end + end + + describe "maybe_notify_mentioned_recipients/2" do + test "returns recipients when activity is not `Create`" do + activity = insert(:like_activity) + assert Utils.maybe_notify_mentioned_recipients(["test"], activity) == ["test"] + end + + test "returns recipients from tag" do + user = insert(:user) + + object = + insert(:note, + user: user, + data: %{ + "tag" => [ + %{"type" => "Hashtag"}, + "", + %{"type" => "Mention", "href" => "https://testing.pleroma.lol/users/lain"}, + %{"type" => "Mention", "href" => "https://shitposter.club/user/5381"}, + %{"type" => "Mention", "href" => "https://shitposter.club/user/5381"} + ] + } + ) + + activity = insert(:note_activity, user: user, note: object) + + assert Utils.maybe_notify_mentioned_recipients(["test"], activity) == [ + "test", + "https://testing.pleroma.lol/users/lain", + "https://shitposter.club/user/5381" + ] + end + + test "returns recipients when object is map" do + user = insert(:user) + object = insert(:note, user: user) + + activity = + insert(:note_activity, + user: user, + note: object, + data_attrs: %{ + "object" => %{ + "tag" => [ + %{"type" => "Hashtag"}, + "", + %{"type" => "Mention", "href" => "https://testing.pleroma.lol/users/lain"}, + %{"type" => "Mention", "href" => "https://shitposter.club/user/5381"}, + %{"type" => "Mention", "href" => "https://shitposter.club/user/5381"} + ] + } + } + ) + + Pleroma.Repo.delete(object) + + assert Utils.maybe_notify_mentioned_recipients(["test"], activity) == [ + "test", + "https://testing.pleroma.lol/users/lain", + "https://shitposter.club/user/5381" + ] + end + + test "returns recipients when object not found" do + user = insert(:user) + object = insert(:note, user: user) + + activity = insert(:note_activity, user: user, note: object) + Pleroma.Repo.delete(object) + + assert Utils.maybe_notify_mentioned_recipients(["test-test"], activity) == [ + "test-test" + ] + end + end + + describe "attachments_from_ids_descs/2" do + test "returns [] when attachment ids is empty" do + assert Utils.attachments_from_ids_descs([], "{}") == [] + end + + test "returns list attachments with desc" do + object = insert(:note) + desc = Jason.encode!(%{object.id => "test-desc"}) + + assert Utils.attachments_from_ids_descs(["#{object.id}", "34"], desc) == [ + Map.merge(object.data, %{"name" => "test-desc"}) + ] + end + end + + describe "attachments_from_ids/1" do + test "returns attachments with descs" do + object = insert(:note) + desc = Jason.encode!(%{object.id => "test-desc"}) + + assert Utils.attachments_from_ids(%{ + "media_ids" => ["#{object.id}"], + "descriptions" => desc + }) == [ + Map.merge(object.data, %{"name" => "test-desc"}) + ] + end + + test "returns attachments without descs" do + object = insert(:note) + assert Utils.attachments_from_ids(%{"media_ids" => ["#{object.id}"]}) == [object.data] + end + + test "returns [] when not pass media_ids" do + assert Utils.attachments_from_ids(%{}) == [] + end + end + + describe "maybe_add_list_data/3" do + test "adds list params when found user list" do + user = insert(:user) + {:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", user) + + assert Utils.maybe_add_list_data(%{additional: %{}, object: %{}}, user, {:list, list.id}) == + %{ + additional: %{"bcc" => [list.ap_id], "listMessage" => list.ap_id}, + object: %{"listMessage" => list.ap_id} + } + end + + test "returns original params when list not found" do + user = insert(:user) + {:ok, %Pleroma.List{} = list} = Pleroma.List.create("title", insert(:user)) + + assert Utils.maybe_add_list_data(%{additional: %{}, object: %{}}, user, {:list, list.id}) == + %{additional: %{}, object: %{}} + end + end + + describe "make_note_data/11" do + test "returns note data" do + user = insert(:user) + note = insert(:note) + user2 = insert(:user) + user3 = insert(:user) + + assert Utils.make_note_data( + user.ap_id, + [user2.ap_id], + "2hu", + "

This is :moominmamma: note

", + [], + note.id, + [name: "jimm"], + "test summary", + [user3.ap_id], + false, + %{"custom_tag" => "test"} + ) == %{ + "actor" => user.ap_id, + "attachment" => [], + "cc" => [user3.ap_id], + "content" => "

This is :moominmamma: note

", + "context" => "2hu", + "sensitive" => false, + "summary" => "test summary", + "tag" => ["jimm"], + "to" => [user2.ap_id], + "type" => "Note", + "custom_tag" => "test" + } + end + end + + describe "maybe_add_attachments/3" do + test "returns parsed results when no_links is true" do + assert Utils.maybe_add_attachments( + {"test", [], ["tags"]}, + [], + true + ) == {"test", [], ["tags"]} + end + + test "adds attachments to parsed results" do + attachment = %{"url" => [%{"href" => "SakuraPM.png"}]} + + assert Utils.maybe_add_attachments( + {"test", [], ["tags"]}, + [attachment], + false + ) == { + "test
SakuraPM.png", + [], + ["tags"] + } + end + end end From 139b196bc0328d812adb9434b2da97265d57257d Mon Sep 17 00:00:00 2001 From: Maksim Date: Tue, 6 Aug 2019 20:19:28 +0000 Subject: [PATCH 40/48] [#1150] fixed parser TwitterCard --- .../web/rich_media/parsers/twitter_card.ex | 21 +- ...facial-recognition-children-teenagers.html | 227 ++++++++++++++++++ ...acial-recognition-children-teenagers2.html | 226 +++++++++++++++++ ...acial-recognition-children-teenagers3.html | 227 ++++++++++++++++++ .../rich_media/parsers/twitter_card_test.exs | 69 ++++++ 5 files changed, 763 insertions(+), 7 deletions(-) create mode 100644 test/fixtures/nypd-facial-recognition-children-teenagers.html create mode 100644 test/fixtures/nypd-facial-recognition-children-teenagers2.html create mode 100644 test/fixtures/nypd-facial-recognition-children-teenagers3.html create mode 100644 test/web/rich_media/parsers/twitter_card_test.exs diff --git a/lib/pleroma/web/rich_media/parsers/twitter_card.ex b/lib/pleroma/web/rich_media/parsers/twitter_card.ex index e4efe2dd0..afaa98f3d 100644 --- a/lib/pleroma/web/rich_media/parsers/twitter_card.ex +++ b/lib/pleroma/web/rich_media/parsers/twitter_card.ex @@ -3,13 +3,20 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.RichMedia.Parsers.TwitterCard do + alias Pleroma.Web.RichMedia.Parsers.MetaTagsParser + + @spec parse(String.t(), map()) :: {:ok, map()} | {:error, String.t()} def parse(html, data) do - Pleroma.Web.RichMedia.Parsers.MetaTagsParser.parse( - html, - data, - "twitter", - "No twitter card metadata found", - "name" - ) + data + |> parse_name_attrs(html) + |> parse_property_attrs(html) + end + + defp parse_name_attrs(data, html) do + MetaTagsParser.parse(html, data, "twitter", %{}, "name") + end + + defp parse_property_attrs({_, data}, html) do + MetaTagsParser.parse(html, data, "twitter", "No twitter card metadata found", "property") end end diff --git a/test/fixtures/nypd-facial-recognition-children-teenagers.html b/test/fixtures/nypd-facial-recognition-children-teenagers.html new file mode 100644 index 000000000..5702c4484 --- /dev/null +++ b/test/fixtures/nypd-facial-recognition-children-teenagers.html @@ -0,0 +1,227 @@ + + + + She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database. - The New York Times + + + + + + + + + + + + + + + + + + + + + +

Advertisement

She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.

With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.

Image
CreditCreditSarah Blesener for The New York Times

[What you need to know to start the day: Get New York Today in your inbox.]

The New York Police Department has been loading thousands of arrest photos of children and teenagers into a facial recognition database despite evidence the technology has a higher risk of false matches in younger faces.

For about four years, internal records show, the department has used the technology to compare crime scene images with its collection of juvenile mug shots, the photos that are taken at an arrest. Most of the photos are of teenagers, largely 13 to 16 years old, but children as young as 11 have been included.

Elected officials and civil rights groups said the disclosure that the city was deploying a powerful surveillance tool on adolescents — whose privacy seems sacrosanct and whose status is protected in the criminal justice system — was a striking example of the Police Department’s ability to adopt advancing technology with little public scrutiny.

Several members of the City Council as well as a range of civil liberties groups said they were unaware of the policy until they were contacted by The New York Times.

Police Department officials defended the decision, saying it was just the latest evolution of a longstanding policing technique: using arrest photos to identify suspects.

“I don’t think this is any secret decision that’s made behind closed doors,” the city’s chief of detectives, Dermot F. Shea, said in an interview. “This is just process, and making sure we’re doing everything to fight crime.”

Other cities have begun to debate whether law enforcement should use facial recognition, which relies on an algorithm to quickly pore through images and suggest matches. In May, San Francisco blocked city agencies, including the police, from using the tool amid unease about potential government abuse. Detroit is facing public resistance to a technology that has been shown to have lower accuracy with people with darker skin.

In New York, the state Education Department recently told the Lockport, N.Y., school district to delay a plan to use facial recognition on students, citing privacy concerns.

“At the end of the day, it should be banned — no young people,” said Councilman Donovan Richards, a Queens Democrat who heads the Public Safety Committee, which oversees the Police Department.

The department said its legal bureau had approved using facial recognition on juveniles. The algorithm may suggest a lead, but detectives would not make an arrest based solely on that, Chief Shea said.

Image
CreditChang W. Lee/The New York Times

Still, facial recognition has not been widely tested on children. Most algorithms are taught to “think” based on adult faces, and there is growing evidence that they do not work as well on children.

The National Institute of Standards and Technology, which is part of the Commerce Department and evaluates facial recognition algorithms for accuracy, recently found the vast majority of more than 100 facial recognition algorithms had a higher rate of mistaken matches among children. The error rate was most pronounced in young children but was also seen in those aged 10 to 16.

Aging poses another problem: The appearance of children and adolescents can change drastically as bones stretch and shift, altering the underlying facial structure.

“I would use extreme caution in using those algorithms,” said Karl Ricanek Jr., a computer science professor and co-founder of the Face Aging Group at the University of North Carolina-Wilmington.

Technology that can match an image of a younger teenager to a recent arrest photo may be less effective at finding the same person even one or two years later, he said.

“The systems do not have the capacity to understand the dynamic changes that occur to a child’s face,” Dr. Ricanek said.

Idemia and DataWorks Plus, the two companies that provide facial recognition software to the Police Department, did not respond to requests for comment.

The New York Police Department can take arrest photos of minors as young as 11 who are charged with a felony, depending on the severity of the charge.

And in many cases, the department keeps the photos for years, making facial recognition comparisons to what may have effectively become outdated images. There are photos of 5,500 individuals in the juvenile database, 4,100 of whom are no longer 16 or under, the department said. Teenagers 17 and older are considered adults in the criminal justice system.

Police officials declined to provide statistics on how often their facial recognition systems provide false matches, or to explain how they evaluate the system’s effectiveness.

“We are comfortable with this technology because it has proved to be a valuable investigative method,” Chief Shea said. Facial recognition has helped lead to thousands of arrests of both adults and juveniles, the department has said.

Mayor Bill de Blasio had been aware the department was using the technology on minors, said Freddi Goldstein, a spokeswoman for the mayor.

She said the Police Department followed “strict guidelines” in applying the technology and City Hall monitored the agency’s compliance with the policies.

The Times learned details of the department’s use of facial recognition by reviewing documents posted online earlier this year by Clare Garvie, a senior associate at the Center on Privacy and Technology at Georgetown Law. Ms. Garvie received the documents as part of an open records lawsuit.

It could not be determined whether other large police departments used facial recognition with juveniles because very few have written policies governing the use of the technology, Ms. Garvie said.

New York detectives rely on a vast network of surveillance cameras throughout the city to provide images of people believed to have committed a crime. Since 2011, the department has had a dedicated unit of officers who use facial recognition to compare those images against millions of photos, usually mug shots. The software proposes matches, which have led to thousands of arrests, the department said.

By 2013, top police officials were meeting to discuss including juveniles in the program, the documents reviewed by The Times showed.

The documents showed that the juvenile database had been integrated into the system by 2015.

“We have these photos. It makes sense,” Chief Shea said in the interview.

State law requires that arrest photos be destroyed if the case is resolved in the juvenile’s favor, or if the child is found to have committed only a misdemeanor, rather than a felony. The photos also must be destroyed if a person reaches age 21 without a criminal record.

When children are charged with crimes, the court system usually takes some steps to prevent their acts from defining them in later years. Children who are 16 and under, for instance, are generally sent to Family Court, where records are not public.

Yet including their photos in a facial recognition database runs the risk that an imperfect algorithm identifies them as possible suspects in later crimes, civil rights advocates said. A mistaken match could lead investigators to focus on the wrong person from the outset, they said.

“It’s very disturbing to know that no matter what I’m doing at that moment, someone might be scanning my picture to try to find someone who committed a crime,” said Bailey, a 17-year-old Brooklyn girl who had admitted guilt in Family Court to a group attack that happened when she was 14. She said she was present at the attack but did not participate.

Bailey, who asked that she be identified only by her last name because she did not want her juvenile arrest to be public, has not been arrested again and is now a student at John Jay College of Criminal Justice.

Recent studies indicate that people of color, as well as children and women, have a greater risk of misidentification than their counterparts, said Joy Buolamwini, the founder of the Algorithmic Justice League and graduate researcher at the M.I.T. Media Lab, who has examined how human biases are built into artificial intelligence.

The racial disparities in the juvenile justice system are stark: In New York, black and Latino juveniles were charged with crimes at far higher rates than whites in 2017, the most recent year for which numbers were available. Black juveniles outnumbered white juveniles more than 15 to 1.

“If the facial recognition algorithm has a negative bias toward a black population, that will get magnified more toward children,” Dr. Ricanek said, adding that in terms of diminished accuracy, “you’re now putting yourself in unknown territory.”

Joseph Goldstein writes about policing and the criminal justice system. He has been a reporter at The Times since 2011, and is based in New York. He also worked for a year in the Kabul bureau, reporting on Afghanistan. @JoeKGoldstein

Ali Watkins is a reporter on the Metro Desk, covering courts and social services. Previously, she covered national security in Washington for The Times, BuzzFeed and McClatchy Newspapers. @AliWatkins

A version of this article appears in print on , Section A, Page 1 of the New York edition with the headline: In New York, Police Computers Scan Faces, Some as Young as 11. Order Reprints | Today’s Paper | Subscribe

Advertisement

+ + + + + + + + + + +
+ +
+ + + + \ No newline at end of file diff --git a/test/fixtures/nypd-facial-recognition-children-teenagers2.html b/test/fixtures/nypd-facial-recognition-children-teenagers2.html new file mode 100644 index 000000000..ae8b26aff --- /dev/null +++ b/test/fixtures/nypd-facial-recognition-children-teenagers2.html @@ -0,0 +1,226 @@ + + + + She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database. - The New York Times + + + + + + + + + + + + + + + + + + + + +

Advertisement

She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.

With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.

Image
CreditCreditSarah Blesener for The New York Times

[What you need to know to start the day: Get New York Today in your inbox.]

The New York Police Department has been loading thousands of arrest photos of children and teenagers into a facial recognition database despite evidence the technology has a higher risk of false matches in younger faces.

For about four years, internal records show, the department has used the technology to compare crime scene images with its collection of juvenile mug shots, the photos that are taken at an arrest. Most of the photos are of teenagers, largely 13 to 16 years old, but children as young as 11 have been included.

Elected officials and civil rights groups said the disclosure that the city was deploying a powerful surveillance tool on adolescents — whose privacy seems sacrosanct and whose status is protected in the criminal justice system — was a striking example of the Police Department’s ability to adopt advancing technology with little public scrutiny.

Several members of the City Council as well as a range of civil liberties groups said they were unaware of the policy until they were contacted by The New York Times.

Police Department officials defended the decision, saying it was just the latest evolution of a longstanding policing technique: using arrest photos to identify suspects.

“I don’t think this is any secret decision that’s made behind closed doors,” the city’s chief of detectives, Dermot F. Shea, said in an interview. “This is just process, and making sure we’re doing everything to fight crime.”

Other cities have begun to debate whether law enforcement should use facial recognition, which relies on an algorithm to quickly pore through images and suggest matches. In May, San Francisco blocked city agencies, including the police, from using the tool amid unease about potential government abuse. Detroit is facing public resistance to a technology that has been shown to have lower accuracy with people with darker skin.

In New York, the state Education Department recently told the Lockport, N.Y., school district to delay a plan to use facial recognition on students, citing privacy concerns.

“At the end of the day, it should be banned — no young people,” said Councilman Donovan Richards, a Queens Democrat who heads the Public Safety Committee, which oversees the Police Department.

The department said its legal bureau had approved using facial recognition on juveniles. The algorithm may suggest a lead, but detectives would not make an arrest based solely on that, Chief Shea said.

Image
CreditChang W. Lee/The New York Times

Still, facial recognition has not been widely tested on children. Most algorithms are taught to “think” based on adult faces, and there is growing evidence that they do not work as well on children.

The National Institute of Standards and Technology, which is part of the Commerce Department and evaluates facial recognition algorithms for accuracy, recently found the vast majority of more than 100 facial recognition algorithms had a higher rate of mistaken matches among children. The error rate was most pronounced in young children but was also seen in those aged 10 to 16.

Aging poses another problem: The appearance of children and adolescents can change drastically as bones stretch and shift, altering the underlying facial structure.

“I would use extreme caution in using those algorithms,” said Karl Ricanek Jr., a computer science professor and co-founder of the Face Aging Group at the University of North Carolina-Wilmington.

Technology that can match an image of a younger teenager to a recent arrest photo may be less effective at finding the same person even one or two years later, he said.

“The systems do not have the capacity to understand the dynamic changes that occur to a child’s face,” Dr. Ricanek said.

Idemia and DataWorks Plus, the two companies that provide facial recognition software to the Police Department, did not respond to requests for comment.

The New York Police Department can take arrest photos of minors as young as 11 who are charged with a felony, depending on the severity of the charge.

And in many cases, the department keeps the photos for years, making facial recognition comparisons to what may have effectively become outdated images. There are photos of 5,500 individuals in the juvenile database, 4,100 of whom are no longer 16 or under, the department said. Teenagers 17 and older are considered adults in the criminal justice system.

Police officials declined to provide statistics on how often their facial recognition systems provide false matches, or to explain how they evaluate the system’s effectiveness.

“We are comfortable with this technology because it has proved to be a valuable investigative method,” Chief Shea said. Facial recognition has helped lead to thousands of arrests of both adults and juveniles, the department has said.

Mayor Bill de Blasio had been aware the department was using the technology on minors, said Freddi Goldstein, a spokeswoman for the mayor.

She said the Police Department followed “strict guidelines” in applying the technology and City Hall monitored the agency’s compliance with the policies.

The Times learned details of the department’s use of facial recognition by reviewing documents posted online earlier this year by Clare Garvie, a senior associate at the Center on Privacy and Technology at Georgetown Law. Ms. Garvie received the documents as part of an open records lawsuit.

It could not be determined whether other large police departments used facial recognition with juveniles because very few have written policies governing the use of the technology, Ms. Garvie said.

New York detectives rely on a vast network of surveillance cameras throughout the city to provide images of people believed to have committed a crime. Since 2011, the department has had a dedicated unit of officers who use facial recognition to compare those images against millions of photos, usually mug shots. The software proposes matches, which have led to thousands of arrests, the department said.

By 2013, top police officials were meeting to discuss including juveniles in the program, the documents reviewed by The Times showed.

The documents showed that the juvenile database had been integrated into the system by 2015.

“We have these photos. It makes sense,” Chief Shea said in the interview.

State law requires that arrest photos be destroyed if the case is resolved in the juvenile’s favor, or if the child is found to have committed only a misdemeanor, rather than a felony. The photos also must be destroyed if a person reaches age 21 without a criminal record.

When children are charged with crimes, the court system usually takes some steps to prevent their acts from defining them in later years. Children who are 16 and under, for instance, are generally sent to Family Court, where records are not public.

Yet including their photos in a facial recognition database runs the risk that an imperfect algorithm identifies them as possible suspects in later crimes, civil rights advocates said. A mistaken match could lead investigators to focus on the wrong person from the outset, they said.

“It’s very disturbing to know that no matter what I’m doing at that moment, someone might be scanning my picture to try to find someone who committed a crime,” said Bailey, a 17-year-old Brooklyn girl who had admitted guilt in Family Court to a group attack that happened when she was 14. She said she was present at the attack but did not participate.

Bailey, who asked that she be identified only by her last name because she did not want her juvenile arrest to be public, has not been arrested again and is now a student at John Jay College of Criminal Justice.

Recent studies indicate that people of color, as well as children and women, have a greater risk of misidentification than their counterparts, said Joy Buolamwini, the founder of the Algorithmic Justice League and graduate researcher at the M.I.T. Media Lab, who has examined how human biases are built into artificial intelligence.

The racial disparities in the juvenile justice system are stark: In New York, black and Latino juveniles were charged with crimes at far higher rates than whites in 2017, the most recent year for which numbers were available. Black juveniles outnumbered white juveniles more than 15 to 1.

“If the facial recognition algorithm has a negative bias toward a black population, that will get magnified more toward children,” Dr. Ricanek said, adding that in terms of diminished accuracy, “you’re now putting yourself in unknown territory.”

Joseph Goldstein writes about policing and the criminal justice system. He has been a reporter at The Times since 2011, and is based in New York. He also worked for a year in the Kabul bureau, reporting on Afghanistan. @JoeKGoldstein

Ali Watkins is a reporter on the Metro Desk, covering courts and social services. Previously, she covered national security in Washington for The Times, BuzzFeed and McClatchy Newspapers. @AliWatkins

A version of this article appears in print on , Section A, Page 1 of the New York edition with the headline: In New York, Police Computers Scan Faces, Some as Young as 11. Order Reprints | Today’s Paper | Subscribe

Advertisement

+ + + + + + + + + + +
+ +
+ + + + diff --git a/test/fixtures/nypd-facial-recognition-children-teenagers3.html b/test/fixtures/nypd-facial-recognition-children-teenagers3.html new file mode 100644 index 000000000..53454d23e --- /dev/null +++ b/test/fixtures/nypd-facial-recognition-children-teenagers3.html @@ -0,0 +1,227 @@ + + + + She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database. - The New York Times + + + + + + + + + + + + + + + + + + + + + +

Advertisement

She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.

With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.

Image
CreditCreditSarah Blesener for The New York Times

[What you need to know to start the day: Get New York Today in your inbox.]

The New York Police Department has been loading thousands of arrest photos of children and teenagers into a facial recognition database despite evidence the technology has a higher risk of false matches in younger faces.

For about four years, internal records show, the department has used the technology to compare crime scene images with its collection of juvenile mug shots, the photos that are taken at an arrest. Most of the photos are of teenagers, largely 13 to 16 years old, but children as young as 11 have been included.

Elected officials and civil rights groups said the disclosure that the city was deploying a powerful surveillance tool on adolescents — whose privacy seems sacrosanct and whose status is protected in the criminal justice system — was a striking example of the Police Department’s ability to adopt advancing technology with little public scrutiny.

Several members of the City Council as well as a range of civil liberties groups said they were unaware of the policy until they were contacted by The New York Times.

Police Department officials defended the decision, saying it was just the latest evolution of a longstanding policing technique: using arrest photos to identify suspects.

“I don’t think this is any secret decision that’s made behind closed doors,” the city’s chief of detectives, Dermot F. Shea, said in an interview. “This is just process, and making sure we’re doing everything to fight crime.”

Other cities have begun to debate whether law enforcement should use facial recognition, which relies on an algorithm to quickly pore through images and suggest matches. In May, San Francisco blocked city agencies, including the police, from using the tool amid unease about potential government abuse. Detroit is facing public resistance to a technology that has been shown to have lower accuracy with people with darker skin.

In New York, the state Education Department recently told the Lockport, N.Y., school district to delay a plan to use facial recognition on students, citing privacy concerns.

“At the end of the day, it should be banned — no young people,” said Councilman Donovan Richards, a Queens Democrat who heads the Public Safety Committee, which oversees the Police Department.

The department said its legal bureau had approved using facial recognition on juveniles. The algorithm may suggest a lead, but detectives would not make an arrest based solely on that, Chief Shea said.

Image
CreditChang W. Lee/The New York Times

Still, facial recognition has not been widely tested on children. Most algorithms are taught to “think” based on adult faces, and there is growing evidence that they do not work as well on children.

The National Institute of Standards and Technology, which is part of the Commerce Department and evaluates facial recognition algorithms for accuracy, recently found the vast majority of more than 100 facial recognition algorithms had a higher rate of mistaken matches among children. The error rate was most pronounced in young children but was also seen in those aged 10 to 16.

Aging poses another problem: The appearance of children and adolescents can change drastically as bones stretch and shift, altering the underlying facial structure.

“I would use extreme caution in using those algorithms,” said Karl Ricanek Jr., a computer science professor and co-founder of the Face Aging Group at the University of North Carolina-Wilmington.

Technology that can match an image of a younger teenager to a recent arrest photo may be less effective at finding the same person even one or two years later, he said.

“The systems do not have the capacity to understand the dynamic changes that occur to a child’s face,” Dr. Ricanek said.

Idemia and DataWorks Plus, the two companies that provide facial recognition software to the Police Department, did not respond to requests for comment.

The New York Police Department can take arrest photos of minors as young as 11 who are charged with a felony, depending on the severity of the charge.

And in many cases, the department keeps the photos for years, making facial recognition comparisons to what may have effectively become outdated images. There are photos of 5,500 individuals in the juvenile database, 4,100 of whom are no longer 16 or under, the department said. Teenagers 17 and older are considered adults in the criminal justice system.

Police officials declined to provide statistics on how often their facial recognition systems provide false matches, or to explain how they evaluate the system’s effectiveness.

“We are comfortable with this technology because it has proved to be a valuable investigative method,” Chief Shea said. Facial recognition has helped lead to thousands of arrests of both adults and juveniles, the department has said.

Mayor Bill de Blasio had been aware the department was using the technology on minors, said Freddi Goldstein, a spokeswoman for the mayor.

She said the Police Department followed “strict guidelines” in applying the technology and City Hall monitored the agency’s compliance with the policies.

The Times learned details of the department’s use of facial recognition by reviewing documents posted online earlier this year by Clare Garvie, a senior associate at the Center on Privacy and Technology at Georgetown Law. Ms. Garvie received the documents as part of an open records lawsuit.

It could not be determined whether other large police departments used facial recognition with juveniles because very few have written policies governing the use of the technology, Ms. Garvie said.

New York detectives rely on a vast network of surveillance cameras throughout the city to provide images of people believed to have committed a crime. Since 2011, the department has had a dedicated unit of officers who use facial recognition to compare those images against millions of photos, usually mug shots. The software proposes matches, which have led to thousands of arrests, the department said.

By 2013, top police officials were meeting to discuss including juveniles in the program, the documents reviewed by The Times showed.

The documents showed that the juvenile database had been integrated into the system by 2015.

“We have these photos. It makes sense,” Chief Shea said in the interview.

State law requires that arrest photos be destroyed if the case is resolved in the juvenile’s favor, or if the child is found to have committed only a misdemeanor, rather than a felony. The photos also must be destroyed if a person reaches age 21 without a criminal record.

When children are charged with crimes, the court system usually takes some steps to prevent their acts from defining them in later years. Children who are 16 and under, for instance, are generally sent to Family Court, where records are not public.

Yet including their photos in a facial recognition database runs the risk that an imperfect algorithm identifies them as possible suspects in later crimes, civil rights advocates said. A mistaken match could lead investigators to focus on the wrong person from the outset, they said.

“It’s very disturbing to know that no matter what I’m doing at that moment, someone might be scanning my picture to try to find someone who committed a crime,” said Bailey, a 17-year-old Brooklyn girl who had admitted guilt in Family Court to a group attack that happened when she was 14. She said she was present at the attack but did not participate.

Bailey, who asked that she be identified only by her last name because she did not want her juvenile arrest to be public, has not been arrested again and is now a student at John Jay College of Criminal Justice.

Recent studies indicate that people of color, as well as children and women, have a greater risk of misidentification than their counterparts, said Joy Buolamwini, the founder of the Algorithmic Justice League and graduate researcher at the M.I.T. Media Lab, who has examined how human biases are built into artificial intelligence.

The racial disparities in the juvenile justice system are stark: In New York, black and Latino juveniles were charged with crimes at far higher rates than whites in 2017, the most recent year for which numbers were available. Black juveniles outnumbered white juveniles more than 15 to 1.

“If the facial recognition algorithm has a negative bias toward a black population, that will get magnified more toward children,” Dr. Ricanek said, adding that in terms of diminished accuracy, “you’re now putting yourself in unknown territory.”

Joseph Goldstein writes about policing and the criminal justice system. He has been a reporter at The Times since 2011, and is based in New York. He also worked for a year in the Kabul bureau, reporting on Afghanistan. @JoeKGoldstein

Ali Watkins is a reporter on the Metro Desk, covering courts and social services. Previously, she covered national security in Washington for The Times, BuzzFeed and McClatchy Newspapers. @AliWatkins

A version of this article appears in print on , Section A, Page 1 of the New York edition with the headline: In New York, Police Computers Scan Faces, Some as Young as 11. Order Reprints | Today’s Paper | Subscribe

Advertisement

+ + + + + + + + + + +
+ +
+ + + + diff --git a/test/web/rich_media/parsers/twitter_card_test.exs b/test/web/rich_media/parsers/twitter_card_test.exs new file mode 100644 index 000000000..f8e1c9b40 --- /dev/null +++ b/test/web/rich_media/parsers/twitter_card_test.exs @@ -0,0 +1,69 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.RichMedia.Parsers.TwitterCardTest do + use ExUnit.Case, async: true + alias Pleroma.Web.RichMedia.Parsers.TwitterCard + + test "returns error when html not contains twitter card" do + assert TwitterCard.parse("", %{}) == {:error, "No twitter card metadata found"} + end + + test "parses twitter card with only name attributes" do + html = File.read!("test/fixtures/nypd-facial-recognition-children-teenagers3.html") + + assert TwitterCard.parse(html, %{}) == + {:ok, + %{ + "app:id:googleplay": "com.nytimes.android", + "app:name:googleplay": "NYTimes", + "app:url:googleplay": "nytimes://reader/id/100000006583622", + site: nil, + title: + "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database. - The New York Times" + }} + end + + test "parses twitter card with only property attributes" do + html = File.read!("test/fixtures/nypd-facial-recognition-children-teenagers2.html") + + assert TwitterCard.parse(html, %{}) == + {:ok, + %{ + card: "summary_large_image", + description: + "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", + image: + "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg", + "image:alt": "", + title: + "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.", + url: + "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html" + }} + end + + test "parses twitter card with name & property attributes" do + html = File.read!("test/fixtures/nypd-facial-recognition-children-teenagers.html") + + assert TwitterCard.parse(html, %{}) == + {:ok, + %{ + "app:id:googleplay": "com.nytimes.android", + "app:name:googleplay": "NYTimes", + "app:url:googleplay": "nytimes://reader/id/100000006583622", + card: "summary_large_image", + description: + "With little oversight, the N.Y.P.D. has been using powerful surveillance technology on photos of children and teenagers.", + image: + "https://static01.nyt.com/images/2019/08/01/nyregion/01nypd-juveniles-promo/01nypd-juveniles-promo-videoSixteenByNineJumbo1600.jpg", + "image:alt": "", + site: nil, + title: + "She Was Arrested at 14. Then Her Photo Went to a Facial Recognition Database.", + url: + "https://www.nytimes.com/2019/08/01/nyregion/nypd-facial-recognition-children-teenagers.html" + }} + end +end From 73d8d5c49f66d77ea77540223aaa8f94d91f63f8 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 7 Aug 2019 00:12:42 +0300 Subject: [PATCH 41/48] Stop depending on the embedded object in restrict_favorited_by --- lib/pleroma/web/activity_pub/activity_pub.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 2877c029e..1a279a7df 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -749,8 +749,8 @@ defp restrict_state(query, _), do: query defp restrict_favorited_by(query, %{"favorited_by" => ap_id}) do from( - activity in query, - where: fragment(~s(? <@ (? #> '{"object","likes"}'\)), ^ap_id, activity.data) + [_activity, object] in query, + where: fragment("(?)->'likes' \\? (?)", object.data, ^ap_id) ) end From 03ad31328c264a1154b7d3b5697b429452a1e6b0 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 7 Aug 2019 00:23:58 +0300 Subject: [PATCH 42/48] OStatus Announce Representer: Do not depend on the object being embedded in the Create activity --- lib/pleroma/web/ostatus/activity_representer.ex | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/web/ostatus/activity_representer.ex b/lib/pleroma/web/ostatus/activity_representer.ex index 760345301..8e55b9f0b 100644 --- a/lib/pleroma/web/ostatus/activity_representer.ex +++ b/lib/pleroma/web/ostatus/activity_representer.ex @@ -183,6 +183,7 @@ def to_simple_form(%{data: %{"type" => "Announce"}} = activity, user, with_autho author = if with_author, do: [{:author, UserRepresenter.to_simple_form(user)}], else: [] retweeted_activity = Activity.get_create_by_object_ap_id(activity.data["object"]) + retweeted_object = Object.normalize(retweeted_activity) retweeted_user = User.get_cached_by_ap_id(retweeted_activity.data["actor"]) retweeted_xml = to_simple_form(retweeted_activity, retweeted_user, true) @@ -197,7 +198,7 @@ def to_simple_form(%{data: %{"type" => "Announce"}} = activity, user, with_autho {:"activity:verb", ['http://activitystrea.ms/schema/1.0/share']}, {:id, h.(activity.data["id"])}, {:title, ['#{user.nickname} repeated a notice']}, - {:content, [type: 'html'], ['RT #{retweeted_activity.data["object"]["content"]}']}, + {:content, [type: 'html'], ['RT #{retweeted_object.data["content"]}']}, {:published, h.(inserted_at)}, {:updated, h.(updated_at)}, {:"ostatus:conversation", [ref: h.(activity.data["context"])], From 32018a4ee0fab43fc0982e6428d3fb93e5ac3c47 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 7 Aug 2019 00:36:13 +0300 Subject: [PATCH 43/48] ActivityPub tests: remove assertions of embedded object being updated, because the objects are no longer supposed to be embedded --- test/web/activity_pub/activity_pub_test.exs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 3d9a678dd..d723f331f 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -677,14 +677,8 @@ test "adds a like activity to the db" do assert object.data["likes"] == [user.ap_id] assert object.data["like_count"] == 1 - [note_activity] = Activity.get_all_create_by_object_ap_id(object.data["id"]) - assert note_activity.data["object"]["like_count"] == 1 - {:ok, _like_activity, object} = ActivityPub.like(user_two, object) assert object.data["like_count"] == 2 - - [note_activity] = Activity.get_all_create_by_object_ap_id(object.data["id"]) - assert note_activity.data["object"]["like_count"] == 2 end end From 5329e84d62bbdf87a4b66e2ba9302a840802416f Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 7 Aug 2019 00:58:48 +0300 Subject: [PATCH 44/48] OStatus tests: stop relying on embedded objects --- test/web/ostatus/ostatus_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/web/ostatus/ostatus_test.exs b/test/web/ostatus/ostatus_test.exs index f8d389020..803a97695 100644 --- a/test/web/ostatus/ostatus_test.exs +++ b/test/web/ostatus/ostatus_test.exs @@ -199,7 +199,7 @@ test "handle incoming retweets - GS, subscription - local message" do assert retweeted_activity.data["type"] == "Create" assert retweeted_activity.data["actor"] == user.ap_id assert retweeted_activity.local - assert retweeted_activity.data["object"]["announcement_count"] == 1 + assert Object.normalize(retweeted_activity).data["announcement_count"] == 1 end test "handle incoming retweets - Mastodon, salmon" do From 4f1b9c54b9317863083bfb767b8e12c6b14cc14c Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 7 Aug 2019 01:02:29 +0300 Subject: [PATCH 45/48] Do not rembed the object after updating it --- CHANGELOG.md | 1 + lib/pleroma/web/activity_pub/utils.ex | 17 +---------------- 2 files changed, 2 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2713461ed..069974e44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Fixed - 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. - Metadata rendering errors resulting in the entire page being inaccessible - Federation/MediaProxy not working with instances that have wrong certificate order - Mastodon API: Handling of search timeouts (`/api/v1/search` and `/api/v2/search`) diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index 39074888b..fc5305c58 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -251,20 +251,6 @@ def insert_full_object(%{"object" => %{"type" => type} = object_data} = map) def insert_full_object(map), do: {:ok, map, nil} - def update_object_in_activities(%{data: %{"id" => id}} = object) do - # TODO - # Update activities that already had this. Could be done in a seperate process. - # Alternatively, just don't do this and fetch the current object each time. Most - # could probably be taken from cache. - relevant_activities = Activity.get_all_create_by_object_ap_id(id) - - Enum.map(relevant_activities, fn activity -> - new_activity_data = activity.data |> Map.put("object", object.data) - changeset = Changeset.change(activity, data: new_activity_data) - Repo.update(changeset) - end) - end - #### Like-related helpers @doc """ @@ -347,8 +333,7 @@ def update_element_in_object(property, element, object) do |> Map.put("#{property}_count", length(element)) |> Map.put("#{property}s", element), changeset <- Changeset.change(object, data: new_data), - {:ok, object} <- Object.update_and_set_cache(changeset), - _ <- update_object_in_activities(object) do + {:ok, object} <- Object.update_and_set_cache(changeset) do {:ok, object} end end From a10c840abaeb8402051665412fbfd50e4a5ea054 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Wed, 7 Aug 2019 20:29:30 +0000 Subject: [PATCH 46/48] Return profile URL when available instead of actor URI for MastodonAPI mention URL Fixes #1165 --- lib/pleroma/web/mastodon_api/views/account_view.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index b2b06eeb9..3212dcbc3 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -28,7 +28,7 @@ def render("mention.json", %{user: user}) do id: to_string(user.id), acct: user.nickname, username: username_from_nickname(user.nickname), - url: user.ap_id + url: User.profile_url(user) || user.ap_id } end From 089d53a961f14681cf91c923eeb67478ec230da9 Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Wed, 7 Aug 2019 20:55:37 +0000 Subject: [PATCH 47/48] Simplify logic to mention.js `url` field `User.profile_url` already fallbacks to ap_id --- lib/pleroma/web/mastodon_api/views/account_view.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 3212dcbc3..82f8cd020 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -28,7 +28,7 @@ def render("mention.json", %{user: user}) do id: to_string(user.id), acct: user.nickname, username: username_from_nickname(user.nickname), - url: User.profile_url(user) || user.ap_id + url: User.profile_url(user) } end From 9c0da1009aa2100a206fae13f88ea9faddcd6bbd Mon Sep 17 00:00:00 2001 From: Thibaut Girka Date: Wed, 7 Aug 2019 21:40:53 +0000 Subject: [PATCH 48/48] Return profile URL in MastodonAPI's `url` field --- lib/pleroma/web/mastodon_api/views/account_view.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index 82f8cd020..de084fd6e 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -106,7 +106,7 @@ defp do_render("account.json", %{user: user} = opts) do following_count: user_info.following_count, statuses_count: user_info.note_count, note: bio || "", - url: user.ap_id, + url: User.profile_url(user), avatar: image, avatar_static: image, header: header,