From 371a4aed2ca9f6926e49f6791c8b4d14292d20e5 Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Sat, 13 Apr 2019 17:40:42 +0700 Subject: [PATCH 01/66] Add User.Info.email_notifications --- lib/pleroma/user/info.ex | 27 +++++++++++++++++++ .../20190412052952_add_user_info_fields.exs | 20 ++++++++++++++ test/user_info_test.exs | 24 +++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 priv/repo/migrations/20190412052952_add_user_info_fields.exs create mode 100644 test/user_info_test.exs diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index 5afa7988c..194dd5581 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -8,6 +8,8 @@ defmodule Pleroma.User.Info do alias Pleroma.User.Info + @type t :: %__MODULE__{} + embedded_schema do field(:banner, :map, default: %{}) field(:background, :map, default: %{}) @@ -40,6 +42,7 @@ defmodule Pleroma.User.Info do field(:hide_follows, :boolean, default: false) field(:pinned_activities, {:array, :string}, default: []) field(:flavour, :string, default: nil) + field(:email_notifications, :map, default: %{"digest" => true}) field(:notification_settings, :map, default: %{"remote" => true, "local" => true, "followers" => true, "follows" => true} @@ -75,6 +78,30 @@ def update_notification_settings(info, settings) do |> validate_required([:notification_settings]) end + @doc """ + Update email notifications in the given User.Info struct. + + Examples: + + iex> update_email_notifications(%Pleroma.User.Info{email_notifications: %{"digest" => false}}, %{"digest" => true}) + %Pleroma.User.Info{email_notifications: %{"digest" => true}} + + """ + @spec update_email_notifications(t(), map()) :: Ecto.Changeset.t() + def update_email_notifications(info, settings) do + email_notifications = + info.email_notifications + |> Map.merge(settings) + |> Map.take(["digest"]) + + params = %{email_notifications: email_notifications} + fields = [:email_notifications] + + info + |> cast(params, fields) + |> validate_required(fields) + end + def add_to_note_count(info, number) do set_note_count(info, info.note_count + number) end diff --git a/priv/repo/migrations/20190412052952_add_user_info_fields.exs b/priv/repo/migrations/20190412052952_add_user_info_fields.exs new file mode 100644 index 000000000..203d0fc3b --- /dev/null +++ b/priv/repo/migrations/20190412052952_add_user_info_fields.exs @@ -0,0 +1,20 @@ +defmodule Pleroma.Repo.Migrations.AddEmailNotificationsToUserInfo do + use Ecto.Migration + + def up do + execute(" + UPDATE users + SET info = info || '{ + \"email_notifications\": { + \"digest\": true + } + }'") + end + + def down do + execute(" + UPDATE users + SET info = info - 'email_notifications' + ") + end +end diff --git a/test/user_info_test.exs b/test/user_info_test.exs new file mode 100644 index 000000000..2d795594e --- /dev/null +++ b/test/user_info_test.exs @@ -0,0 +1,24 @@ +defmodule Pleroma.UserInfoTest do + alias Pleroma.Repo + alias Pleroma.User.Info + + use Pleroma.DataCase + + import Pleroma.Factory + + describe "update_email_notifications/2" do + setup do + user = insert(:user, %{info: %{email_notifications: %{"digest" => true}}}) + + {:ok, user: user} + end + + test "Notifications are updated", %{user: user} do + true = user.info.email_notifications["digest"] + changeset = Info.update_email_notifications(user.info, %{"digest" => false}) + assert changeset.valid? + {:ok, result} = Ecto.Changeset.apply_action(changeset, :insert) + assert result.email_notifications["digest"] == false + end + end +end From dc21181f6504b55afa68de63f170fcb0f1084a6b Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Sun, 14 Apr 2019 22:29:05 +0700 Subject: [PATCH 02/66] Update updated_at field on notification read --- lib/pleroma/notification.ex | 5 ++++- test/notification_test.exs | 23 +++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index b357d5399..29845b9da 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -58,7 +58,10 @@ def set_read_up_to(%{id: user_id} = _user, id) do where: n.user_id == ^user_id, where: n.id <= ^id, update: [ - set: [seen: true] + set: [ + seen: true, + updated_at: ^NaiveDateTime.utc_now() + ] ] ) diff --git a/test/notification_test.exs b/test/notification_test.exs index c3db77b6c..907b9e669 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -300,6 +300,29 @@ test "it sets all notifications as read up to a specified notification ID" do assert n2.seen == true assert n3.seen == false end + + test "Updates `updated_at` field" do + user1 = insert(:user) + user2 = insert(:user) + + Enum.each(0..10, fn i -> + {:ok, _activity} = + TwitterAPI.create_status(user1, %{ + "status" => "#{i} hi @#{user2.nickname}" + }) + end) + + Process.sleep(1000) + + [notification | _] = Notification.for_user(user2) + + Notification.set_read_up_to(user2, notification.id) + + Notification.for_user(user2) + |> Enum.each(fn notification -> + assert notification.updated_at > notification.inserted_at + end) + end end describe "notification target determination" do From 2f0203a4a1c7a507aa5cf50be2fd372536ebfc81 Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Wed, 17 Apr 2019 16:59:05 +0700 Subject: [PATCH 03/66] Resolve conflicts --- config/config.exs | 10 ++++++++ lib/pleroma/user.ex | 2 ++ mix.exs | 5 ++-- mix.lock | 51 +++++++++++++++++++++----------------- test/notification_test.exs | 22 ++++++++++------ 5 files changed, 57 insertions(+), 33 deletions(-) diff --git a/config/config.exs b/config/config.exs index 595e3505c..747d33884 100644 --- a/config/config.exs +++ b/config/config.exs @@ -464,6 +464,16 @@ total_user_limit: 300, enabled: true +config :pleroma, :email_notifications, + digest: %{ + # When to send digest email, in crontab format (https://en.wikipedia.org/wiki/Cron) + schedule: "0 0 * * 0", + # Minimum interval between digest emails to one user + interval: 7, + # Minimum user inactivity threshold + inactivity_threshold: 7 + } + # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. import_config "#{Mix.env()}.exs" diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 78eb29ddd..0982f6ed8 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -55,6 +55,8 @@ defmodule Pleroma.User do field(:tags, {:array, :string}, default: []) field(:bookmarks, {:array, :string}, default: []) field(:last_refreshed_at, :naive_datetime_usec) + field(:current_sign_in_at, :naive_datetime) + field(:last_digest_emailed_at, :naive_datetime) has_many(:notifications, Notification) has_many(:registrations, Registration) embeds_one(:info, Pleroma.User.Info) diff --git a/mix.exs b/mix.exs index 15e182239..da2e284f8 100644 --- a/mix.exs +++ b/mix.exs @@ -8,7 +8,7 @@ def project do elixir: "~> 1.7", elixirc_paths: elixirc_paths(Mix.env()), compilers: [:phoenix, :gettext] ++ Mix.compilers(), - elixirc_options: [warnings_as_errors: true], + # elixirc_options: [warnings_as_errors: true], xref: [exclude: [:eldap]], start_permanent: Mix.env() == :prod, aliases: aliases(), @@ -110,7 +110,8 @@ defp deps do {:prometheus_ecto, "~> 1.4"}, {:prometheus_process_collector, "~> 1.4"}, {:recon, github: "ferd/recon", tag: "2.4.0"}, - {:quack, "~> 0.1.1"} + {:quack, "~> 0.1.1"}, + {:quantum, "~> 2.3"} ] ++ oauth_deps end diff --git a/mix.lock b/mix.lock index d494cc82d..6e322240a 100644 --- a/mix.lock +++ b/mix.lock @@ -3,23 +3,24 @@ "auto_linker": {:git, "https://git.pleroma.social/pleroma/auto_linker.git", "90613b4bae875a3610c275b7056b61ffdd53210d", [ref: "90613b4bae875a3610c275b7056b61ffdd53210d"]}, "base64url": {:hex, :base64url, "0.0.1", "36a90125f5948e3afd7be97662a1504b934dd5dac78451ca6e9abf85a10286be", [:rebar], [], "hexpm"}, "bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"}, - "cachex": {:hex, :cachex, "3.0.2", "1351caa4e26e29f7d7ec1d29b53d6013f0447630bbf382b4fb5d5bad0209f203", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm"}, - "calendar": {:hex, :calendar, "0.17.4", "22c5e8d98a4db9494396e5727108dffb820ee0d18fed4b0aa8ab76e4f5bc32f1", [:mix], [{:tzdata, "~> 0.5.8 or ~> 0.1.201603", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, + "cachex": {:hex, :cachex, "3.0.3", "4e2d3e05814a5738f5ff3903151d5c25636d72a3527251b753f501ad9c657967", [:mix], [{:eternal, "~> 1.2", [hex: :eternal, repo: "hexpm", optional: false]}, {:unsafe, "~> 1.0", [hex: :unsafe, repo: "hexpm", optional: false]}], "hexpm"}, + "calendar": {:hex, :calendar, "0.17.5", "0ff5b09a60b9677683aa2a6fee948558660501c74a289103ea099806bc41a352", [:mix], [{:tzdata, "~> 0.5.20 or ~> 0.1.201603", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, "certifi": {:hex, :certifi, "2.5.1", "867ce347f7c7d78563450a18a6a28a8090331e77fa02380b4a21962a65d36ee5", [:rebar3], [{:parse_trans, "~>3.3", [hex: :parse_trans, repo: "hexpm", optional: false]}], "hexpm"}, "combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm"}, - "comeonin": {:hex, :comeonin, "4.1.1", "c7304fc29b45b897b34142a91122bc72757bc0c295e9e824999d5179ffc08416", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm"}, + "comeonin": {:hex, :comeonin, "4.1.2", "3eb5620fd8e35508991664b4c2b04dd41e52f1620b36957be837c1d7784b7592", [:mix], [{:argon2_elixir, "~> 1.2", [hex: :argon2_elixir, repo: "hexpm", optional: true]}, {:bcrypt_elixir, "~> 0.12.1 or ~> 1.0", [hex: :bcrypt_elixir, repo: "hexpm", optional: true]}, {:pbkdf2_elixir, "~> 0.12", [hex: :pbkdf2_elixir, repo: "hexpm", optional: true]}], "hexpm"}, "connection": {:hex, :connection, "1.0.4", "a1cae72211f0eef17705aaededacac3eb30e6625b04a6117c1b2db6ace7d5976", [:mix], [], "hexpm"}, "cors_plug": {:hex, :cors_plug, "1.5.2", "72df63c87e4f94112f458ce9d25800900cc88608c1078f0e4faddf20933eda6e", [:mix], [{:plug, "~> 1.3 or ~> 1.4 or ~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, - "cowboy": {:hex, :cowboy, "2.6.1", "f2e06f757c337b3b311f9437e6e072b678fcd71545a7b2865bdaa154d078593f", [:rebar3], [{:cowlib, "~> 2.7.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, - "cowlib": {:hex, :cowlib, "2.7.0", "3ef16e77562f9855a2605900cedb15c1462d76fb1be6a32fc3ae91973ee543d2", [:rebar3], [], "hexpm"}, + "cowboy": {:hex, :cowboy, "2.6.3", "99aa50e94e685557cad82e704457336a453d4abcb77839ad22dbe71f311fcc06", [:rebar3], [{:cowlib, "~> 2.7.3", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, + "cowlib": {:hex, :cowlib, "2.7.3", "a7ffcd0917e6d50b4d5fb28e9e2085a0ceb3c97dea310505f7460ff5ed764ce9", [:rebar3], [], "hexpm"}, "credo": {:hex, :credo, "0.9.3", "76fa3e9e497ab282e0cf64b98a624aa11da702854c52c82db1bf24e54ab7c97a", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, + "crontab": {:hex, :crontab, "1.1.5", "2c9439506ceb0e9045de75879e994b88d6f0be88bfe017d58cb356c66c4a5482", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "crypt": {:git, "https://github.com/msantos/crypt", "1f2b58927ab57e72910191a7ebaeff984382a1d3", [ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"]}, - "db_connection": {:hex, :db_connection, "2.0.5", "ddb2ba6761a08b2bb9ca0e7d260e8f4dd39067426d835c24491a321b7f92a4da", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"}, + "db_connection": {:hex, :db_connection, "2.0.6", "bde2f85d047969c5b5800cb8f4b3ed6316c8cb11487afedac4aa5f93fd39abfa", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"}, "decimal": {:hex, :decimal, "1.7.0", "30d6b52c88541f9a66637359ddf85016df9eb266170d53105f02e4a67e00c5aa", [:mix], [], "hexpm"}, "earmark": {:hex, :earmark, "1.3.2", "b840562ea3d67795ffbb5bd88940b1bed0ed9fa32834915125ea7d02e35888a5", [:mix], [], "hexpm"}, - "ecto": {:hex, :ecto, "3.0.7", "44dda84ac6b17bbbdeb8ac5dfef08b7da253b37a453c34ab1a98de7f7e5fec7f", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, + "ecto": {:hex, :ecto, "3.0.8", "9eb6a1fcfc593e6619d45ef51afe607f1554c21ca188a1cd48eecc27223567f1", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 2.2 or ~> 3.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, "ecto_sql": {:hex, :ecto_sql, "3.0.5", "7e44172b4f7aca4469f38d7f6a3da394dbf43a1bcf0ca975e958cb957becd74e", [:mix], [{:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0.6", [hex: :ecto, repo: "hexpm", optional: false]}, {:mariaex, "~> 0.9.1", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.14.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, - "eternal": {:hex, :eternal, "1.2.0", "e2a6b6ce3b8c248f7dc31451aefca57e3bdf0e48d73ae5043229380a67614c41", [:mix], [], "hexpm"}, + "eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm"}, "ex_aws": {:hex, :ex_aws, "2.1.0", "b92651527d6c09c479f9013caa9c7331f19cba38a650590d82ebf2c6c16a1d8a", [:mix], [{:configparser_ex, "~> 2.0", [hex: :configparser_ex, repo: "hexpm", optional: true]}, {:hackney, "1.6.3 or 1.6.5 or 1.7.1 or 1.8.6 or ~> 1.9", [hex: :hackney, repo: "hexpm", optional: true]}, {:jsx, "~> 2.8", [hex: :jsx, repo: "hexpm", optional: true]}, {:poison, ">= 1.2.0", [hex: :poison, repo: "hexpm", optional: true]}, {:sweet_xml, "~> 0.6", [hex: :sweet_xml, repo: "hexpm", optional: true]}, {:xml_builder, "~> 0.1.0", [hex: :xml_builder, repo: "hexpm", optional: true]}], "hexpm"}, "ex_aws_s3": {:hex, :ex_aws_s3, "2.0.1", "9e09366e77f25d3d88c5393824e613344631be8db0d1839faca49686e99b6704", [:mix], [{:ex_aws, "~> 2.0", [hex: :ex_aws, repo: "hexpm", optional: false]}, {:sweet_xml, ">= 0.0.0", [hex: :sweet_xml, repo: "hexpm", optional: true]}], "hexpm"}, "ex_doc": {:hex, :ex_doc, "0.20.2", "1bd0dfb0304bade58beb77f20f21ee3558cc3c753743ae0ddbb0fd7ba2912331", [:mix], [{:earmark, "~> 1.3", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, @@ -27,57 +28,61 @@ "ex_syslogger": {:git, "https://github.com/slashmili/ex_syslogger.git", "f3963399047af17e038897c69e20d552e6899e1d", [tag: "1.4.0"]}, "floki": {:hex, :floki, "0.20.4", "be42ac911fece24b4c72f3b5846774b6e61b83fe685c2fc9d62093277fb3bc86", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}, {:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"}, "gen_smtp": {:hex, :gen_smtp, "0.13.0", "11f08504c4bdd831dc520b8f84a1dce5ce624474a797394e7aafd3c29f5dcd25", [:rebar3], [], "hexpm"}, - "gettext": {:hex, :gettext, "0.15.0", "40a2b8ce33a80ced7727e36768499fc9286881c43ebafccae6bab731e2b2b8ce", [:mix], [], "hexpm"}, + "gen_stage": {:hex, :gen_stage, "0.14.1", "9d46723fda072d4f4bb31a102560013f7960f5d80ea44dcb96fd6304ed61e7a4", [:mix], [], "hexpm"}, + "gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"}, + "gettext": {:hex, :gettext, "0.16.1", "e2130b25eebcbe02bb343b119a07ae2c7e28bd4b146c4a154da2ffb2b3507af2", [:mix], [], "hexpm"}, "hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, "html_entities": {:hex, :html_entities, "0.4.0", "f2fee876858cf6aaa9db608820a3209e45a087c5177332799592142b50e89a6b", [:mix], [], "hexpm"}, "html_sanitize_ex": {:hex, :html_sanitize_ex, "1.3.0", "f005ad692b717691203f940c686208aa3d8ffd9dd4bb3699240096a51fa9564e", [:mix], [{:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"}, "httpoison": {:hex, :httpoison, "1.2.0", "2702ed3da5fd7a8130fc34b11965c8cfa21ade2f232c00b42d96d4967c39a3a3", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, - "jose": {:hex, :jose, "1.8.4", "7946d1e5c03a76ac9ef42a6e6a20001d35987afd68c2107bcd8f01a84e75aa73", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"}, + "jose": {:hex, :jose, "1.9.0", "4167c5f6d06ffaebffd15cdb8da61a108445ef5e85ab8f5a7ad926fdf3ada154", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"}, + "libring": {:hex, :libring, "1.4.0", "41246ba2f3fbc76b3971f6bce83119dfec1eee17e977a48d8a9cfaaf58c2a8d6", [:mix], [], "hexpm"}, "makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, "makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, "meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm"}, "mime": {:hex, :mime, "1.3.1", "30ce04ab3175b6ad0bdce0035cba77bba68b813d523d1aac73d9781b4d193cf8", [:mix], [], "hexpm"}, "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm"}, - "mochiweb": {:hex, :mochiweb, "2.15.0", "e1daac474df07651e5d17cc1e642c4069c7850dc4508d3db7263a0651330aacc", [:rebar3], [], "hexpm"}, - "mock": {:hex, :mock, "0.3.1", "994f00150f79a0ea50dc9d86134cd9ebd0d177ad60bd04d1e46336cdfdb98ff9", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, + "mochiweb": {:hex, :mochiweb, "2.18.0", "eb55f1db3e6e960fac4e6db4e2db9ec3602cc9f30b86cd1481d56545c3145d2e", [:rebar3], [], "hexpm"}, + "mock": {:hex, :mock, "0.3.3", "42a433794b1291a9cf1525c6d26b38e039e0d3a360732b5e467bfc77ef26c914", [:mix], [{:meck, "~> 0.8.13", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"}, "mogrify": {:hex, :mogrify, "0.6.1", "de1b527514f2d95a7bbe9642eb556061afb337e220cf97adbf3a4e6438ed70af", [:mix], [], "hexpm"}, "nimble_parsec": {:hex, :nimble_parsec, "0.5.0", "90e2eca3d0266e5c53f8fbe0079694740b9c91b6747f2b7e3c5d21966bba8300", [:mix], [], "hexpm"}, "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, - "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.3", "6706a148809a29c306062862c803406e88f048277f6e85b68faf73291e820b84", [:mix], [], "hexpm"}, - "phoenix": {:hex, :phoenix, "1.4.1", "801f9d632808657f1f7c657c8bbe624caaf2ba91429123ebe3801598aea4c3d9", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"}, + "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.4", "8dd29ed783f2e12195d7e0a4640effc0a7c37e6537da491f1db01839eee6d053", [:mix], [], "hexpm"}, + "phoenix": {:hex, :phoenix, "1.4.3", "8eed4a64ff1e12372cd634724bddd69185938f52c18e1396ebac76375d85677d", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.0.0", "c43117a136e7399ea04ecaac73f8f23ee0ffe3e07acfcb8062fe5f4c9f0f6531", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, - "phoenix_html": {:hex, :phoenix_html, "2.13.1", "fa8f034b5328e2dfa0e4131b5569379003f34bc1fafdaa84985b0b9d2f12e68b", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, - "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.1", "6668d787e602981f24f17a5fbb69cc98f8ab085114ebfac6cc36e10a90c8e93c", [:mix], [], "hexpm"}, + "phoenix_html": {:hex, :phoenix_html, "2.13.2", "f5d27c9b10ce881a60177d2b5227314fc60881e6b66b41dfe3349db6ed06cf57", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm"}, "pleroma_job_queue": {:hex, :pleroma_job_queue, "0.2.0", "879e660aa1cebe8dc6f0aaaa6aa48b4875e89cd961d4a585fd128e0773b31a18", [:mix], [], "hexpm"}, "plug": {:hex, :plug, "1.7.2", "d7b7db7fbd755e8283b6c0a50be71ec0a3d67d9213d74422d9372effc8e87fd1", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}], "hexpm"}, - "plug_cowboy": {:hex, :plug_cowboy, "2.0.1", "d798f8ee5acc86b7d42dbe4450b8b0dadf665ce588236eb0a751a132417a980e", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.0.2", "6055f16868cc4882b24b6e1d63d2bada94fb4978413377a3b32ac16c18dffba2", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, - "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm"}, - "postgrex": {:hex, :postgrex, "0.14.1", "63247d4a5ad6b9de57a0bac5d807e1c32d41e39c04b8a4156a26c63bcd8a2e49", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, + "postgrex": {:hex, :postgrex, "0.14.2", "6680591bbce28d92f043249205e8b01b36cab9ef2a7911abc43649242e1a3b78", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, "prometheus": {:hex, :prometheus, "4.2.2", "a830e77b79dc6d28183f4db050a7cac926a6c58f1872f9ef94a35cd989aceef8", [:mix, :rebar3], [], "hexpm"}, "prometheus_ecto": {:hex, :prometheus_ecto, "1.4.1", "6c768ea9654de871e5b32fab2eac348467b3021604ebebbcbd8bcbe806a65ed5", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm"}, "prometheus_ex": {:hex, :prometheus_ex, "3.0.5", "fa58cfd983487fc5ead331e9a3e0aa622c67232b3ec71710ced122c4c453a02f", [:mix], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm"}, "prometheus_phoenix": {:hex, :prometheus_phoenix, "1.2.1", "964a74dfbc055f781d3a75631e06ce3816a2913976d1df7830283aa3118a797a", [:mix], [{:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.3 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm"}, "prometheus_plugs": {:hex, :prometheus_plugs, "1.1.5", "25933d48f8af3a5941dd7b621c889749894d8a1082a6ff7c67cc99dec26377c5", [:mix], [{:accept, "~> 0.1", [hex: :accept, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}, {:prometheus_process_collector, "~> 1.1", [hex: :prometheus_process_collector, repo: "hexpm", optional: true]}], "hexpm"}, - "prometheus_process_collector": {:hex, :prometheus_process_collector, "1.4.0", "6dbd39e3165b9ef1c94a7a820e9ffe08479f949dcdd431ed4aaea7b250eebfde", [:rebar3], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm"}, + "prometheus_process_collector": {:hex, :prometheus_process_collector, "1.4.3", "657386e8f142fc817347d95c1f3a05ab08710f7df9e7f86db6facaed107ed929", [:rebar3], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm"}, "quack": {:hex, :quack, "0.1.1", "cca7b4da1a233757fdb44b3334fce80c94785b3ad5a602053b7a002b5a8967bf", [:mix], [{:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: false]}, {:tesla, "~> 1.2.0", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm"}, + "quantum": {:hex, :quantum, "2.3.4", "72a0e8855e2adc101459eac8454787cb74ab4169de6ca50f670e72142d4960e9", [:mix], [{:calendar, "~> 0.17", [hex: :calendar, repo: "hexpm", optional: true]}, {:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.12", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:swarm, "~> 3.3", [hex: :swarm, repo: "hexpm", optional: false]}, {:timex, "~> 3.1", [hex: :timex, repo: "hexpm", optional: true]}], "hexpm"}, "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"}, "recon": {:git, "https://github.com/ferd/recon.git", "75d70c7c08926d2f24f1ee6de14ee50fe8a52763", [tag: "2.4.0"]}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"}, - "swoosh": {:hex, :swoosh, "0.20.0", "9a6c13822c9815993c03b6f8fccc370fcffb3c158d9754f67b1fdee6b3a5d928", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.12", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.4", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm"}, + "swarm": {:hex, :swarm, "3.4.0", "64f8b30055d74640d2186c66354b33b999438692a91be275bb89cdc7e401f448", [:mix], [{:gen_state_machine, "~> 2.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}, {:libring, "~> 1.0", [hex: :libring, repo: "hexpm", optional: false]}], "hexpm"}, + "swoosh": {:hex, :swoosh, "0.23.1", "209b7cc6d862c09d2a064c16caa4d4d1c9c936285476459e16591e0065f8432b", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.12", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"}, "syslog": {:git, "https://github.com/Vagabond/erlang-syslog.git", "4a6c6f2c996483e86c1320e9553f91d337bcb6aa", [tag: "1.0.5"]}, "telemetry": {:hex, :telemetry, "0.3.0", "099a7f3ce31e4780f971b4630a3c22ec66d22208bc090fe33a2a3a6a67754a73", [:rebar3], [], "hexpm"}, "tesla": {:hex, :tesla, "1.2.1", "864783cc27f71dd8c8969163704752476cec0f3a51eb3b06393b3971dc9733ff", [:mix], [{:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm"}, "timex": {:hex, :timex, "3.5.0", "b0a23167da02d0fe4f1a4e104d1f929a00d348502b52432c05de875d0b9cffa5", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, - "tzdata": {:hex, :tzdata, "0.5.17", "50793e3d85af49736701da1a040c415c97dc1caf6464112fd9bd18f425d3053b", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, + "tzdata": {:hex, :tzdata, "0.5.20", "304b9e98a02840fb32a43ec111ffbe517863c8566eb04a061f1c4dbb90b4d84c", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "ueberauth": {:hex, :ueberauth, "0.6.1", "9e90d3337dddf38b1ca2753aca9b1e53d8a52b890191cdc55240247c89230412", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.4.1", "d869e4c68901dd9531385bb0c8c40444ebf624e60b6962d95952775cac5e90cd", [:rebar3], [], "hexpm"}, - "unsafe": {:hex, :unsafe, "1.0.0", "7c21742cd05380c7875546b023481d3a26f52df8e5dfedcb9f958f322baae305", [:mix], [], "hexpm"}, + "unsafe": {:hex, :unsafe, "1.0.1", "a27e1874f72ee49312e0a9ec2e0b27924214a05e3ddac90e91727bc76f8613d8", [:mix], [], "hexpm"}, "web_push_encryption": {:hex, :web_push_encryption, "0.2.1", "d42cecf73420d9dc0053ba3299cc8c8d6ff2be2487d67ca2a57265868e4d9a98", [:mix], [{:httpoison, "~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jose, "~> 1.8", [hex: :jose, repo: "hexpm", optional: false]}, {:poison, "~> 3.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, "websocket_client": {:git, "https://github.com/jeremyong/websocket_client.git", "9a6f65d05ebf2725d62fb19262b21f1805a59fbf", []}, } diff --git a/test/notification_test.exs b/test/notification_test.exs index 907b9e669..27d8cace7 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -4,12 +4,15 @@ defmodule Pleroma.NotificationTest do use Pleroma.DataCase + + import Pleroma.Factory + import Mock + alias Pleroma.Notification alias Pleroma.User alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.CommonAPI alias Pleroma.Web.TwitterAPI.TwitterAPI - import Pleroma.Factory describe "create_notifications" do test "notifies someone when they are directly addressed" do @@ -312,16 +315,19 @@ test "Updates `updated_at` field" do }) end) - Process.sleep(1000) - [notification | _] = Notification.for_user(user2) - Notification.set_read_up_to(user2, notification.id) + utc_now = NaiveDateTime.utc_now() + future = NaiveDateTime.add(utc_now, 5, :second) - Notification.for_user(user2) - |> Enum.each(fn notification -> - assert notification.updated_at > notification.inserted_at - end) + with_mock NaiveDateTime, utc_now: fn -> future end do + Notification.set_read_up_to(user2, notification.id) + + Notification.for_user(user2) + |> Enum.each(fn notification -> + assert notification.updated_at > notification.inserted_at + end) + end end end From aeafa0b2ef996f15f9ff4a6ade70a693b12b208f Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Fri, 19 Apr 2019 22:16:17 +0700 Subject: [PATCH 04/66] Add Notification.for_user_since/2 --- config/config.exs | 1 + lib/pleroma/notification.ex | 21 +++++++++++++++++ test/notification_test.exs | 45 +++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/config/config.exs b/config/config.exs index 747d33884..c452b728b 100644 --- a/config/config.exs +++ b/config/config.exs @@ -467,6 +467,7 @@ config :pleroma, :email_notifications, digest: %{ # When to send digest email, in crontab format (https://en.wikipedia.org/wiki/Cron) + # 0 0 * * 0 - once a week at midnight on Sunday morning schedule: "0 0 * * 0", # Minimum interval between digest emails to one user interval: 7, diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 29845b9da..d79f0f563 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -17,6 +17,8 @@ defmodule Pleroma.Notification do import Ecto.Query import Ecto.Changeset + @type t :: %__MODULE__{} + schema "notifications" do field(:seen, :boolean, default: false) belongs_to(:user, User, type: Pleroma.FlakeId) @@ -51,6 +53,25 @@ def for_user(user, opts \\ %{}) do |> Pagination.fetch_paginated(opts) end + @doc """ + Returns notifications for user received since given date. + + ## Examples + + iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-13 11:22:33]) + [%Pleroma.Notification{}, %Pleroma.Notification{}] + + iex> Pleroma.Notification.for_user_since(%Pleroma.User{}, ~N[2019-04-15 11:22:33]) + [] + """ + @spec for_user_since(Pleroma.User.t(), NaiveDateTime.t()) :: [t()] + def for_user_since(user, date) do + from(n in for_user_query(user), + where: n.updated_at > ^date + ) + |> Repo.all() + end + def set_read_up_to(%{id: user_id} = _user, id) do query = from( diff --git a/test/notification_test.exs b/test/notification_test.exs index 27d8cace7..dbc4f48f6 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -331,6 +331,51 @@ test "Updates `updated_at` field" do end end + describe "for_user_since/2" do + defp days_ago(days) do + NaiveDateTime.add( + NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second), + -days * 60 * 60 * 24, + :second + ) + end + + test "Returns recent notifications" do + user1 = insert(:user) + user2 = insert(:user) + + Enum.each(0..10, fn i -> + {:ok, _activity} = + CommonAPI.post(user1, %{ + "status" => "hey ##{i} @#{user2.nickname}!" + }) + end) + + {old, new} = Enum.split(Notification.for_user(user2), 5) + + Enum.each(old, fn notification -> + notification + |> cast(%{updated_at: days_ago(10)}, [:updated_at]) + |> Pleroma.Repo.update!() + end) + + recent_notifications_ids = + user2 + |> Notification.for_user_since( + NaiveDateTime.add(NaiveDateTime.utc_now(), -5 * 86400, :second) + ) + |> Enum.map(& &1.id) + + Enum.each(old, fn %{id: id} -> + refute id in recent_notifications_ids + end) + + Enum.each(new, fn %{id: id} -> + assert id in recent_notifications_ids + end) + end + end + describe "notification target determination" do test "it sends notifications to addressed users in new messages" do user = insert(:user) From 8add1194448cfc183dce01b86451422195d44023 Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Fri, 19 Apr 2019 22:17:54 +0700 Subject: [PATCH 05/66] Add User.list_inactive_users_query/1 --- lib/pleroma/user.ex | 38 +++++++ ...d_signin_and_last_digest_dates_to_user.exs | 9 ++ test/user_test.exs | 103 ++++++++++++++++++ 3 files changed, 150 insertions(+) create mode 100644 priv/repo/migrations/20190413085040_add_signin_and_last_digest_dates_to_user.exs diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 0982f6ed8..c67a7b7a1 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1447,4 +1447,42 @@ defp paginate(query, page, page_size) do def showing_reblogs?(%User{} = user, %User{} = target) do target.ap_id not in user.info.muted_reblogs end + + @doc """ + The function returns a query to get users with no activity for given interval of days. + Inactive users are those who didn't read any notification, or had any activity where + the user is the activity's actor, during `inactivity_threshold` days. + Deactivated users will not appear in this list. + + ## Examples + + iex> Pleroma.User.list_inactive_users() + %Ecto.Query{} + """ + @spec list_inactive_users_query(integer()) :: Ecto.Query.t() + def list_inactive_users_query(inactivity_threshold \\ 7) do + negative_inactivity_threshold = -inactivity_threshold + now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) + # Subqueries are not supported in `where` clauses, join gets too complicated. + has_read_notifications = + from(n in Pleroma.Notification, + where: n.seen == true, + group_by: n.id, + having: max(n.updated_at) > datetime_add(^now, ^negative_inactivity_threshold, "day"), + select: n.user_id + ) + |> Pleroma.Repo.all() + + from(u in Pleroma.User, + left_join: a in Pleroma.Activity, + on: u.ap_id == a.actor, + where: not is_nil(u.nickname), + where: fragment("not (?->'deactivated' @> 'true')", u.info), + where: u.id not in ^has_read_notifications, + group_by: u.id, + having: + max(a.inserted_at) < datetime_add(^now, ^negative_inactivity_threshold, "day") or + is_nil(max(a.inserted_at)) + ) + end end diff --git a/priv/repo/migrations/20190413085040_add_signin_and_last_digest_dates_to_user.exs b/priv/repo/migrations/20190413085040_add_signin_and_last_digest_dates_to_user.exs new file mode 100644 index 000000000..4312b171f --- /dev/null +++ b/priv/repo/migrations/20190413085040_add_signin_and_last_digest_dates_to_user.exs @@ -0,0 +1,9 @@ +defmodule Pleroma.Repo.Migrations.AddSigninAndLastDigestDatesToUser do + use Ecto.Migration + + def change do + alter table(:users) do + add(:last_digest_emailed_at, :naive_datetime, default: fragment("now()")) + end + end +end diff --git a/test/user_test.exs b/test/user_test.exs index d2167a970..ba02997dc 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -1167,4 +1167,107 @@ test "follower count is updated when a follower is blocked" do assert Map.get(user_show, "followers_count") == 2 end + + describe "list_inactive_users_query/1" do + defp days_ago(days) do + NaiveDateTime.add( + NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second), + -days * 60 * 60 * 24, + :second + ) + end + + test "Users are inactive by default" do + total = 10 + + users = + Enum.map(1..total, fn _ -> + insert(:user, last_digest_emailed_at: days_ago(20), info: %{deactivated: false}) + end) + + inactive_users_ids = + Pleroma.User.list_inactive_users_query() + |> Pleroma.Repo.all() + |> Enum.map(& &1.id) + + Enum.each(users, fn user -> + assert user.id in inactive_users_ids + end) + end + + test "Only includes users who has no recent activity" do + total = 10 + + users = + Enum.map(1..total, fn _ -> + insert(:user, last_digest_emailed_at: days_ago(20), info: %{deactivated: false}) + end) + + {inactive, active} = Enum.split(users, trunc(total / 2)) + + Enum.map(active, fn user -> + to = Enum.random(users -- [user]) + + {:ok, _} = + Pleroma.Web.TwitterAPI.TwitterAPI.create_status(user, %{ + "status" => "hey @#{to.nickname}" + }) + end) + + inactive_users_ids = + Pleroma.User.list_inactive_users_query() + |> Pleroma.Repo.all() + |> Enum.map(& &1.id) + + Enum.each(active, fn user -> + refute user.id in inactive_users_ids + end) + + Enum.each(inactive, fn user -> + assert user.id in inactive_users_ids + end) + end + + test "Only includes users with no read notifications" do + total = 10 + + users = + Enum.map(1..total, fn _ -> + insert(:user, last_digest_emailed_at: days_ago(20), info: %{deactivated: false}) + end) + + [sender | recipients] = users + {inactive, active} = Enum.split(recipients, trunc(total / 2)) + + Enum.each(recipients, fn to -> + {:ok, _} = + Pleroma.Web.TwitterAPI.TwitterAPI.create_status(sender, %{ + "status" => "hey @#{to.nickname}" + }) + + {:ok, _} = + Pleroma.Web.TwitterAPI.TwitterAPI.create_status(sender, %{ + "status" => "hey again @#{to.nickname}" + }) + end) + + Enum.each(active, fn user -> + [n1, _n2] = Pleroma.Notification.for_user(user) + {:ok, _} = Pleroma.Notification.read_one(user, n1.id) + end) + + inactive_users_ids = + Pleroma.User.list_inactive_users_query() + |> Pleroma.Repo.all() + |> Enum.map(& &1.id) + + Enum.each(active, fn user -> + refute user.id in inactive_users_ids + end) + + Enum.each(inactive, fn user -> + assert user.id in inactive_users_ids + end) + end + end end From bc7862106d9881f858a58319e9e4b44cba1bcf01 Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Fri, 19 Apr 2019 23:26:41 +0700 Subject: [PATCH 06/66] Fix tests --- lib/pleroma/user.ex | 1 - test/notification_test.exs | 27 --------------------------- test/support/builders/user_builder.ex | 3 ++- test/support/factory.ex | 3 ++- 4 files changed, 4 insertions(+), 30 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index c67a7b7a1..7053dfaf3 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -55,7 +55,6 @@ defmodule Pleroma.User do field(:tags, {:array, :string}, default: []) field(:bookmarks, {:array, :string}, default: []) field(:last_refreshed_at, :naive_datetime_usec) - field(:current_sign_in_at, :naive_datetime) field(:last_digest_emailed_at, :naive_datetime) has_many(:notifications, Notification) has_many(:registrations, Registration) diff --git a/test/notification_test.exs b/test/notification_test.exs index dbc4f48f6..462398d75 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -6,7 +6,6 @@ defmodule Pleroma.NotificationTest do use Pleroma.DataCase import Pleroma.Factory - import Mock alias Pleroma.Notification alias Pleroma.User @@ -303,32 +302,6 @@ test "it sets all notifications as read up to a specified notification ID" do assert n2.seen == true assert n3.seen == false end - - test "Updates `updated_at` field" do - user1 = insert(:user) - user2 = insert(:user) - - Enum.each(0..10, fn i -> - {:ok, _activity} = - TwitterAPI.create_status(user1, %{ - "status" => "#{i} hi @#{user2.nickname}" - }) - end) - - [notification | _] = Notification.for_user(user2) - - utc_now = NaiveDateTime.utc_now() - future = NaiveDateTime.add(utc_now, 5, :second) - - with_mock NaiveDateTime, utc_now: fn -> future end do - Notification.set_read_up_to(user2, notification.id) - - Notification.for_user(user2) - |> Enum.each(fn notification -> - assert notification.updated_at > notification.inserted_at - end) - end - end end describe "for_user_since/2" do diff --git a/test/support/builders/user_builder.ex b/test/support/builders/user_builder.ex index f58e1b0ad..6da16f71a 100644 --- a/test/support/builders/user_builder.ex +++ b/test/support/builders/user_builder.ex @@ -9,7 +9,8 @@ def build(data \\ %{}) do nickname: "testname", password_hash: Comeonin.Pbkdf2.hashpwsalt("test"), bio: "A tester.", - ap_id: "some id" + ap_id: "some id", + last_digest_emailed_at: NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) } Map.merge(user, data) diff --git a/test/support/factory.ex b/test/support/factory.ex index ea59912cf..0840f31ec 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -12,7 +12,8 @@ def user_factory do nickname: sequence(:nickname, &"nick#{&1}"), password_hash: Comeonin.Pbkdf2.hashpwsalt("test"), bio: sequence(:bio, &"Tester Number #{&1}"), - info: %{} + info: %{}, + last_digest_emailed_at: NaiveDateTime.utc_now() } %{ From 64a2c6a041ca62ad84b1d682ef56fbca45e44de5 Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Sat, 20 Apr 2019 19:42:19 +0700 Subject: [PATCH 07/66] Digest emails --- config/config.exs | 2 + lib/mix/tasks/pleroma/instance.ex | 2 + lib/mix/tasks/pleroma/sample_config.eex | 2 + lib/pleroma/application.ex | 22 ++++++- lib/pleroma/digest_email_worker.ex | 45 ++++++++++++++ lib/pleroma/emails/user_email.ex | 59 ++++++++++++++++++- lib/pleroma/jwt.ex | 9 +++ lib/pleroma/quantum_scheduler.ex | 4 ++ lib/pleroma/user.ex | 36 +++++++++++ .../web/mailer/subscription_controller.ex | 18 ++++++ lib/pleroma/web/router.ex | 2 + .../web/templates/email/digest.html.eex | 20 +++++++ .../web/templates/layout/email.html.eex | 10 ++++ .../subscription/unsubscribe_failure.html.eex | 1 + .../subscription/unsubscribe_success.html.eex | 1 + lib/pleroma/web/views/email_view.ex | 5 ++ .../web/views/mailer/subscription_view.ex | 3 + mix.exs | 4 +- mix.lock | 2 + 19 files changed, 243 insertions(+), 4 deletions(-) create mode 100644 lib/pleroma/digest_email_worker.ex create mode 100644 lib/pleroma/jwt.ex create mode 100644 lib/pleroma/quantum_scheduler.ex create mode 100644 lib/pleroma/web/mailer/subscription_controller.ex create mode 100644 lib/pleroma/web/templates/email/digest.html.eex create mode 100644 lib/pleroma/web/templates/layout/email.html.eex create mode 100644 lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex create mode 100644 lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex create mode 100644 lib/pleroma/web/views/email_view.ex create mode 100644 lib/pleroma/web/views/mailer/subscription_view.ex diff --git a/config/config.exs b/config/config.exs index 25dc91eb1..2663b1ebd 100644 --- a/config/config.exs +++ b/config/config.exs @@ -468,6 +468,8 @@ config :pleroma, :email_notifications, digest: %{ + # Globally enable or disable digest emails + active: true, # When to send digest email, in crontab format (https://en.wikipedia.org/wiki/Cron) # 0 0 * * 0 - once a week at midnight on Sunday morning schedule: "0 0 * * 0", diff --git a/lib/mix/tasks/pleroma/instance.ex b/lib/mix/tasks/pleroma/instance.ex index 6cee8d630..d276df93a 100644 --- a/lib/mix/tasks/pleroma/instance.ex +++ b/lib/mix/tasks/pleroma/instance.ex @@ -125,6 +125,7 @@ def run(["gen" | rest]) do ) secret = :crypto.strong_rand_bytes(64) |> Base.encode64() |> binary_part(0, 64) + jwt_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) @@ -142,6 +143,7 @@ def run(["gen" | rest]) do dbpass: dbpass, version: Pleroma.Mixfile.project() |> Keyword.get(:version), secret: secret, + jwt_secret: jwt_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) diff --git a/lib/mix/tasks/pleroma/sample_config.eex b/lib/mix/tasks/pleroma/sample_config.eex index 52bd57cb7..ec7d8821e 100644 --- a/lib/mix/tasks/pleroma/sample_config.eex +++ b/lib/mix/tasks/pleroma/sample_config.eex @@ -76,3 +76,5 @@ config :web_push_encryption, :vapid_details, # storage_url: "https://swift-endpoint.prodider.com/v1/AUTH_/", # object_url: "https://cdn-endpoint.provider.com/" # + +config :joken, default_signer: "<%= jwt_secret %>" diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index eeb415084..76f8d9bcd 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -105,7 +105,8 @@ def start(_type, _args) do id: :cachex_idem ), worker(Pleroma.FlakeId, []), - worker(Pleroma.ScheduledActivityWorker, []) + worker(Pleroma.ScheduledActivityWorker, []), + worker(Pleroma.QuantumScheduler, []) ] ++ hackney_pool_children() ++ [ @@ -125,7 +126,9 @@ def start(_type, _args) do # See http://elixir-lang.org/docs/stable/elixir/Supervisor.html # for other strategies and supported options opts = [strategy: :one_for_one, name: Pleroma.Supervisor] - Supervisor.start_link(children, opts) + result = Supervisor.start_link(children, opts) + :ok = after_supervisor_start() + result end defp setup_instrumenters do @@ -183,4 +186,19 @@ defp hackney_pool_children do :hackney_pool.child_spec(pool, options) end end + + defp after_supervisor_start() do + with digest_config <- Application.get_env(:pleroma, :email_notifications)[:digest], + true <- digest_config[:active], + %Crontab.CronExpression{} = schedule <- + Crontab.CronExpression.Parser.parse!(digest_config[:schedule]) do + Pleroma.QuantumScheduler.new_job() + |> Quantum.Job.set_name(:digest_emails) + |> Quantum.Job.set_schedule(schedule) + |> Quantum.Job.set_task(&Pleroma.DigestEmailWorker.run/0) + |> Pleroma.QuantumScheduler.add_job() + end + + :ok + end end diff --git a/lib/pleroma/digest_email_worker.ex b/lib/pleroma/digest_email_worker.ex new file mode 100644 index 000000000..fa6067a03 --- /dev/null +++ b/lib/pleroma/digest_email_worker.ex @@ -0,0 +1,45 @@ +defmodule Pleroma.DigestEmailWorker do + import Ecto.Query + require Logger + + # alias Pleroma.User + + def run() do + Logger.warn("Running digester") + config = Application.get_env(:pleroma, :email_notifications)[:digest] + negative_interval = -Map.fetch!(config, :interval) + inactivity_threshold = Map.fetch!(config, :inactivity_threshold) + inactive_users_query = Pleroma.User.list_inactive_users_query(inactivity_threshold) + + now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) + + from(u in inactive_users_query, + where: fragment("? #> '{\"email_notifications\",\"digest\"}' @> 'true'", u.info), + where: u.last_digest_emailed_at < datetime_add(^now, ^negative_interval, "day"), + select: u + ) + |> Pleroma.Repo.all() + |> run(:pre) + end + + defp run(v, :pre) do + Logger.warn("Running for #{length(v)} users") + run(v) + end + + defp run([]), do: :ok + + defp run([user | users]) do + with %Swoosh.Email{} = email <- Pleroma.Emails.UserEmail.digest_email(user) do + Logger.warn("Sending to #{user.nickname}") + Pleroma.Emails.Mailer.deliver_async(email) + else + _ -> + Logger.warn("Skipping #{user.nickname}") + end + + Pleroma.User.touch_last_digest_emailed_at(user) + + run(users) + end +end diff --git a/lib/pleroma/emails/user_email.ex b/lib/pleroma/emails/user_email.ex index 8502a0d0c..64f855112 100644 --- a/lib/pleroma/emails/user_email.ex +++ b/lib/pleroma/emails/user_email.ex @@ -5,7 +5,7 @@ defmodule Pleroma.Emails.UserEmail do @moduledoc "User emails" - import Swoosh.Email + use Phoenix.Swoosh, view: Pleroma.Web.EmailView, layout: {Pleroma.Web.LayoutView, :email} alias Pleroma.Web.Endpoint alias Pleroma.Web.Router @@ -92,4 +92,61 @@ def account_confirmation_email(user) do |> subject("#{instance_name()} account confirmation") |> html_body(html_body) end + + @doc """ + Email used in digest email notifications + Includes Mentions and New Followers data + If there are no mentions (even when new followers exist), the function will return nil + """ + @spec digest_email(Pleroma.User.t()) :: Swoosh.Email.t() | nil + def digest_email(user) do + new_notifications = + Pleroma.Notification.for_user_since(user, user.last_digest_emailed_at) + |> Enum.reduce(%{followers: [], mentions: []}, fn + %{activity: %{data: %{"type" => "Create"}, actor: actor}} = notification, acc -> + new_mention = %{data: notification, from: Pleroma.User.get_by_ap_id(actor)} + %{acc | mentions: [new_mention | acc.mentions]} + + %{activity: %{data: %{"type" => "Follow"}, actor: actor}} = notification, acc -> + new_follower = %{data: notification, from: Pleroma.User.get_by_ap_id(actor)} + %{acc | followers: [new_follower | acc.followers]} + + _, acc -> + acc + end) + + with [_ | _] = mentions <- new_notifications.mentions do + html_data = %{ + instance: instance_name(), + user: user, + mentions: mentions, + followers: new_notifications.followers, + unsubscribe_link: unsubscribe_url(user, "digest") + } + + new() + |> to(recipient(user)) + |> from(sender()) + |> subject("Your digest from #{instance_name()}") + |> render_body("digest.html", html_data) + else + _ -> + nil + end + end + + @doc """ + Generate unsubscribe link for given user and notifications type. + The link contains JWT token with the data, and subscription can be modified without + authorization. + """ + @spec unsubscribe_url(Pleroma.User.t(), String.t()) :: String.t() + def unsubscribe_url(user, notifications_type) do + token = + %{"sub" => user.id, "act" => %{"unsubscribe" => notifications_type}, "exp" => false} + |> Pleroma.JWT.generate_and_sign!() + |> Base.encode64() + + Router.Helpers.subscription_url(Pleroma.Web.Endpoint, :unsubscribe, token) + end end diff --git a/lib/pleroma/jwt.ex b/lib/pleroma/jwt.ex new file mode 100644 index 000000000..10102ff5d --- /dev/null +++ b/lib/pleroma/jwt.ex @@ -0,0 +1,9 @@ +defmodule Pleroma.JWT do + use Joken.Config + + @impl true + def token_config do + default_claims(skip: [:aud]) + |> add_claim("aud", &Pleroma.Web.Endpoint.url/0, &(&1 == Pleroma.Web.Endpoint.url())) + end +end diff --git a/lib/pleroma/quantum_scheduler.ex b/lib/pleroma/quantum_scheduler.ex new file mode 100644 index 000000000..9a3df81f6 --- /dev/null +++ b/lib/pleroma/quantum_scheduler.ex @@ -0,0 +1,4 @@ +defmodule Pleroma.QuantumScheduler do + use Quantum.Scheduler, + otp_app: :pleroma +end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 7053dfaf3..2509d2366 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1484,4 +1484,40 @@ def list_inactive_users_query(inactivity_threshold \\ 7) do is_nil(max(a.inserted_at)) ) end + + @doc """ + Enable or disable email notifications for user + + ## Examples + + iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => false}}}, "digest", true) + Pleroma.User{info: %{email_notifications: %{"digest" => true}}} + + iex> Pleroma.User.switch_email_notifications(Pleroma.User{info: %{email_notifications: %{"digest" => true}}}, "digest", false) + Pleroma.User{info: %{email_notifications: %{"digest" => false}}} + """ + @spec switch_email_notifications(t(), String.t(), boolean()) :: + {:ok, t()} | {:error, Ecto.Changeset.t()} + def switch_email_notifications(user, type, status) do + info = Pleroma.User.Info.update_email_notifications(user.info, %{type => status}) + + change(user) + |> put_embed(:info, info) + |> update_and_set_cache() + end + + @doc """ + Set `last_digest_emailed_at` value for the user to current time + """ + @spec touch_last_digest_emailed_at(t()) :: t() + def touch_last_digest_emailed_at(user) do + now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) + + {:ok, updated_user} = + user + |> change(%{last_digest_emailed_at: now}) + |> update_and_set_cache() + + updated_user + end end diff --git a/lib/pleroma/web/mailer/subscription_controller.ex b/lib/pleroma/web/mailer/subscription_controller.ex new file mode 100644 index 000000000..2334ebacb --- /dev/null +++ b/lib/pleroma/web/mailer/subscription_controller.ex @@ -0,0 +1,18 @@ +defmodule Pleroma.Web.Mailer.SubscriptionController do + use Pleroma.Web, :controller + + alias Pleroma.{JWT, Repo, User} + + def unsubscribe(conn, %{"token" => encoded_token}) do + with {:ok, token} <- Base.decode64(encoded_token), + {:ok, claims} <- JWT.verify_and_validate(token), + %{"act" => %{"unsubscribe" => type}, "sub" => uid} <- claims, + %User{} = user <- Repo.get(User, uid), + {:ok, _user} <- User.switch_email_notifications(user, type, false) do + render(conn, "unsubscribe_success.html", email: user.email) + else + _err -> + render(conn, "unsubscribe_failure.html") + end + end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 8b665d61b..09e51e602 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -562,6 +562,8 @@ defmodule Pleroma.Web.Router do post("/push/hub/:nickname", Websub.WebsubController, :websub_subscription_request) get("/push/subscriptions/:id", Websub.WebsubController, :websub_subscription_confirmation) post("/push/subscriptions/:id", Websub.WebsubController, :websub_incoming) + + get("/mailer/unsubscribe/:token", Mailer.SubscriptionController, :unsubscribe) end scope "/", Pleroma.Web do diff --git a/lib/pleroma/web/templates/email/digest.html.eex b/lib/pleroma/web/templates/email/digest.html.eex new file mode 100644 index 000000000..93c9c884f --- /dev/null +++ b/lib/pleroma/web/templates/email/digest.html.eex @@ -0,0 +1,20 @@ +

Hey <%= @user.nickname %>, here is what you've missed!

+ +

New Mentions:

+
    +<%= for %{data: mention, from: from} <- @mentions do %> +
  • <%= link from.nickname, to: mention.activity.actor %>: <%= raw mention.activity.object.data["content"] %>
  • +<% end %> +
+ +<%= if @followers != [] do %> +

<%= length(@followers) %> New Followers:

+
    +<%= for %{data: follow, from: from} <- @followers do %> +
  • <%= link from.nickname, to: follow.activity.actor %>
  • +<% end %> +
+<% end %> + +

You have received this email because you have signed up to receive digest emails from <%= @instance %> Pleroma instance.

+

The email address you are subscribed as is <%= @user.email %>. To unsubscribe, please go <%= link "here", to: @unsubscribe_link %>.

\ No newline at end of file diff --git a/lib/pleroma/web/templates/layout/email.html.eex b/lib/pleroma/web/templates/layout/email.html.eex new file mode 100644 index 000000000..f6dcd7f0f --- /dev/null +++ b/lib/pleroma/web/templates/layout/email.html.eex @@ -0,0 +1,10 @@ + + + + + <%= @email.subject %> + + + <%= render @view_module, @view_template, assigns %> + + \ No newline at end of file diff --git a/lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex b/lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex new file mode 100644 index 000000000..7b476f02d --- /dev/null +++ b/lib/pleroma/web/templates/mailer/subscription/unsubscribe_failure.html.eex @@ -0,0 +1 @@ +

UNSUBSCRIBE FAILURE

diff --git a/lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex b/lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex new file mode 100644 index 000000000..6dfa2c185 --- /dev/null +++ b/lib/pleroma/web/templates/mailer/subscription/unsubscribe_success.html.eex @@ -0,0 +1 @@ +

UNSUBSCRIBE SUCCESSFUL

diff --git a/lib/pleroma/web/views/email_view.ex b/lib/pleroma/web/views/email_view.ex new file mode 100644 index 000000000..b63eb162c --- /dev/null +++ b/lib/pleroma/web/views/email_view.ex @@ -0,0 +1,5 @@ +defmodule Pleroma.Web.EmailView do + use Pleroma.Web, :view + import Phoenix.HTML + import Phoenix.HTML.Link +end diff --git a/lib/pleroma/web/views/mailer/subscription_view.ex b/lib/pleroma/web/views/mailer/subscription_view.ex new file mode 100644 index 000000000..fc3d20816 --- /dev/null +++ b/lib/pleroma/web/views/mailer/subscription_view.ex @@ -0,0 +1,3 @@ +defmodule Pleroma.Web.Mailer.SubscriptionView do + use Pleroma.Web, :view +end diff --git a/mix.exs b/mix.exs index da2e284f8..6bb105538 100644 --- a/mix.exs +++ b/mix.exs @@ -93,6 +93,7 @@ defp deps do {:ex_doc, "~> 0.20.2", only: :dev, runtime: false}, {:web_push_encryption, "~> 0.2.1"}, {:swoosh, "~> 0.20"}, + {:phoenix_swoosh, "~> 0.2"}, {:gen_smtp, "~> 0.13"}, {:websocket_client, git: "https://github.com/jeremyong/websocket_client.git", only: :test}, {:floki, "~> 0.20.0"}, @@ -111,7 +112,8 @@ defp deps do {:prometheus_process_collector, "~> 1.4"}, {:recon, github: "ferd/recon", tag: "2.4.0"}, {:quack, "~> 0.1.1"}, - {:quantum, "~> 2.3"} + {:quantum, "~> 2.3"}, + {:joken, "~> 2.0"} ] ++ oauth_deps end diff --git a/mix.lock b/mix.lock index 6e322240a..73aed012f 100644 --- a/mix.lock +++ b/mix.lock @@ -37,6 +37,7 @@ "httpoison": {:hex, :httpoison, "1.2.0", "2702ed3da5fd7a8130fc34b11965c8cfa21ade2f232c00b42d96d4967c39a3a3", [:mix], [{:hackney, "~> 1.8", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm"}, "idna": {:hex, :idna, "6.0.0", "689c46cbcdf3524c44d5f3dde8001f364cd7608a99556d8fbd8239a5798d4c10", [:rebar3], [{:unicode_util_compat, "0.4.1", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm"}, "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, + "joken": {:hex, :joken, "2.0.1", "ec9ab31bf660f343380da033b3316855197c8d4c6ef597fa3fcb451b326beb14", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm"}, "jose": {:hex, :jose, "1.9.0", "4167c5f6d06ffaebffd15cdb8da61a108445ef5e85ab8f5a7ad926fdf3ada154", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"}, "libring": {:hex, :libring, "1.4.0", "41246ba2f3fbc76b3971f6bce83119dfec1eee17e977a48d8a9cfaaf58c2a8d6", [:mix], [], "hexpm"}, "makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, @@ -55,6 +56,7 @@ "phoenix_ecto": {:hex, :phoenix_ecto, "4.0.0", "c43117a136e7399ea04ecaac73f8f23ee0ffe3e07acfcb8062fe5f4c9f0f6531", [:mix], [{:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.9", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "phoenix_html": {:hex, :phoenix_html, "2.13.2", "f5d27c9b10ce881a60177d2b5227314fc60881e6b66b41dfe3349db6ed06cf57", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm"}, + "phoenix_swoosh": {:hex, :phoenix_swoosh, "0.2.0", "a7e0b32077cd6d2323ae15198839b05d9caddfa20663fd85787479e81f89520e", [:mix], [{:phoenix, "~> 1.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.2", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:swoosh, "~> 0.1", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm"}, "pleroma_job_queue": {:hex, :pleroma_job_queue, "0.2.0", "879e660aa1cebe8dc6f0aaaa6aa48b4875e89cd961d4a585fd128e0773b31a18", [:mix], [], "hexpm"}, "plug": {:hex, :plug, "1.7.2", "d7b7db7fbd755e8283b6c0a50be71ec0a3d67d9213d74422d9372effc8e87fd1", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}], "hexpm"}, "plug_cowboy": {:hex, :plug_cowboy, "2.0.2", "6055f16868cc4882b24b6e1d63d2bada94fb4978413377a3b32ac16c18dffba2", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, From 05cdb2f2389376081973d96b32e876d2a032d1f1 Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Sat, 20 Apr 2019 19:42:50 +0700 Subject: [PATCH 08/66] Do not track coverage files --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 774893b35..8166e65e9 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,5 @@ erl_crash.dump # Prevent committing docs files /priv/static/doc/* + +/cover From 724311e15177a1a97f533f11ff17d8d0146800ef Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Sat, 20 Apr 2019 19:57:43 +0700 Subject: [PATCH 09/66] Fix Credo warnings --- lib/pleroma/application.ex | 2 +- lib/pleroma/digest_email_worker.ex | 4 ++-- lib/pleroma/web/mailer/subscription_controller.ex | 4 +++- mix.exs | 2 +- test/notification_test.exs | 2 +- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 76f8d9bcd..299f8807b 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -187,7 +187,7 @@ defp hackney_pool_children do end end - defp after_supervisor_start() do + defp after_supervisor_start do with digest_config <- Application.get_env(:pleroma, :email_notifications)[:digest], true <- digest_config[:active], %Crontab.CronExpression{} = schedule <- diff --git a/lib/pleroma/digest_email_worker.ex b/lib/pleroma/digest_email_worker.ex index fa6067a03..7be470f5f 100644 --- a/lib/pleroma/digest_email_worker.ex +++ b/lib/pleroma/digest_email_worker.ex @@ -4,7 +4,7 @@ defmodule Pleroma.DigestEmailWorker do # alias Pleroma.User - def run() do + def run do Logger.warn("Running digester") config = Application.get_env(:pleroma, :email_notifications)[:digest] negative_interval = -Map.fetch!(config, :interval) @@ -14,7 +14,7 @@ def run() do now = NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second) from(u in inactive_users_query, - where: fragment("? #> '{\"email_notifications\",\"digest\"}' @> 'true'", u.info), + where: fragment(~s(? #> '{"email_notifications","digest"}' @> 'true'), u.info), where: u.last_digest_emailed_at < datetime_add(^now, ^negative_interval, "day"), select: u ) diff --git a/lib/pleroma/web/mailer/subscription_controller.ex b/lib/pleroma/web/mailer/subscription_controller.ex index 2334ebacb..478a83518 100644 --- a/lib/pleroma/web/mailer/subscription_controller.ex +++ b/lib/pleroma/web/mailer/subscription_controller.ex @@ -1,7 +1,9 @@ defmodule Pleroma.Web.Mailer.SubscriptionController do use Pleroma.Web, :controller - alias Pleroma.{JWT, Repo, User} + alias Pleroma.JWT + alias Pleroma.Repo + alias Pleroma.User def unsubscribe(conn, %{"token" => encoded_token}) do with {:ok, token} <- Base.decode64(encoded_token), diff --git a/mix.exs b/mix.exs index 6bb105538..2cdfb1392 100644 --- a/mix.exs +++ b/mix.exs @@ -8,7 +8,7 @@ def project do elixir: "~> 1.7", elixirc_paths: elixirc_paths(Mix.env()), compilers: [:phoenix, :gettext] ++ Mix.compilers(), - # elixirc_options: [warnings_as_errors: true], + elixirc_options: [warnings_as_errors: true], xref: [exclude: [:eldap]], start_permanent: Mix.env() == :prod, aliases: aliases(), diff --git a/test/notification_test.exs b/test/notification_test.exs index 462398d75..3bbce8fcf 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -335,7 +335,7 @@ test "Returns recent notifications" do recent_notifications_ids = user2 |> Notification.for_user_since( - NaiveDateTime.add(NaiveDateTime.utc_now(), -5 * 86400, :second) + NaiveDateTime.add(NaiveDateTime.utc_now(), -5 * 86_400, :second) ) |> Enum.map(& &1.id) From 2359ee38b38de17df4dfe9cbdfe551bb7d9a034d Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Sun, 21 Apr 2019 16:36:25 +0700 Subject: [PATCH 10/66] Set digest emails to false by default --- lib/pleroma/user/info.ex | 2 +- priv/repo/migrations/20190412052952_add_user_info_fields.exs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index 194dd5581..d827293b8 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -42,7 +42,7 @@ defmodule Pleroma.User.Info do field(:hide_follows, :boolean, default: false) field(:pinned_activities, {:array, :string}, default: []) field(:flavour, :string, default: nil) - field(:email_notifications, :map, default: %{"digest" => true}) + field(:email_notifications, :map, default: %{"digest" => false}) field(:notification_settings, :map, default: %{"remote" => true, "local" => true, "followers" => true, "follows" => true} diff --git a/priv/repo/migrations/20190412052952_add_user_info_fields.exs b/priv/repo/migrations/20190412052952_add_user_info_fields.exs index 203d0fc3b..646c91f32 100644 --- a/priv/repo/migrations/20190412052952_add_user_info_fields.exs +++ b/priv/repo/migrations/20190412052952_add_user_info_fields.exs @@ -6,7 +6,7 @@ def up do UPDATE users SET info = info || '{ \"email_notifications\": { - \"digest\": true + \"digest\": false } }'") end From f1d90ee94206db00025d41b13a2906aa30d748f0 Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Sun, 21 Apr 2019 16:40:05 +0700 Subject: [PATCH 11/66] Remove debug code --- lib/pleroma/digest_email_worker.ex | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/lib/pleroma/digest_email_worker.ex b/lib/pleroma/digest_email_worker.ex index 7be470f5f..65013f77e 100644 --- a/lib/pleroma/digest_email_worker.ex +++ b/lib/pleroma/digest_email_worker.ex @@ -1,11 +1,7 @@ defmodule Pleroma.DigestEmailWorker do import Ecto.Query - require Logger - - # alias Pleroma.User def run do - Logger.warn("Running digester") config = Application.get_env(:pleroma, :email_notifications)[:digest] negative_interval = -Map.fetch!(config, :interval) inactivity_threshold = Map.fetch!(config, :inactivity_threshold) @@ -19,23 +15,14 @@ def run do select: u ) |> Pleroma.Repo.all() - |> run(:pre) - end - - defp run(v, :pre) do - Logger.warn("Running for #{length(v)} users") - run(v) + |> run() end defp run([]), do: :ok defp run([user | users]) do with %Swoosh.Email{} = email <- Pleroma.Emails.UserEmail.digest_email(user) do - Logger.warn("Sending to #{user.nickname}") Pleroma.Emails.Mailer.deliver_async(email) - else - _ -> - Logger.warn("Skipping #{user.nickname}") end Pleroma.User.touch_last_digest_emailed_at(user) From b87ad13803df59d88feb736c3d0ff9cf514989d7 Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Sun, 21 Apr 2019 19:36:31 +0700 Subject: [PATCH 12/66] Move comments for email_notifications config to docs --- config/config.exs | 5 ----- docs/config.md | 12 ++++++++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/config/config.exs b/config/config.exs index 2663b1ebd..b1d506b59 100644 --- a/config/config.exs +++ b/config/config.exs @@ -468,14 +468,9 @@ config :pleroma, :email_notifications, digest: %{ - # Globally enable or disable digest emails active: true, - # When to send digest email, in crontab format (https://en.wikipedia.org/wiki/Cron) - # 0 0 * * 0 - once a week at midnight on Sunday morning schedule: "0 0 * * 0", - # Minimum interval between digest emails to one user interval: 7, - # Minimum user inactivity threshold inactivity_threshold: 7 } diff --git a/docs/config.md b/docs/config.md index 5a97033b2..69d389382 100644 --- a/docs/config.md +++ b/docs/config.md @@ -435,6 +435,18 @@ Authentication / authorization settings. * `oauth_consumer_template`: OAuth consumer mode authentication form template. By default it's `consumer.html` which corresponds to `lib/pleroma/web/templates/o_auth/o_auth/consumer.html.eex`. * `oauth_consumer_strategies`: the list of enabled OAuth consumer strategies; by default it's set by OAUTH_CONSUMER_STRATEGIES environment variable. +## :email_notifications + +Email notifications settings. + + - digest - emails of "what you've missed" for users who have been + inactive for a while. + - active: globally enable or disable digest emails + - schedule: When to send digest email, in [crontab format](https://en.wikipedia.org/wiki/Cron). + "0 0 * * 0" is the default, meaning "once a week at midnight on Sunday morning" + - interval: Minimum interval between digest emails to one user + - inactivity_threshold: Minimum user inactivity threshold + # OAuth consumer mode OAuth consumer mode allows sign in / sign up via external OAuth providers (e.g. Twitter, Facebook, Google, Microsoft, etc.). From 5cee2fe9fea4f0c98acd49a2a288ecd44bce3d1f Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Wed, 29 May 2019 21:31:27 +0300 Subject: [PATCH 13/66] Replace Application.get_env/2 with Pleroma.Config.get/1 --- lib/pleroma/digest_email_worker.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/digest_email_worker.ex b/lib/pleroma/digest_email_worker.ex index 65013f77e..f7b3d81cd 100644 --- a/lib/pleroma/digest_email_worker.ex +++ b/lib/pleroma/digest_email_worker.ex @@ -2,7 +2,7 @@ defmodule Pleroma.DigestEmailWorker do import Ecto.Query def run do - config = Application.get_env(:pleroma, :email_notifications)[:digest] + config = Pleroma.Config.get([:email_notifications, :digest]) negative_interval = -Map.fetch!(config, :interval) inactivity_threshold = Map.fetch!(config, :inactivity_threshold) inactive_users_query = Pleroma.User.list_inactive_users_query(inactivity_threshold) From 3e1761058711b12fa995f2b43117fb90ca40c9ad Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Tue, 4 Jun 2019 02:48:21 +0300 Subject: [PATCH 14/66] Add task to test emails --- lib/mix/tasks/pleroma/digest.ex | 34 +++++++++++++++ lib/pleroma/digest_email_worker.ex | 4 +- lib/pleroma/emails/user_email.ex | 20 +++++++-- .../web/templates/email/digest.html.eex | 4 +- test/mix/tasks/pleroma.digest_test.exs | 42 +++++++++++++++++++ 5 files changed, 96 insertions(+), 8 deletions(-) create mode 100644 lib/mix/tasks/pleroma/digest.ex create mode 100644 test/mix/tasks/pleroma.digest_test.exs diff --git a/lib/mix/tasks/pleroma/digest.ex b/lib/mix/tasks/pleroma/digest.ex new file mode 100644 index 000000000..7ac3df5c7 --- /dev/null +++ b/lib/mix/tasks/pleroma/digest.ex @@ -0,0 +1,34 @@ +defmodule Mix.Tasks.Pleroma.Digest do + use Mix.Task + alias Mix.Tasks.Pleroma.Common + + @shortdoc "Manages digest emails" + @moduledoc """ + Manages digest emails + + ## Send digest email since given date (user registration date by default) + ignoring user activity status. + + ``mix pleroma.digest test `` + + Example: ``mix pleroma.digest test donaldtheduck 2019-05-20`` + """ + def run(["test", nickname | opts]) do + Common.start_pleroma() + + user = Pleroma.User.get_by_nickname(nickname) + + last_digest_emailed_at = + with [date] <- opts, + {:ok, datetime} <- Timex.parse(date, "{YYYY}-{0M}-{0D}") do + datetime + else + _ -> user.inserted_at + end + + patched_user = %{user | last_digest_emailed_at: last_digest_emailed_at} + + :ok = Pleroma.DigestEmailWorker.run([patched_user]) + Mix.shell().info("Digest email have been sent to #{nickname} (#{user.email})") + end +end diff --git a/lib/pleroma/digest_email_worker.ex b/lib/pleroma/digest_email_worker.ex index f7b3d81cd..8c28dca18 100644 --- a/lib/pleroma/digest_email_worker.ex +++ b/lib/pleroma/digest_email_worker.ex @@ -18,9 +18,9 @@ def run do |> run() end - defp run([]), do: :ok + def run([]), do: :ok - defp run([user | users]) do + def run([user | users]) do with %Swoosh.Email{} = email <- Pleroma.Emails.UserEmail.digest_email(user) do Pleroma.Emails.Mailer.deliver_async(email) end diff --git a/lib/pleroma/emails/user_email.ex b/lib/pleroma/emails/user_email.ex index 64f855112..0ad0aed40 100644 --- a/lib/pleroma/emails/user_email.ex +++ b/lib/pleroma/emails/user_email.ex @@ -103,12 +103,24 @@ def digest_email(user) do new_notifications = Pleroma.Notification.for_user_since(user, user.last_digest_emailed_at) |> Enum.reduce(%{followers: [], mentions: []}, fn - %{activity: %{data: %{"type" => "Create"}, actor: actor}} = notification, acc -> - new_mention = %{data: notification, from: Pleroma.User.get_by_ap_id(actor)} + %{activity: %{data: %{"type" => "Create"}, actor: actor} = activity} = notification, + acc -> + new_mention = %{ + data: notification, + object: Pleroma.Object.normalize(activity), + from: Pleroma.User.get_by_ap_id(actor) + } + %{acc | mentions: [new_mention | acc.mentions]} - %{activity: %{data: %{"type" => "Follow"}, actor: actor}} = notification, acc -> - new_follower = %{data: notification, from: Pleroma.User.get_by_ap_id(actor)} + %{activity: %{data: %{"type" => "Follow"}, actor: actor} = activity} = notification, + acc -> + new_follower = %{ + data: notification, + object: Pleroma.Object.normalize(activity), + from: Pleroma.User.get_by_ap_id(actor) + } + %{acc | followers: [new_follower | acc.followers]} _, acc -> diff --git a/lib/pleroma/web/templates/email/digest.html.eex b/lib/pleroma/web/templates/email/digest.html.eex index 93c9c884f..c9dd699fd 100644 --- a/lib/pleroma/web/templates/email/digest.html.eex +++ b/lib/pleroma/web/templates/email/digest.html.eex @@ -2,8 +2,8 @@

New Mentions:

    -<%= for %{data: mention, from: from} <- @mentions do %> -
  • <%= link from.nickname, to: mention.activity.actor %>: <%= raw mention.activity.object.data["content"] %>
  • +<%= for %{data: mention, object: object, from: from} <- @mentions do %> +
  • <%= link from.nickname, to: mention.activity.actor %>: <%= raw object.data["content"] %>
  • <% end %>
diff --git a/test/mix/tasks/pleroma.digest_test.exs b/test/mix/tasks/pleroma.digest_test.exs new file mode 100644 index 000000000..1a54ac35b --- /dev/null +++ b/test/mix/tasks/pleroma.digest_test.exs @@ -0,0 +1,42 @@ +defmodule Mix.Tasks.Pleroma.DigestTest do + use Pleroma.DataCase + + import Pleroma.Factory + import Swoosh.TestAssertions + + alias Pleroma.Web.CommonAPI + + setup_all do + Mix.shell(Mix.Shell.Process) + + on_exit(fn -> + Mix.shell(Mix.Shell.IO) + end) + + :ok + end + + describe "pleroma.digest test" do + test "Sends digest to the given user" do + user1 = insert(:user) + user2 = insert(:user) + + Enum.each(0..10, fn i -> + {:ok, _activity} = + CommonAPI.post(user1, %{ + "status" => "hey ##{i} @#{user2.nickname}!" + }) + end) + + Mix.Tasks.Pleroma.Digest.run(["test", user2.nickname]) + + assert_email_sent( + to: {user2.name, user2.email}, + html_body: ~r/new mentions:/i + ) + + assert_received {:mix_shell, :info, [message]} + assert message =~ "Digest email have been sent" + end + end +end From bd325132ca337c57f63b6443ae44748d9a422f65 Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Tue, 4 Jun 2019 03:07:49 +0300 Subject: [PATCH 15/66] Fix tests --- config/test.exs | 2 ++ test/mix/tasks/pleroma.digest_test.exs | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/config/test.exs b/config/test.exs index 41cddb9bd..adb874223 100644 --- a/config/test.exs +++ b/config/test.exs @@ -67,6 +67,8 @@ config :pleroma, :database, rum_enabled: rum_enabled IO.puts("RUM enabled: #{rum_enabled}") +config :joken, default_signer: "yU8uHKq+yyAkZ11Hx//jcdacWc8yQ1bxAAGrplzB0Zwwjkp35v0RK9SO8WTPr6QZ" + try do import_config "test.secret.exs" rescue diff --git a/test/mix/tasks/pleroma.digest_test.exs b/test/mix/tasks/pleroma.digest_test.exs index 1a54ac35b..3dafe05fe 100644 --- a/test/mix/tasks/pleroma.digest_test.exs +++ b/test/mix/tasks/pleroma.digest_test.exs @@ -30,13 +30,13 @@ test "Sends digest to the given user" do Mix.Tasks.Pleroma.Digest.run(["test", user2.nickname]) + assert_received {:mix_shell, :info, [message]} + assert message =~ "Digest email have been sent" + assert_email_sent( to: {user2.name, user2.email}, html_body: ~r/new mentions:/i ) - - assert_received {:mix_shell, :info, [message]} - assert message =~ "Digest email have been sent" end end end From 7718a215e9b20471169bf2474771a4fe486a3050 Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Tue, 4 Jun 2019 03:08:00 +0300 Subject: [PATCH 16/66] Update changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2144bbe28..fef10463a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [unreleased] ### Added +- Digest email for inactive users - Optional SSH access mode. (Needs `erlang-ssh` package on some distributions). - [MongooseIM](https://github.com/esl/MongooseIM) http authentication support. - LDAP authentication @@ -25,6 +26,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Configuration: `notify_email` option - Configuration: Media proxy `whitelist` option - Configuration: `report_uri` option +- Configuration: `email_notifications` option - Pleroma API: User subscriptions - Pleroma API: Healthcheck endpoint - Pleroma API: `/api/v1/pleroma/mascot` per-user frontend mascot configuration endpoints From f6036ce3b9649902ce1c2af819616ad25f0caef1 Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Tue, 4 Jun 2019 03:38:53 +0300 Subject: [PATCH 17/66] Fix tests --- test/mix/tasks/pleroma.digest_test.exs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/test/mix/tasks/pleroma.digest_test.exs b/test/mix/tasks/pleroma.digest_test.exs index 3dafe05fe..595f64ed7 100644 --- a/test/mix/tasks/pleroma.digest_test.exs +++ b/test/mix/tasks/pleroma.digest_test.exs @@ -28,9 +28,18 @@ test "Sends digest to the given user" do }) end) - Mix.Tasks.Pleroma.Digest.run(["test", user2.nickname]) + yesterday = + NaiveDateTime.add( + NaiveDateTime.truncate(NaiveDateTime.utc_now(), :second), + -60 * 60 * 24, + :second + ) - assert_received {:mix_shell, :info, [message]} + {:ok, yesterday_date} = Timex.format(yesterday, "%F", :strftime) + + :ok = Mix.Tasks.Pleroma.Digest.run(["test", user2.nickname, yesterday_date]) + + assert_receive {:mix_shell, :info, [message]} assert message =~ "Digest email have been sent" assert_email_sent( From c0fa0001476a8a45878a0c75125627164497eddf Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Fri, 7 Jun 2019 01:22:35 +0300 Subject: [PATCH 18/66] Set default config for digest to false --- config/config.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.exs b/config/config.exs index 5a05ee043..509f4b081 100644 --- a/config/config.exs +++ b/config/config.exs @@ -494,7 +494,7 @@ config :pleroma, :email_notifications, digest: %{ - active: true, + active: false, schedule: "0 0 * * 0", interval: 7, inactivity_threshold: 7 From 0384459ce552c50edb582413808a099086b6495e Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Fri, 12 Jul 2019 18:16:54 +0300 Subject: [PATCH 19/66] Update mix.lock --- mix.lock | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mix.lock b/mix.lock index 654972ddd..a09022acb 100644 --- a/mix.lock +++ b/mix.lock @@ -35,6 +35,8 @@ "excoveralls": {:hex, :excoveralls, "0.11.1", "dd677fbdd49114fdbdbf445540ec735808250d56b011077798316505064edb2c", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, "floki": {:hex, :floki, "0.20.4", "be42ac911fece24b4c72f3b5846774b6e61b83fe685c2fc9d62093277fb3bc86", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}, {:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"}, "gen_smtp": {:hex, :gen_smtp, "0.14.0", "39846a03522456077c6429b4badfd1d55e5e7d0fdfb65e935b7c5e38549d9202", [:rebar3], [], "hexpm"}, + "gen_stage": {:hex, :gen_stage, "0.14.2", "6a2a578a510c5bfca8a45e6b27552f613b41cf584b58210f017088d3d17d0b14", [:mix], [], "hexpm"}, + "gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"}, "gettext": {:hex, :gettext, "0.17.0", "abe21542c831887a2b16f4c94556db9c421ab301aee417b7c4fbde7fbdbe01ec", [:mix], [], "hexpm"}, "hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, "html_entities": {:hex, :html_entities, "0.4.0", "f2fee876858cf6aaa9db608820a3209e45a087c5177332799592142b50e89a6b", [:mix], [], "hexpm"}, @@ -83,6 +85,7 @@ "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"}, "recon": {:git, "https://github.com/ferd/recon.git", "75d70c7c08926d2f24f1ee6de14ee50fe8a52763", [tag: "2.4.0"]}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"}, + "swarm": {:hex, :swarm, "3.4.0", "64f8b30055d74640d2186c66354b33b999438692a91be275bb89cdc7e401f448", [:mix], [{:gen_state_machine, "~> 2.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}, {:libring, "~> 1.0", [hex: :libring, repo: "hexpm", optional: false]}], "hexpm"}, "swoosh": {:hex, :swoosh, "0.23.2", "7dda95ff0bf54a2298328d6899c74dae1223777b43563ccebebb4b5d2b61df38", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"}, "syslog": {:git, "https://github.com/Vagabond/erlang-syslog.git", "4a6c6f2c996483e86c1320e9553f91d337bcb6aa", [tag: "1.0.5"]}, "telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"}, From e8fa477793e1395664f79d572800f11994cdd38d Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sat, 13 Jul 2019 19:17:57 +0300 Subject: [PATCH 20/66] 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 21/66] 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 22/66] 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 23/66] 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 24/66] 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 168dc97c37f274b258b04eb7e883640b84259714 Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Sun, 14 Jul 2019 22:04:55 +0300 Subject: [PATCH 25/66] Make opts optional in Pleroma.Notification.for_user_query/2 --- lib/pleroma/notification.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index f680fe049..04bbfa0df 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -33,7 +33,7 @@ def changeset(%Notification{} = notification, attrs) do |> cast(attrs, [:seen]) end - def for_user_query(user, opts) do + def for_user_query(user, opts \\ []) do query = Notification |> where(user_id: ^user.id) From b052a9d4d0323eb64c0a741a499906659a674244 Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Sun, 14 Jul 2019 22:32:11 +0300 Subject: [PATCH 26/66] Update DigestEmailWorker to compile and send emails via queue --- lib/mix/tasks/pleroma/digest.ex | 2 +- lib/pleroma/digest_email_worker.ex | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/mix/tasks/pleroma/digest.ex b/lib/mix/tasks/pleroma/digest.ex index 19c4ce71e..81c207e10 100644 --- a/lib/mix/tasks/pleroma/digest.ex +++ b/lib/mix/tasks/pleroma/digest.ex @@ -27,7 +27,7 @@ def run(["test", nickname | opts]) do patched_user = %{user | last_digest_emailed_at: last_digest_emailed_at} - :ok = Pleroma.DigestEmailWorker.run([patched_user]) + _user = Pleroma.DigestEmailWorker.perform(patched_user) Mix.shell().info("Digest email have been sent to #{nickname} (#{user.email})") end end diff --git a/lib/pleroma/digest_email_worker.ex b/lib/pleroma/digest_email_worker.ex index 8c28dca18..adc24797f 100644 --- a/lib/pleroma/digest_email_worker.ex +++ b/lib/pleroma/digest_email_worker.ex @@ -1,6 +1,8 @@ defmodule Pleroma.DigestEmailWorker do import Ecto.Query + @queue_name :digest_emails + def run do config = Pleroma.Config.get([:email_notifications, :digest]) negative_interval = -Map.fetch!(config, :interval) @@ -15,18 +17,19 @@ def run do select: u ) |> Pleroma.Repo.all() - |> run() + |> Enum.each(&PleromaJobQueue.enqueue(@queue_name, __MODULE__, [&1])) end - def run([]), do: :ok - - def run([user | users]) do + @doc """ + Send digest email to the given user. + Updates `last_digest_emailed_at` field for the user and returns the updated user. + """ + @spec perform(Pleroma.User.t()) :: Pleroma.User.t() + def perform(user) do with %Swoosh.Email{} = email <- Pleroma.Emails.UserEmail.digest_email(user) do Pleroma.Emails.Mailer.deliver_async(email) end Pleroma.User.touch_last_digest_emailed_at(user) - - run(users) end end From e7c175c943e9e3f53df76d812c09cfeffdb1c56b Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Tue, 16 Jul 2019 16:49:50 +0300 Subject: [PATCH 27/66] Use PleromaJobQueue for scheduling --- lib/pleroma/application.ex | 18 ++++++------------ lib/pleroma/digest_email_worker.ex | 2 +- lib/pleroma/quantum_scheduler.ex | 4 ---- mix.exs | 4 ++-- mix.lock | 11 ++--------- 5 files changed, 11 insertions(+), 28 deletions(-) delete mode 100644 lib/pleroma/quantum_scheduler.ex diff --git a/lib/pleroma/application.ex b/lib/pleroma/application.ex index 29cd14477..7df6bc9ae 100644 --- a/lib/pleroma/application.ex +++ b/lib/pleroma/application.ex @@ -115,10 +115,6 @@ def start(_type, _args) do %{ id: Pleroma.ScheduledActivityWorker, start: {Pleroma.ScheduledActivityWorker, :start_link, []} - }, - %{ - id: Pleroma.QuantumScheduler, - start: {Pleroma.QuantumScheduler, :start_link, []} } ] ++ hackney_pool_children() ++ @@ -231,14 +227,12 @@ defp hackney_pool_children do defp after_supervisor_start do with digest_config <- Application.get_env(:pleroma, :email_notifications)[:digest], - true <- digest_config[:active], - %Crontab.CronExpression{} = schedule <- - Crontab.CronExpression.Parser.parse!(digest_config[:schedule]) do - Pleroma.QuantumScheduler.new_job() - |> Quantum.Job.set_name(:digest_emails) - |> Quantum.Job.set_schedule(schedule) - |> Quantum.Job.set_task(&Pleroma.DigestEmailWorker.run/0) - |> Pleroma.QuantumScheduler.add_job() + true <- digest_config[:active] do + PleromaJobQueue.schedule( + digest_config[:schedule], + :digest_emails, + Pleroma.DigestEmailWorker + ) end :ok diff --git a/lib/pleroma/digest_email_worker.ex b/lib/pleroma/digest_email_worker.ex index adc24797f..18e67d39b 100644 --- a/lib/pleroma/digest_email_worker.ex +++ b/lib/pleroma/digest_email_worker.ex @@ -3,7 +3,7 @@ defmodule Pleroma.DigestEmailWorker do @queue_name :digest_emails - def run do + def perform do config = Pleroma.Config.get([:email_notifications, :digest]) negative_interval = -Map.fetch!(config, :interval) inactivity_threshold = Map.fetch!(config, :inactivity_threshold) diff --git a/lib/pleroma/quantum_scheduler.ex b/lib/pleroma/quantum_scheduler.ex deleted file mode 100644 index 9a3df81f6..000000000 --- a/lib/pleroma/quantum_scheduler.ex +++ /dev/null @@ -1,4 +0,0 @@ -defmodule Pleroma.QuantumScheduler do - use Quantum.Scheduler, - otp_app: :pleroma -end diff --git a/mix.exs b/mix.exs index a4f468726..25332deb9 100644 --- a/mix.exs +++ b/mix.exs @@ -140,7 +140,8 @@ defp deps do {:http_signatures, git: "https://git.pleroma.social/pleroma/http_signatures.git", ref: "9789401987096ead65646b52b5a2ca6bf52fc531"}, - {:pleroma_job_queue, "~> 0.2.0"}, + {:pleroma_job_queue, + git: "https://git.pleroma.social/pleroma/pleroma_job_queue.git", ref: "0637ccb1"}, {:telemetry, "~> 0.3"}, {:prometheus_ex, "~> 3.0"}, {:prometheus_plugs, "~> 1.1"}, @@ -148,7 +149,6 @@ defp deps do {:prometheus_ecto, "~> 1.4"}, {:recon, github: "ferd/recon", tag: "2.4.0"}, {:quack, "~> 0.1.1"}, - {:quantum, "~> 2.3"}, {:joken, "~> 2.0"}, {:benchee, "~> 1.0"}, {:esshd, "~> 0.1.0", runtime: Application.get_env(:esshd, :enabled, false)}, diff --git a/mix.lock b/mix.lock index 4f1214bca..3621d9c27 100644 --- a/mix.lock +++ b/mix.lock @@ -15,7 +15,7 @@ "cowboy": {:hex, :cowboy, "2.6.3", "99aa50e94e685557cad82e704457336a453d4abcb77839ad22dbe71f311fcc06", [:rebar3], [{:cowlib, "~> 2.7.3", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "~> 1.7.1", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm"}, "cowlib": {:hex, :cowlib, "2.7.3", "a7ffcd0917e6d50b4d5fb28e9e2085a0ceb3c97dea310505f7460ff5ed764ce9", [:rebar3], [], "hexpm"}, "credo": {:hex, :credo, "0.9.3", "76fa3e9e497ab282e0cf64b98a624aa11da702854c52c82db1bf24e54ab7c97a", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:poison, ">= 0.0.0", [hex: :poison, repo: "hexpm", optional: false]}], "hexpm"}, - "crontab": {:hex, :crontab, "1.1.5", "2c9439506ceb0e9045de75879e994b88d6f0be88bfe017d58cb356c66c4a5482", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, + "crontab": {:hex, :crontab, "1.1.7", "b9219f0bdc8678b94143655a8f229716c5810c0636a4489f98c0956137e53985", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm"}, "crypt": {:git, "https://github.com/msantos/crypt", "1f2b58927ab57e72910191a7ebaeff984382a1d3", [ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"]}, "db_connection": {:hex, :db_connection, "2.0.6", "bde2f85d047969c5b5800cb8f4b3ed6316c8cb11487afedac4aa5f93fd39abfa", [:mix], [{:connection, "~> 1.0.2", [hex: :connection, repo: "hexpm", optional: false]}], "hexpm"}, "decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [:mix], [], "hexpm"}, @@ -35,8 +35,6 @@ "excoveralls": {:hex, :excoveralls, "0.11.1", "dd677fbdd49114fdbdbf445540ec735808250d56b011077798316505064edb2c", [:mix], [{:hackney, "~> 1.0", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm"}, "floki": {:hex, :floki, "0.20.4", "be42ac911fece24b4c72f3b5846774b6e61b83fe685c2fc9d62093277fb3bc86", [:mix], [{:html_entities, "~> 0.4.0", [hex: :html_entities, repo: "hexpm", optional: false]}, {:mochiweb, "~> 2.15", [hex: :mochiweb, repo: "hexpm", optional: false]}], "hexpm"}, "gen_smtp": {:hex, :gen_smtp, "0.14.0", "39846a03522456077c6429b4badfd1d55e5e7d0fdfb65e935b7c5e38549d9202", [:rebar3], [], "hexpm"}, - "gen_stage": {:hex, :gen_stage, "0.14.2", "6a2a578a510c5bfca8a45e6b27552f613b41cf584b58210f017088d3d17d0b14", [:mix], [], "hexpm"}, - "gen_state_machine": {:hex, :gen_state_machine, "2.0.5", "9ac15ec6e66acac994cc442dcc2c6f9796cf380ec4b08267223014be1c728a95", [:mix], [], "hexpm"}, "gettext": {:hex, :gettext, "0.17.0", "abe21542c831887a2b16f4c94556db9c421ab301aee417b7c4fbde7fbdbe01ec", [:mix], [], "hexpm"}, "hackney": {:hex, :hackney, "1.15.1", "9f8f471c844b8ce395f7b6d8398139e26ddca9ebc171a8b91342ee15a19963f4", [:rebar3], [{:certifi, "2.5.1", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "6.0.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "1.0.1", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~>1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "1.1.4", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}], "hexpm"}, "html_entities": {:hex, :html_entities, "0.4.0", "f2fee876858cf6aaa9db608820a3209e45a087c5177332799592142b50e89a6b", [:mix], [], "hexpm"}, @@ -47,7 +45,6 @@ "jason": {:hex, :jason, "1.1.2", "b03dedea67a99223a2eaf9f1264ce37154564de899fd3d8b9a21b1a6fd64afe7", [:mix], [{:decimal, "~> 1.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm"}, "joken": {:hex, :joken, "2.0.1", "ec9ab31bf660f343380da033b3316855197c8d4c6ef597fa3fcb451b326beb14", [:mix], [{:jose, "~> 1.9", [hex: :jose, repo: "hexpm", optional: false]}], "hexpm"}, "jose": {:hex, :jose, "1.9.0", "4167c5f6d06ffaebffd15cdb8da61a108445ef5e85ab8f5a7ad926fdf3ada154", [:mix, :rebar3], [{:base64url, "~> 0.0.1", [hex: :base64url, repo: "hexpm", optional: false]}], "hexpm"}, - "libring": {:hex, :libring, "1.4.0", "41246ba2f3fbc76b3971f6bce83119dfec1eee17e977a48d8a9cfaaf58c2a8d6", [:mix], [], "hexpm"}, "makeup": {:hex, :makeup, "0.8.0", "9cf32aea71c7fe0a4b2e9246c2c4978f9070257e5c9ce6d4a28ec450a839b55f", [:mix], [{:nimble_parsec, "~> 0.5.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, "makeup_elixir": {:hex, :makeup_elixir, "0.13.0", "be7a477997dcac2e48a9d695ec730b2d22418292675c75aa2d34ba0909dcdeda", [:mix], [{:makeup, "~> 0.8", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, "meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm"}, @@ -66,26 +63,22 @@ "phoenix_html": {:hex, :phoenix_html, "2.13.1", "fa8f034b5328e2dfa0e4131b5569379003f34bc1fafdaa84985b0b9d2f12e68b", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm"}, "phoenix_swoosh": {:hex, :phoenix_swoosh, "0.2.0", "a7e0b32077cd6d2323ae15198839b05d9caddfa20663fd85787479e81f89520e", [:mix], [{:phoenix, "~> 1.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.2", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:swoosh, "~> 0.1", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm"}, - "pleroma_job_queue": {:hex, :pleroma_job_queue, "0.2.0", "879e660aa1cebe8dc6f0aaaa6aa48b4875e89cd961d4a585fd128e0773b31a18", [:mix], [], "hexpm"}, + "pleroma_job_queue": {:git, "https://git.pleroma.social/pleroma/pleroma_job_queue.git", "0637ccb163bab951fc8cd8bcfa3e6c10f0cc0c66", [ref: "0637ccb1"]}, "plug": {:hex, :plug, "1.8.2", "0bcce1daa420f189a6491f3940cc77ea7fb1919761175c9c3b59800d897440fc", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"}, "plug_cowboy": {:hex, :plug_cowboy, "2.0.2", "6055f16868cc4882b24b6e1d63d2bada94fb4978413377a3b32ac16c18dffba2", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"}, "plug_static_index_html": {:hex, :plug_static_index_html, "1.0.0", "840123d4d3975585133485ea86af73cb2600afd7f2a976f9f5fd8b3808e636a0", [:mix], [{:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "poison": {:hex, :poison, "3.1.0", "d9eb636610e096f86f25d9a46f35a9facac35609a7591b3be3326e99a0484665", [:mix], [], "hexpm"}, - "poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm"}, "postgrex": {:hex, :postgrex, "0.14.3", "5754dee2fdf6e9e508cbf49ab138df964278700b764177e8f3871e658b345a1e", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, "prometheus": {:hex, :prometheus, "4.2.2", "a830e77b79dc6d28183f4db050a7cac926a6c58f1872f9ef94a35cd989aceef8", [:mix, :rebar3], [], "hexpm"}, "prometheus_ecto": {:hex, :prometheus_ecto, "1.4.1", "6c768ea9654de871e5b32fab2eac348467b3021604ebebbcbd8bcbe806a65ed5", [:mix], [{:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm"}, "prometheus_ex": {:hex, :prometheus_ex, "3.0.5", "fa58cfd983487fc5ead331e9a3e0aa622c67232b3ec71710ced122c4c453a02f", [:mix], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm"}, "prometheus_phoenix": {:hex, :prometheus_phoenix, "1.2.1", "964a74dfbc055f781d3a75631e06ce3816a2913976d1df7830283aa3118a797a", [:mix], [{:phoenix, "~> 1.3", [hex: :phoenix, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.3 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}], "hexpm"}, "prometheus_plugs": {:hex, :prometheus_plugs, "1.1.5", "25933d48f8af3a5941dd7b621c889749894d8a1082a6ff7c67cc99dec26377c5", [:mix], [{:accept, "~> 0.1", [hex: :accept, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}, {:prometheus_ex, "~> 1.1 or ~> 2.0 or ~> 3.0", [hex: :prometheus_ex, repo: "hexpm", optional: false]}, {:prometheus_process_collector, "~> 1.1", [hex: :prometheus_process_collector, repo: "hexpm", optional: true]}], "hexpm"}, - "prometheus_process_collector": {:hex, :prometheus_process_collector, "1.4.0", "6dbd39e3165b9ef1c94a7a820e9ffe08479f949dcdd431ed4aaea7b250eebfde", [:rebar3], [{:prometheus, "~> 4.0", [hex: :prometheus, repo: "hexpm", optional: false]}], "hexpm"}, "quack": {:hex, :quack, "0.1.1", "cca7b4da1a233757fdb44b3334fce80c94785b3ad5a602053b7a002b5a8967bf", [:mix], [{:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: false]}, {:tesla, "~> 1.2.0", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm"}, - "quantum": {:hex, :quantum, "2.3.4", "72a0e8855e2adc101459eac8454787cb74ab4169de6ca50f670e72142d4960e9", [:mix], [{:calendar, "~> 0.17", [hex: :calendar, repo: "hexpm", optional: true]}, {:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}, {:gen_stage, "~> 0.12", [hex: :gen_stage, repo: "hexpm", optional: false]}, {:swarm, "~> 3.3", [hex: :swarm, repo: "hexpm", optional: false]}, {:timex, "~> 3.1", [hex: :timex, repo: "hexpm", optional: true]}], "hexpm"}, "ranch": {:hex, :ranch, "1.7.1", "6b1fab51b49196860b733a49c07604465a47bdb78aa10c1c16a3d199f7f8c881", [:rebar3], [], "hexpm"}, "recon": {:git, "https://github.com/ferd/recon.git", "75d70c7c08926d2f24f1ee6de14ee50fe8a52763", [tag: "2.4.0"]}, "ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.4", "f0eafff810d2041e93f915ef59899c923f4568f4585904d010387ed74988e77b", [:make, :mix, :rebar3], [], "hexpm"}, - "swarm": {:hex, :swarm, "3.4.0", "64f8b30055d74640d2186c66354b33b999438692a91be275bb89cdc7e401f448", [:mix], [{:gen_state_machine, "~> 2.0", [hex: :gen_state_machine, repo: "hexpm", optional: false]}, {:libring, "~> 1.0", [hex: :libring, repo: "hexpm", optional: false]}], "hexpm"}, "swoosh": {:hex, :swoosh, "0.23.2", "7dda95ff0bf54a2298328d6899c74dae1223777b43563ccebebb4b5d2b61df38", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"}, "syslog": {:git, "https://github.com/Vagabond/erlang-syslog.git", "4a6c6f2c996483e86c1320e9553f91d337bcb6aa", [tag: "1.0.5"]}, "telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"}, From ae4fc58589ac48a0853719e6f83b2559b6de44fb Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Sat, 20 Jul 2019 01:24:01 +0300 Subject: [PATCH 28/66] Remove flavour from userinfo --- lib/pleroma/user/info.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/pleroma/user/info.ex b/lib/pleroma/user/info.ex index ae2f66cf1..60b7a82ab 100644 --- a/lib/pleroma/user/info.ex +++ b/lib/pleroma/user/info.ex @@ -43,7 +43,6 @@ defmodule Pleroma.User.Info do field(:hide_follows, :boolean, default: false) field(:hide_favorites, :boolean, default: true) field(:pinned_activities, {:array, :string}, default: []) - field(:flavour, :string, default: nil) field(:email_notifications, :map, default: %{"digest" => false}) field(:mascot, :map, default: nil) field(:emoji, {:array, :map}, default: []) From d4ee76ab6355db0bed59b5126fe04d3399561798 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sat, 20 Jul 2019 18:52:41 +0000 Subject: [PATCH 29/66] 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 30/66] 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 afc7708dbe00a70be616f00f01b22b0d01b9b61b Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Sun, 21 Jul 2019 00:01:58 +0300 Subject: [PATCH 31/66] Fix pleroma_job_queue version --- mix.exs | 3 +-- mix.lock | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/mix.exs b/mix.exs index 0eb92bb12..315ac808d 100644 --- a/mix.exs +++ b/mix.exs @@ -140,8 +140,7 @@ defp deps do {:http_signatures, git: "https://git.pleroma.social/pleroma/http_signatures.git", ref: "293d77bb6f4a67ac8bde1428735c3b42f22cbb30"}, - {:pleroma_job_queue, - git: "https://git.pleroma.social/pleroma/pleroma_job_queue.git", ref: "0637ccb1"}, + {:pleroma_job_queue, "~> 0.3"}, {:telemetry, "~> 0.3"}, {:prometheus_ex, "~> 3.0"}, {:prometheus_plugs, "~> 1.1"}, diff --git a/mix.lock b/mix.lock index d14dcac21..e7c2f25fc 100644 --- a/mix.lock +++ b/mix.lock @@ -63,7 +63,7 @@ "phoenix_html": {:hex, :phoenix_html, "2.13.1", "fa8f034b5328e2dfa0e4131b5569379003f34bc1fafdaa84985b0b9d2f12e68b", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "phoenix_pubsub": {:hex, :phoenix_pubsub, "1.1.2", "496c303bdf1b2e98a9d26e89af5bba3ab487ba3a3735f74bf1f4064d2a845a3e", [:mix], [], "hexpm"}, "phoenix_swoosh": {:hex, :phoenix_swoosh, "0.2.0", "a7e0b32077cd6d2323ae15198839b05d9caddfa20663fd85787479e81f89520e", [:mix], [{:phoenix, "~> 1.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.2", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:swoosh, "~> 0.1", [hex: :swoosh, repo: "hexpm", optional: false]}], "hexpm"}, - "pleroma_job_queue": {:git, "https://git.pleroma.social/pleroma/pleroma_job_queue.git", "0637ccb163bab951fc8cd8bcfa3e6c10f0cc0c66", [ref: "0637ccb1"]}, + "pleroma_job_queue": {:hex, :pleroma_job_queue, "0.3.0", "b84538d621f0c3d6fcc1cff9d5648d3faaf873b8b21b94e6503428a07a48ec47", [:mix], [{:crontab, "~> 1.1", [hex: :crontab, repo: "hexpm", optional: false]}], "hexpm"}, "plug": {:hex, :plug, "1.8.2", "0bcce1daa420f189a6491f3940cc77ea7fb1919761175c9c3b59800d897440fc", [:mix], [{:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.0", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"}, "plug_cowboy": {:hex, :plug_cowboy, "2.0.2", "6055f16868cc4882b24b6e1d63d2bada94fb4978413377a3b32ac16c18dffba2", [:mix], [{:cowboy, "~> 2.5", [hex: :cowboy, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "plug_crypto": {:hex, :plug_crypto, "1.0.0", "18e49317d3fa343f24620ed22795ec29d4a5e602d52d1513ccea0b07d8ea7d4d", [:mix], [], "hexpm"}, From f42719506c539a4058c52d3a6e4a828948ac74ce Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 31 Jul 2019 14:20:34 +0300 Subject: [PATCH 32/66] 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 301ea0dc0466371032f44f3e936d1b951ed9784c Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 31 Jul 2019 19:37:55 +0300 Subject: [PATCH 33/66] 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 f98235f2adfff290d95c7353c63225c07e5f86ff Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 1 Aug 2019 16:33:36 +0700 Subject: [PATCH 34/66] 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 35/66] 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 36/66] 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 37/66] 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 38/66] 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 39/66] 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 40/66] 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 41/66] 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 42/66] 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 43/66] 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 44/66] 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 45/66] 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 46/66] 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 47/66] 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 48/66] 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 49/66] 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 50/66] 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 51/66] 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 52/66] 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 53/66] 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 zcmV;D9Bt!?P)tD+plyU2}VYLaJUd03B~u;NAQ4W&u=D^@;&#TVH{XAI_fz zuoqRY7=V^_75L(@f>{9jQ1yxdXkJ^6Z=NbZNU#RrAgW$50F7(P@xwEP2oDXG0FI#Q z6$8+)x*WeeUxcV|4ZyKfyS@6(RwgNYyI_pmt>${&}Ss2{9Ug(-iBO z0Mx7~gYC6qB*%qH0B2M63IcGvUV_vF4ZwL+y{wcg?INYw5fZ@7RK1A+WJUng|MWQf`>`On7nB+S zK%{IIz%5k0$p8YAKzHpg%>H~VcfDRfdjQn{csw5XMke{z^+z+?GvT77QUah7fT8|w zi~?x>d%^5sKqUYKK*l%1FxcJ5Mfu$Ma=NYnWcfRmm7y$018^%(K6zJ=SATX5 z`{&6Z-S+_7mXxA6O9OBNRc{3V{XN>%K&xYg1i;<%i&eT2L;z$06F^tZ&)o515_i3x zuiA|u0w8Y-_cZ)9>3lVlxn9jtZVx~J#)0m-Kbi9teV+@+$H5yGlps4rdmMZzRc{dh z^2uPQK2HX%KOcYqOrHcc|22>O^Mt-v0o5zY;G9>2wCO2f=Tr4o03d#6U=qCg)2roG zfNGOM34qrdfM;lcZ(el}vp*lN`?T=CUnxfX^tA9(sd`J?3FPQ&-OAkaikS12Ed5^$ zd9etwks5&Gsd@_l2*@jdnqQgorBv>EEl>AX!+w6Q2$9pTh8<1STLHiX(7E$lI9^DE zo;QJHn%0)%+ouW<5~2Y(gsQg)fUfGFnB%1s?s`pM04?jv@x|kF>jUgd)ms37e-!9# z*~G-|pE+O6)_sMLPad(<3Lyl*3gPHx=C^;!ar8R;Gz}(!xA0Fa=JsgA$ z3(Iv~C2ZYYmRl&20LV!pLwz0G^=dY^JrgDnROkYr_nk^CzcojeKPHk!)msF>U|$zT zIM7x71GhaD!W}Qf>w=*Fy-LixE=vONiNsU&mH;5rqkWD-yCUG?T!1@XOon^Ge|tC> ze7_3MU7jHUbpFfURe(ozHSHUh)g-%wxI0dZ6mJV3Qa7aT^i$f;>WWL_{&g>WJ zf4)m&YWRc}iWa_=j=RP?DFB-_54AD@(1n1d;*~(d=Fe`a|LIQcgn9)f@1*1A^TQ|r zIa&*mRrfzF9sxLVsd!0Q6>9_)6Ho*8HHje)%S_{xLKYxG3CnO*$IvQ4+4cJpyo$X8_`ECQC5tas&drVRsr=j@>q%L$}*If7NWQ?ugUIVQDK1+f-PU))$r5pViZ0c z_lG9cjl$y63blJ^{X3Wa;XQ=PnC{Z(4`xQ>Wm(bI(DyZe0;0woX`RD6Cd19F8y~CMKY7pHtQU|L0$~;F)Kh#%G^@ zh1{Lnl@R_n0qX%5wO~D9Pr=qsVgq#T-kpp- zq5#k?=zYp52ncX31vxn@U{Hw+$TweojtB0)7Z-?jNJxk`1fp87TJVlwy1wu2Y~A)q z6+{X`1!h68E-(&3fZ)r)g9qWcXP?4lmrihBbK!y?K|0pVS2Mk$3=_tWTzMx1K8pRlIZ*TCqe}F&I&7+z z5Fy}uWFBvlGtr>D{kt0tJl9jMmlTY{QB_3=c5eR#t8$j%hyQ(xPe1t(@4WpcUY|b~ z^XI;TH(#HR58i(lUyC-$Ua}CIHm;Lv?ro%oON$Gs-j6B(q!r7SsQV2ORo_z>o37h9 zM*{^!(@YqW-B#-@sCm7=;}CFAbepC?~1c8x)$?4S&7X* zztFJmF$b13v5JT;;NyReFgz`*)#o|mXOh)yr9c#mU~GLc6`$W1h1)OiN55zdzHWhV;-Vpu4g2PEyVtP= zfY;LV0GqTHrqKc#Ml|?{Mnb^nv=icVLvZx)Awb@zsM-57_U_!^veZi}A^#XHIyFvUi5Jd~fkX3F<7L?1vjOEL(qE z^<V=7%vUf9(`)V4#dLSB?5Aw#{;~i4yna0mGNdT@Nsf_6Fv{pca1hw?}Q-tBv z23$MI{DXxcHr{2B4M-H%XO<1?pG`;3bN#V;?r5xiYaBMbe?2yRcC+}%cVp}KGqH2g zGx&AoyU5?Oh2_?|cG%af0sdB5e43>Tz|ldW;1a>= z-tzY{uv3aW7tBd$QW%&^!01y|6!3Yil~9CW4UI5*2V6VJbYl=XNWH`x2r{YIH6EYn z!0wkK!I0>psX<_GJ#M9P<6go2URXHUUzJ$rE}uuuAX=a~1)u?&#eVSh^?}K3MyAZ{ zrU(E9oXdbEMCIo3-W@Th{Wz1Y+?yf@e>e!#EkSu!w`r}6?h?S)&kqi#ld%G-NM$Jc z1-Y32gcvV*Z=|!m3D-$V^bxV*u>>re=7n`n#-JqsR}__2H=_X5xi;VEA}xQm-E?njlj(b6)b$iQf~rNUo{J04~;AA%zGY5bPJgB!d|kYyzK8 z938^-k6W9$vxP>JQfDl0$`>%hRiHoT`@6(|;t$TA;)PwJ1-H9ox1bARs{H zGkWywNlixWMFtZ`)dDnv`5j+>v&-!r5yD1}T=!3?0PLRI=)lM{0V$=m9w0qHFir4- zx0hCI4)jKFfVb*!-77f=!@8MNM(g51R*WBN!`MN=7}Z@vzc>x4HVsz(NF5K)nGT>C z1)!k@zwX)vvzcqF7h>X5kdTxt>wB6AK?WJ@Hgu`-i7bMz^~X5cIX+4?IEY zps&#zI#GRd-9HjmJ)u5F!C^aiZU9w5pI={1RTF~79~O4UBR47+aH}9raI2uGEtH1) z>jfm|2uACVkL$Ss{046XhKPIBXB38wo`4G{+=y{k{ssU0{s*e*W~t$$Z9yt1DMh5y zdYI^*x4>5rATSAR(0oJT9qfcDBmyDMXqovgEtk;Amm4;y>BvknR%yJ=&x_U*X6_U( zVV5wJ8#@f}HN6K43pL&v`5xU#?*9GkzBtVLR}^Mn*BO65Ck$iySPb=F;p6R9$qA)| zp$*bly_yBRC9Qnxt^dI%AAcx8pDjQ`E&e}Ml$RPoq!WgMyglNEmZOrI5B|PhAfwE|-msf|5GD)^JqUbl7np-Y`2D@%&vuzMm0Cqfgf5Xf;zGl2UC3Y5sMALzokaE6%XX z5@%CO-N@c%4C`)EdHf+=-B7Pjm-k1VV#fKWS#jwwJ8l{sDol1@<~8AX>E0(s9`}P}Tw83yj?HI8;36;9}%*2uLs#Y&|*=xvo zzCWaD7mH`uj+u~It~?X9mUCTK2RCgI)z07!Rr#KI%6O&VxP^x(A}B;nKoOyTkJsS! z454FN7}^Q+ka#Z?(h5Pi@4WQ}463dyXO`?FG;uCLX_W{WT#?26 zJzKHur$-dy7u=&U*1()RK_<5u(8Z)+49rr=wgM>BeYMQaBF6dm93S#UlN{KMyxp6zf9qoG zUh@fdEPPr*%eP?ltAE1Mhoaq4M(N+DGu=Jo+o%9ACg83Md{k@2bi22YE4->5R7i2v zP!sas>yK5>_eIXLryd`k>w^`~^v1HsGm!mYB;?QrVYU~3zE@QIlt5(bd-dBm(+{yn zxIuJ`D<|+uYq$Jsm=PQ>Xpr)wdnEW{?>sT4=GcT;R>u2}6TF5Ui)T5&_N;8ss5feE zw^$~i!Fcs>yPg$n-84BcCLtJ(W1TFWqAzEY6 zR*4-+X4HS2$sh3tg}?)*nX5j?#M7=WA$n~<+n~t;%=>q={D6#>UJCg4*?XT+wzje? zaZdp248@jmw~3pkwJ~~8fKTp?MWqPA!UtP!;F+!aX;!?7?N_{?fpt$MAFGI?#RYcv zyfn!eCNq&l=D)ACQTiXVW=wFDz1K2AKszM=jdWZ#Ou16HbG%cfWotgl#7;4=mbWrH zh4(^1Eq*%#BYK(*>))=mVOkJs@xj752~2>TbRk%#cin#b_84Tu1h|7#V+Y&t&D1!$ zs?-X!wpET^8LlT#__jQs!t5R=L6Zfb`om28`$C6tuYI&OPJV*b%#pJ`kfmV95(2J& zT(3EBCt}T}@G+*&f&J|MI7!c%+=4*iylHfZT5C5x=Pu_~8M^H+tL&UA`dIO};4wz$ z4#H#Z%po^(GR>??+$dC~at+o+vf$(6dt!O`Q;B}39yQ&W}g{jF5uo$~<-AIcLz*WQT%=z>0tAdP8 z&!#8=+A}|myMf%;E8z9`n^qXG~qOb!k>bR#LNgi@kJn9dBrTqz>SQ5xjvE*}d({&r5N zag8xrJEZOc?sy@SsXob!Mk=sc{NCsP5eXxr$byJv&1i5@;3$lFqVj2|QuVXSmtDH4 z1^#hvC@vgem5iJTJrV!?0!P9C_fGiIXi^B&uS=TWI&-5;6auD&vQ|+!grtZkMV)#69SZN1S&QmCPvvow`=X3&JuuN9X$8e z=fBahm~U(aAD*EhJgzHHw*{o@uDh~BXaUlL+-&)aMB*yMC|oeRV=6N&Hd3&NtA4lH3U;6rhcu3K%o$b(EbVjy%mxkaA8f9~^-qakl4 zpA$p04o0&X`Qsq52%_}8OpNKFVbb|ADE$0v3drwPCz%8(LaWm7tC_S{x>cE20woMV$0Y6wP4a{>y4vA zB_NcBtSFap_^9~BKoP2UEkN0pPf@gLE)Fby7{%E$Fl}NtJa(O#k-TPIqd8ltk@$C* zHDt_!QA2JMdq?*1Prkep|DT^bW}cq`rCZJq!_DV~0iR}}{*x?B{F7_DIjVlaYfn#w zs;%sxbcx;MjR0>Aog-7RKKnBOd8pf)gZjMnK;c%PWEX%kG*p$q+Z2oIhH9LF$IP-( z?HcXf)V{nUqtQOUJbkk$a}s1AvxZ!%D5&gzK*kX-PKxB`-C7`9uj?oHoM2{=*U`6r ztIys$!J(eV$HxIE#V7M0f!-U@q;)`wG#WzAp8Q|9s0elYm!dj%F=}>aqvqEo0QTXh zZ{LRc+%=($x05+XQn^gk%D|_noxJ0Q*i?M~KNp4Jwy_S}Gr=j@I|a4l_tr}sYKC%4 z_T{JBac(~g24w}SB#_<3IHE?dLhy(nO)$4_T9C>XGBwPaEa0Y3(V=0qA7X5FH(hr)S@ZYBIfG zl@fs3-Aj~E{B7bDYT&vj267!$;ZzF2TD|s<3^3F#tACiH)f-iOD_AU8BiN`nUf8Z< zvtXUh+@JMBGG5oWW!);6ptrNN9HzA{k9ZRelR;NjX%ha;t4G*TE{vxZp>TxA@A5S{ zk&&69?msfhwf_J2uU7)71?lo}7eTes?Vz+|+Eq?mJKW-8aF;Yxt^0w^7IKjfVsINYp<}iJIl=Wi6)@%1-UwV z;D(WQU{QBzL=mc9C+SVL3AWwmdtZI^K1lN(zZbF55r(F@M(cpI*yuv`@+0>G*bS7e zN3{q+&921&iZS@RY!-vpKU{q2t&mO8R(xL%=Z>*%}bdA zJT@@`SkeO*4>T*`w1zm<{ahlilfu0aV0N&h8XHfkN3+F-h|UgVfBPEF7}UpL{0u>- zj^+tO3*HV248UKm8IOV;RP|M8C|sqOuV(+gWdQbK=-|F;pt|+wfrLcY40`G22t4<9 zW9FFDqtoVtelF${Sz7bc(`WQB;Z42CbEq={r}pWC&Jj_3t@;zDrl+$rnY#t8*&Z;5=5p1yOZnewz z*_A9<@3gL#rw&dw!9N7$RGs558H<8negIGj)a3$uC;){Rd(jvU7CV$0gUA6j+P}Nc zH_DJ%=I^g+>s~t9xeWoksPkTUZ|^)6cHj5!bc z)_()i>gC_6_iQ1dWHJZR7);JEb}i+)%GbtOMoYDmPn`v25s{I+$0HrKLCwW8mw1o4 z0&jtbbh?Y>VC>lS*O9*kTK+xhuej{O5$bt{YxW<~wq9A*?CEQ<7{QbXt!3ywUteFO z8SDL_ZD)DEhWdY8^H3w280+*?v2uK#r8;r`=iFcf>Zfx=$HW{mv-q5Y&Dn4N?VO@h zwU#5ZU@y-k&^1*f=p9Fm`|PiIs1cpVP$?5U%#_iW$B*5_z2mMPe-0jJ(4ov1LyLWi zT2V#qBv-G*1m5!@>fdIw9pOk=51l4p(?n*?80HnRQD*$#($E7b(8 z_0UF(#qA9kr>xOdG4Ay|%|nM!yZ|rX5~a45GXv+eXwZDD9LQra|7xvu(HR27#Ks~^ z7Xm)gTTvlt4;_|*1P5kZ6RyIi*Nw0$<|gabzQKGNS6uT$Lql7;D>a`U31@zfUcGq6 zf^omyG!Gq-a(L(jTeWhq?2ZUBn~Yb;)UAE1YaJyn)LJhk3hL+<@(^zQey`U&bX0ne z4U&g{dAG!GqX0-*aEfOb s{;xea761SM02t(NJ%`;D4*&oF5L~GgO2%Z*Hvj+t07*qoM6N<$f`>UBrvLx| 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 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 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 zcmV+z9p~bSP)CX0dqyn}FTJNj+gwr$(CZQHhO+qP|^wr)>P z);8`r--({muNwcd@+Y@n&_0reQ)T+sloM%5L(Zfnck-ejiXaokr$m&T(oj0eOj#%g z<^GyNR9IfWrU>Q#TAN&yRqdaiQd2TYNH&V3VDct6($eo;Ho8tbXf_R|PSlhdPz&lo zQ)xfJ-_sd|snc|i?$T}b{R?_c9|_;b!Ql%yJ2@zH8V4A4jxcB)$PsP^GODjC zcYb!&J3_Cv5gO;%{*DeZ=J)zUZ|J$YuFG_W4$xLwNi%3P^`z!hjqD)ngv}d_{Xhp1pTvXP|ZN} ztr~#7)dJ9`sz3Ty55TZGff(B`2oswGV_J(4%xV>ixotzSs6!Z*b_&CaF5y_!Egb86 zMkqG*iNw}^k=W9gue5Vu6n1^fZrU**%HBp^H}s0_zq)(4GNz2ZuzeWjvStVb%%4x{IU*NkF4!6J>^C` zUucsJk(G7yo zs-!QByftM{;)*BpZ5;Eb=RLp>>>V7Xw7)&miVE30;{r91lB$YQA3CkoYLF*)&bT&X z$wXhTSYiV8ZmN;{e>I=k$Q2KH&1<)tLBYdtU(^ z)wObN`9Ufu0A6|{oGYmq=8xH|+7aUn9Z|KC27JO8{U zKr&1MFTcCj+ABU;YrcKY`Oar&(m)J;>1n(<;w3DZ`6Y=i<7*0C;2)Wc9_L?)2kw6elg13gs>MIUTCf4Y zF=2gFAT7B)|8ANgfL7uGaX7zt!K94ElSkf{DGt9D9YlVl^I9PO!J=^q81;L<%R%Sm^2cP zjeNuV-5t)OWs5qX8{SJ&j$@Im5Y^Mu@x;}EaC38mDK;K4rdVj2D?Git;oHLjX=BpU8D`^kX>s!OMeC)CvM%uxh8Q0>dAQK%lQY@t$5t zN=`yrdODI*QWPlw3ZYZy&gyehY%F|zef3r0b78n404I>aqs<>C!OPv24=DPfJBlEZ>x)5spMr>si_=$x0^#+B08Y8wCdyeoK|WR{|Q_E&`nt zMy9L?{CQB2J^Fa7A%GKb<=e@~nGk^uZv^7`o1?At^2Gsb@HEmp5At)BaIo)b34j41 zhNg3xD1v1YMB@KuB$3In`!DfBxW5K3FE2z#$JE~v?4&4CWu@rcwJRc{q9iM;;td&w z02&xS1=*u~vHjgp>y8DZEt(dIJI{4N`Q)?)0?3m9PN)dr;!Gb01VBTYvG(0W#t^uo z@atx+2HEoaOlTpyEAGA6p9V#Ecm!901_B_{GcxEbay!rkFv$>ror&tnUou2P_UIt1 zwP2Zb^1Ll-d7VBWirYW~0Wg9W;^)eG07nEMS=gN2w4lp1D3mUuBv^ZW#w9qxjP8HskvRhN4H$o_0r&q!UQP^Yf69 z5Z&aoH>^8MaqVevKJoPa)si12`@M^ci(cUJq!9=B(|*5vTsRi@ z*RW~wBi7R67He5GV6CabkwbaVG*7&6gDZ{8CD-Xb09pD|Dy}`pk0owA<8!5H))v4* z944bKNb^)e@DB{O1%u+_=@jZu{Y3rzZwR2ivp3R*d%kto@8hf$07@$@C^&im#1B7C zQE%R~{?V)&YV=IbDyLvSSC*DJEC2?O`bp&x|JUzh%=8QNpEbF_gas>D*a@HhBO2ar z8eD7+9{E{=A`(LYi4wxKIeW{;gjx@7{MA|sptz(|jDHXX`G-L~@!TtX11094jCJoe zd@5N0!abZttk-mRJPyFD;#YlbS;fy^FtLoTSs6qojkpR-=^u}WE)QanEnn*gVRs`L z)YyLiMtdwCpkdYf-K+(NwkzWo7UZK)3}1Nc2uh2NfrwAP6iES^?8}WiW)uZ$6
znn6b+f!pOZc$TLC2HzjSAWxH1$IKQ^9T1P}d)htwUryxU83K@}e);$)ERy|xW7gf) z(uyi;xdrmn9}>eW<11Qft56~$(6j(NeYtV~8x7E~0F)(SEN#E@!a#>}0Zi&}HK73? zyDGRzD+x1^Ohw;vp55^CRKG;yFa)rY#vdD|_WK?423ypAe;i!$?S|J$%%8u&wj}=} zl%95jeV6+`w}v(Kl0lAU1Nj=eK22fYgX$wF{qf|l!}@@6k4Nf;pHsi8xUCC-%eOGW z_VG<~lbyX~qy4dXkcNFLzOWuw`+a$%#wUp(BPR0A<=->m)4==Y6G&mKe%qD~e)_s# zhXue4AIsUJLfmj}sy7NwQDJ;gB7dL$)1(U^O(WW>0Jvx8OpL|S;T~8#Si{m`ZsNIn zTb7RSvMw3w!te2C>L`~pDNYxLd^F3~@mMxC1dC+9&mQh+rSZAn7ZnxBet)P@lbe_QpZ4z{`4`5M<A;7VnAFSWk8nom2qaf=Yi%!=vIaulj-8eOQsX?4Ce#IRs@MTDly!ZW-rAXf7%5MvjCC4> zEag@%={Xakt*bxkW!XIKX-i)2HPByM68&CN5kO&p&_n2@>hKfH ziL9z&QH9E`@$;ZC<&86?%T%u8rvMR8Sb!@o%4Or2D9%4nqJ=U|C5ai1W@pR|z0Pj7~$Jh!lXUZ#aT{G*zOtMUNLWlX_1f|L0%& zgfuCl$Ss0f#f`mHB=8$U08U_4Wf^L!Dv+F%*dzgPRnXi*U<%Pxh46;=lN|0a`gKSG z|0g4SO+HQVgmV97swnhVLjX=-ZFQyk7#<$Zdby?vKnp}_Ogm-;_4e>)lV1Nj&qgZ& zOzt1Y6mygQj;9kwAUUVV5P%a1OEnH1*oS~Xwp_V4X$#OaU-U|L<9^J2*`e-W7KA)y zGbCSw38p635&ZL(Q2t%6Apj@PT6-Kv4S}+71;A4C z--am%kki&UdZt+d7;?XgPVO=U-~<*I=7YH5`s~@{odF|9m@nV+Q*f{6zY!1 zU^hJi*en4IGFAZRl3259g)%Hp16{gxqp?*Kus&6LRePHNKKffUlQ|AGg)FKYFv0qQ zvY$9Cs(Au<{T{;yaB?)T0K`>SUZD&Vmym#XVT{Ry=$IJQR2dN&iKyskxO@58&hGtU zr9h)&GiL&^up|vnU8|0NJPtN%`L~|}?oa~QU`FR?C{bf4l z%=!)U=FGx>7S6*Tzb`=cl0UE_CkLChY{vSpuEElw8fy=mI3LigDtab7sR}xf^;#Ua zkY)>j{=>sps9yish5%YX1dI$e1^Ia!uymBa)FpXsc<20)EIwuV`Kbl)ftoI+hDv<$j~G7vdb39}$i9Rmahd2E$rS z00w0hC_9d#;$q~!*O^Tyt={>>ZG~}#ga|iyc=^KBRn-bk7rGk)Xr--ytAG^Avhh@3 z1zLq%1vo6~^}B2_g_N`mMk_f*RL>N~83Jg9RiLcOiu`>WRRiks3DK?k^sjq239elF z+xepVIo;KikQ51HdwY9>rV@5*X#p54C}1xFJCCK;p&gyURucf*#WE8-ZBxP`W02Xo z6FNz_IxQmuetv%XxY$b!0i1#y4ne05s9}8|BZJM_1}XsTS^6G_O|_Z;c&cEDcz%{7 zhwcl7x4VX*;9ym{o?)wKW?n;g4zDo;aEj7!a@UTnDiT;+c&u4}Us?qm*}V!m6QZQ4 zHKyhF|4AyVsOkziLHyStaR~8MhURc64vAv&O7$=V>JvgwJV(F~!10Pw)YeoXSl+B> zo_-1hSA`}EsJINpC6!qF`Q_|lZV{uiZa}|J;8<2il@$8-%O;%HQN;n^>Ftm9X0x8k zwoeFEBKZF+!TE*&8evIs0SJy-PfJS!!4;wDuW?j$I&GQpS1cK{%3VqJ%_R>yXn7%nG(bGYp5!=f9V7|L2lAKUo&g3}5e1IR-g z#e46*qu$u!!lQb>K$FQn$NlT3atq)z6Q>Jc(NH%W`eQKi_UytJA5Fm3S6-yg{Z))l zXivwFEp3g7K=%vuIKe8-Q4AS82m|`RF6`H)ICA7j+;|K0aA z2l2BoTe!51aj|eMpQ@EP%Rn^Ck0`ewfBzP&d?%HoYn*#uz@!9Zj|oBHnlFHoJXG&r z4jf*|!>6zesMv<@zj_Pj_wH^Ng4t}gH@Dii2KmT1L2R6T#=sN8dN2?!#Jst)nk@h| z*Wl=WtoookmW>H=?y+xzNzFbg+d2a%+J))^Ij}Qt8BnwyK(X)uex3FOuDz^Roq_$( zAu-8*jf^e?);5NPh3O*5)a)=2EUcbBZL$LbD3_0iAk$7b=g6r7$d(mlm86}k_AP?- z*k)AG==rN@cz&*gz>hex62#DcFWD-bO)l!Ar(YOP16&dG{sxLdzfr10A^?6wY&7f5f>g8X?VTjnX3Xbi1k{{4fqO=ef#y7EiM4g7GNx& zvjRCMnECzX!p8atHvKCSBOXh^-mSeLF{Ws68uB538olZF6T)iYyoJksMm@#ZtptPo~` zr$MC<1yDLU6Bnnb{aQ{Mi$SEN**cVd{q}7D}&mNNLh7X`=B|m)sU5g8V{Qz7Al48`AZoCqX)4zEc z1HYr1(MC@>IJ1)D(pO~Tl2nkIDuscP0S1;zit z`NH44WWtO=@p$a&Ahfd?TaO8l`-O|t)wBP{5iP8lF@0)_3xH2Lr;gK5gKe!|=+xdz zO%7q*V0{rR8gEABlyn&?m2AuZ3wx({;H_sO)#n3Vcv>z0T03uqh)5oivhkiN-bgWd zAT+>L0!Hd@$iu>%x;=l0@NQv-&`nb?kR-&y`E&nU0Gu*fRLWpa9jcLk_Pq;5A&>#2SbgBORDZ-D0 zqlAAG-X*+9Xcl^DCxw9?LafTosS|*jGOEJ7W#yGPyk#DK%=!wsxw$ASEp|`Z9Sxg?)uRgnmL-p^I?75bCKma9X(D$Hxb|cWwh< zW&OV@`8v317Pfxg11rYZ`T`$)x-SUF1 zN2>6UFhVnMT6ogjzP-u>)>c=bq`VphNB1E|;`=LxYS{Fit#9y-`=4>T0LaC%*JDnm z8bPb#`XO%ocuaVYc4|aw22LBp&p+=x5alI>whZ6eX=*h6>bK3xL+3d8EBZX3*9l(af-HOgKlwwecKsO8{(bMg_kbuC<8mtb-en)DV$YRzLmHF(HFQ0{4+u^dafT0| zPYqcm=l1i{e01YKqZw#* zx;{F3fwm#g>&{Gq#(m;d65TT5}Qy8V4H8Ai1n<;5X ztEHx{$g>tl^0r~a2kF@JQIwkQ!F{_&vdW#`E_^aGGXq3rMJZ}(EqLd>kMQcP!6?a6 zmqRO0pF*gb#t?6N_P zyu3L|J;B!r-G!Jt`#0#g=DM5sGhE-pm;1ds-{Y4IH4)6!rJX+6%SSuwU?6|o*wG-^ zC&*NXpkC)@f;lp)z2-JXh@}XO8#&h+7yQ`*#H2c|>@pFVkx)Af{&IS=36=f^d zIRAWj_~^x$iUYN?76vBWa?8!i0d#kF!z(Yp1me+09dektzW#h` z=a(HjN>E2a$BZNR`1-N*S|8AmFT7DZt6^YYWMm|?dUbL<P#Iy{CQ4Kto`E)jtA%{B**kyIYhpNcOI8XB;BC!7o56Q10M(cJM#!h1 zowU3(LSO!4H3Mw|-oh$+`I(L15~fepsNX>Oj%J|E;64U_^dU-(F8yg~^;@V9srq{Z zZ3a{P{rx#I<%Ih_kANW|A#HSsg0cJ;a$o1suaU88-1m8zv~K*nGy`o4os{?Ap#!>g z?@ohrVun6XkNZCF5!Q|Wq-LPaAyinxo_I$4)P<0B(=~a2u&1Y|e%`-DGtg#{ApBZb zOAnt$r&B0jSJrjb#t03xeW=QwNy1NsV~xz60RsjM7%*VKfB^#r3>YwAz<>b*b@+c% 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 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 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 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 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 54/66] 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 55/66] 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 56/66] 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 57/66] 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 58/66] [#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 59/66] 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 60/66] 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 61/66] 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 62/66] 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 63/66] 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 64/66] 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 65/66] 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 66/66] 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,