From 371a4aed2ca9f6926e49f6791c8b4d14292d20e5 Mon Sep 17 00:00:00 2001 From: Roman Chvanikov Date: Sat, 13 Apr 2019 17:40:42 +0700 Subject: [PATCH 001/202] 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 002/202] 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 003/202] 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 004/202] 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 005/202] 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 006/202] 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 007/202] 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 008/202] 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 009/202] 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 010/202] 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 011/202] 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 012/202] 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 013/202] 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 014/202] 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 015/202] 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 016/202] 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 017/202] 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 018/202] 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 019/202] 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 020/202] 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 021/202] 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 022/202] 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 023/202] 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 024/202] 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 025/202] 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 026/202] 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 027/202] 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 028/202] 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 029/202] 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 030/202] 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 031/202] 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 e818381042b2bd1d6838f61b150d2816115bddb5 Mon Sep 17 00:00:00 2001 From: kPherox Date: Tue, 23 Jul 2019 18:45:04 +0900 Subject: [PATCH 032/202] Use User.get_or_fetch/1 instead of OStatus.find_or_make_user/1 --- lib/pleroma/web/twitter_api/controllers/util_controller.ex | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 9e4da7dca..39bc6147c 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -17,7 +17,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do alias Pleroma.Web alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI - alias Pleroma.Web.OStatus alias Pleroma.Web.WebFinger def help_test(conn, _params) do @@ -60,7 +59,7 @@ def remote_follow(%{assigns: %{user: user}} = conn, %{"acct" => acct}) do %Activity{id: activity_id} = Activity.get_create_by_object_ap_id(object.data["id"]) redirect(conn, to: "/notice/#{activity_id}") else - {err, followee} = OStatus.find_or_make_user(acct) + {err, followee} = User.get_or_fetch(acct) avatar = User.avatar_url(followee) name = followee.nickname id = followee.id From 4af4f6166bd04b5a302856034fdda94dd61045ed Mon Sep 17 00:00:00 2001 From: Sadposter Date: Wed, 24 Jul 2019 11:09:06 +0100 Subject: [PATCH 033/202] honour domain blocks on streaming notifications --- lib/pleroma/web/streamer.ex | 3 +++ test/web/streamer_test.exs | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex index 86e2dc4dd..d233d2a41 100644 --- a/lib/pleroma/web/streamer.ex +++ b/lib/pleroma/web/streamer.ex @@ -234,10 +234,13 @@ defp should_send?(%User{} = user, %Activity{} = item) do blocks = user.info.blocks || [] mutes = user.info.mutes || [] reblog_mutes = user.info.muted_reblogs || [] + domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks) + %{host: host} = URI.parse(parent.data["actor"]) with parent when not is_nil(parent) <- Object.normalize(item), true <- Enum.all?([blocks, mutes, reblog_mutes], &(item.actor not in &1)), true <- Enum.all?([blocks, mutes], &(parent.data["actor"] not in &1)), + false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host), true <- thread_containment(item, user) do true else diff --git a/test/web/streamer_test.exs b/test/web/streamer_test.exs index 8f56e7486..95d5e5d58 100644 --- a/test/web/streamer_test.exs +++ b/test/web/streamer_test.exs @@ -103,6 +103,24 @@ test "it doesn't send notify to the 'user:notification' stream when a thread is Streamer.stream("user:notification", notif) Task.await(task) end + + test "it doesn't send notify to the 'user:notification' stream' when a domain is blocked", %{ + user: user + } do + user2 = insert(:user, %{ap_id: "https://hecking-lewd-place.com/user/meanie"}) + task = Task.async(fn -> refute_receive {:text, _}, 4_000 end) + + Streamer.add_socket( + "user:notification", + %{transport_pid: task.pid, assigns: %{user: user}} + ) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"}) + {:ok, user} = User.block_domain(user, "hecking-lewd-place.com") + {:ok, notif, _} = CommonAPI.favorite(activity.id, user2) + Streamer.stream("user:notification", notif) + Task.await(task) + end end test "it sends to public" do From 48bd3be9cb9b378dfde78e769e2f00ed77129ab9 Mon Sep 17 00:00:00 2001 From: Sadposter Date: Wed, 24 Jul 2019 11:11:33 +0100 Subject: [PATCH 034/202] move domain block check to with block --- lib/pleroma/web/streamer.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex index d233d2a41..e4259e869 100644 --- a/lib/pleroma/web/streamer.ex +++ b/lib/pleroma/web/streamer.ex @@ -235,11 +235,11 @@ defp should_send?(%User{} = user, %Activity{} = item) do mutes = user.info.mutes || [] reblog_mutes = user.info.muted_reblogs || [] domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks) - %{host: host} = URI.parse(parent.data["actor"]) with parent when not is_nil(parent) <- Object.normalize(item), true <- Enum.all?([blocks, mutes, reblog_mutes], &(item.actor not in &1)), true <- Enum.all?([blocks, mutes], &(parent.data["actor"] not in &1)), + %{host: host} <- URI.parse(parent.data["actor"]), false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host), true <- thread_containment(item, user) do true From f5d574f4ed9aa997a47d03f02adeb701d96f6789 Mon Sep 17 00:00:00 2001 From: sadposter Date: Wed, 24 Jul 2019 11:35:16 +0100 Subject: [PATCH 035/202] check both item and parent domain blocks --- lib/pleroma/web/streamer.ex | 6 ++++-- test/web/streamer_test.exs | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex index e4259e869..9ee331030 100644 --- a/lib/pleroma/web/streamer.ex +++ b/lib/pleroma/web/streamer.ex @@ -239,8 +239,10 @@ defp should_send?(%User{} = user, %Activity{} = item) do with parent when not is_nil(parent) <- Object.normalize(item), true <- Enum.all?([blocks, mutes, reblog_mutes], &(item.actor not in &1)), true <- Enum.all?([blocks, mutes], &(parent.data["actor"] not in &1)), - %{host: host} <- URI.parse(parent.data["actor"]), - false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host), + %{host: item_host} <- URI.parse(item.actor), + %{host: parent_host} <- URI.parse(parent.data["actor"]), + false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, item_host), + false <- Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, parent_host), true <- thread_containment(item, user) do true else diff --git a/test/web/streamer_test.exs b/test/web/streamer_test.exs index 95d5e5d58..d47b37efb 100644 --- a/test/web/streamer_test.exs +++ b/test/web/streamer_test.exs @@ -115,9 +115,10 @@ test "it doesn't send notify to the 'user:notification' stream' when a domain is %{transport_pid: task.pid, assigns: %{user: user}} ) - {:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"}) {:ok, user} = User.block_domain(user, "hecking-lewd-place.com") + {:ok, activity} = CommonAPI.post(user, %{"status" => "super hot take"}) {:ok, notif, _} = CommonAPI.favorite(activity.id, user2) + Streamer.stream("user:notification", notif) Task.await(task) end From 4504135894d5b52c74818fadc3f7ed49ace1702b Mon Sep 17 00:00:00 2001 From: Eugenij Date: Wed, 24 Jul 2019 15:12:27 +0000 Subject: [PATCH 036/202] Add `domain_blocking` to the relationship API (GET /api/v1/accounts/relationships) --- CHANGELOG.md | 1 + lib/pleroma/user.ex | 25 ++++++++++++------- .../web/mastodon_api/views/account_view.ex | 6 ++--- test/web/mastodon_api/account_view_test.exs | 10 ++++++++ 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 35a5a6c21..a3f54d19e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Mastodon API: Add support for categories for custom emojis by reusing the group feature. - Mastodon API: Add support for muting/unmuting notifications - Mastodon API: Add support for the `blocked_by` attribute in the relationship API (`GET /api/v1/accounts/relationships`). +- Mastodon API: Add support for the `domain_blocking` attribute in the relationship API (`GET /api/v1/accounts/relationships`). - Mastodon API: Add `pleroma.deactivated` to the Account entity - Mastodon API: added `/auth/password` endpoint for password reset with rate limit. - Mastodon API: /api/v1/accounts/:id/statuses now supports nicknames or user id diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 982ca8bc1..974f6df18 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -882,19 +882,26 @@ def muted_notifications?(nil, _), do: false def muted_notifications?(user, %{ap_id: ap_id}), do: Enum.member?(user.info.muted_notifications, ap_id) - def blocks?(%User{info: info} = _user, %{ap_id: ap_id}) do - blocks = info.blocks - - domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(info.domain_blocks) - - %{host: host} = URI.parse(ap_id) - - Enum.member?(blocks, ap_id) || - Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host) + def blocks?(%User{} = user, %User{} = target) do + blocks_ap_id?(user, target) || blocks_domain?(user, target) end def blocks?(nil, _), do: false + def blocks_ap_id?(%User{} = user, %User{} = target) do + Enum.member?(user.info.blocks, target.ap_id) + end + + def blocks_ap_id?(_, _), do: false + + def blocks_domain?(%User{} = user, %User{} = target) do + domain_blocks = Pleroma.Web.ActivityPub.MRF.subdomains_regex(user.info.domain_blocks) + %{host: host} = URI.parse(target.ap_id) + Pleroma.Web.ActivityPub.MRF.subdomain_match?(domain_blocks, host) + end + + def blocks_domain?(_, _), do: false + def subscribed_to?(user, %{ap_id: ap_id}) do with %User{} = target <- get_cached_by_ap_id(ap_id) do Enum.member?(target.info.subscribers, user.ap_id) diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index befb35c26..b2b06eeb9 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -50,13 +50,13 @@ def render("relationship.json", %{user: %User{} = user, target: %User{} = target id: to_string(target.id), following: User.following?(user, target), followed_by: User.following?(target, user), - blocking: User.blocks?(user, target), - blocked_by: User.blocks?(target, user), + blocking: User.blocks_ap_id?(user, target), + blocked_by: User.blocks_ap_id?(target, user), muting: User.mutes?(user, target), muting_notifications: User.muted_notifications?(user, target), subscribing: User.subscribed_to?(user, target), requested: requested, - domain_blocking: false, + domain_blocking: User.blocks_domain?(user, target), showing_reblogs: User.showing_reblogs?(user, target), endorsed: false } diff --git a/test/web/mastodon_api/account_view_test.exs b/test/web/mastodon_api/account_view_test.exs index fa44d35cc..905e9af98 100644 --- a/test/web/mastodon_api/account_view_test.exs +++ b/test/web/mastodon_api/account_view_test.exs @@ -231,6 +231,16 @@ test "represent a relationship for the blocking and blocked user" do AccountView.render("relationship.json", %{user: user, target: other_user}) end + test "represent a relationship for the user blocking a domain" do + user = insert(:user) + other_user = insert(:user, ap_id: "https://bad.site/users/other_user") + + {:ok, user} = User.block_domain(user, "bad.site") + + assert %{domain_blocking: true, blocking: false} = + AccountView.render("relationship.json", %{user: user, target: other_user}) + end + test "represent a relationship for the user with a pending follow request" do user = insert(:user) other_user = insert(:user, %{info: %User.Info{locked: true}}) From 55341ac71740d3d8aded9c6520e06b9c509a6670 Mon Sep 17 00:00:00 2001 From: Maksim Date: Wed, 24 Jul 2019 15:13:10 +0000 Subject: [PATCH 037/202] tests WebFinger --- lib/pleroma/plugs/set_format_plug.ex | 24 +++++++++++ lib/pleroma/web/web_finger/web_finger.ex | 3 +- .../web/web_finger/web_finger_controller.ex | 43 +++++++++---------- test/plugs/set_format_plug_test.exs | 38 ++++++++++++++++ test/support/http_request_mock.ex | 9 ++++ .../web_finger/web_finger_controller_test.exs | 43 +++++++++++++++++++ test/web/web_finger/web_finger_test.exs | 5 +++ 7 files changed, 141 insertions(+), 24 deletions(-) create mode 100644 lib/pleroma/plugs/set_format_plug.ex create mode 100644 test/plugs/set_format_plug_test.exs diff --git a/lib/pleroma/plugs/set_format_plug.ex b/lib/pleroma/plugs/set_format_plug.ex new file mode 100644 index 000000000..5ca741c64 --- /dev/null +++ b/lib/pleroma/plugs/set_format_plug.ex @@ -0,0 +1,24 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.SetFormatPlug do + import Plug.Conn, only: [assign: 3, fetch_query_params: 1] + + def init(_), do: nil + + def call(conn, _) do + case get_format(conn) do + nil -> conn + format -> assign(conn, :format, format) + end + end + + defp get_format(conn) do + conn.private[:phoenix_format] || + case fetch_query_params(conn) do + %{query_params: %{"_format" => format}} -> format + _ -> nil + end + end +end diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex index fa34c7ced..0514ade2b 100644 --- a/lib/pleroma/web/web_finger/web_finger.ex +++ b/lib/pleroma/web/web_finger/web_finger.ex @@ -187,6 +187,7 @@ def find_lrdd_template(domain) do end end + @spec finger(String.t()) :: {:ok, map()} | {:error, any()} def finger(account) do account = String.trim_leading(account, "@") @@ -220,8 +221,6 @@ def finger(account) do else with {:ok, doc} <- Jason.decode(body) do webfinger_from_json(doc) - else - {:error, e} -> e end end else diff --git a/lib/pleroma/web/web_finger/web_finger_controller.ex b/lib/pleroma/web/web_finger/web_finger_controller.ex index b77c75ec5..896eb15f9 100644 --- a/lib/pleroma/web/web_finger/web_finger_controller.ex +++ b/lib/pleroma/web/web_finger/web_finger_controller.ex @@ -7,6 +7,7 @@ defmodule Pleroma.Web.WebFinger.WebFingerController do alias Pleroma.Web.WebFinger + plug(Pleroma.Plugs.SetFormatPlug) plug(Pleroma.Web.FederatingPlug) def host_meta(conn, _params) do @@ -17,30 +18,28 @@ def host_meta(conn, _params) do |> send_resp(200, xml) end - def webfinger(conn, %{"resource" => resource}) do - case get_format(conn) do - n when n in ["xml", "xrd+xml"] -> - with {:ok, response} <- WebFinger.webfinger(resource, "XML") do - conn - |> put_resp_content_type("application/xrd+xml") - |> send_resp(200, response) - else - _e -> send_resp(conn, 404, "Couldn't find user") - end - - n when n in ["json", "jrd+json"] -> - with {:ok, response} <- WebFinger.webfinger(resource, "JSON") do - json(conn, response) - else - _e -> send_resp(conn, 404, "Couldn't find user") - end - - _ -> - send_resp(conn, 404, "Unsupported format") + def webfinger(%{assigns: %{format: format}} = conn, %{"resource" => resource}) + when format in ["xml", "xrd+xml"] do + with {:ok, response} <- WebFinger.webfinger(resource, "XML") do + conn + |> put_resp_content_type("application/xrd+xml") + |> send_resp(200, response) + else + _e -> send_resp(conn, 404, "Couldn't find user") end end - def webfinger(conn, _params) do - send_resp(conn, 400, "Bad Request") + def webfinger(%{assigns: %{format: format}} = conn, %{"resource" => resource}) + when format in ["json", "jrd+json"] do + with {:ok, response} <- WebFinger.webfinger(resource, "JSON") do + json(conn, response) + else + _e -> + conn + |> put_status(404) + |> json("Couldn't find user") + end end + + def webfinger(conn, _params), do: send_resp(conn, 400, "Bad Request") end diff --git a/test/plugs/set_format_plug_test.exs b/test/plugs/set_format_plug_test.exs new file mode 100644 index 000000000..bb21956bb --- /dev/null +++ b/test/plugs/set_format_plug_test.exs @@ -0,0 +1,38 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2018 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Plugs.SetFormatPlugTest do + use ExUnit.Case, async: true + use Plug.Test + + alias Pleroma.Plugs.SetFormatPlug + + test "set format from params" do + conn = + :get + |> conn("/cofe?_format=json") + |> SetFormatPlug.call([]) + + assert %{format: "json"} == conn.assigns + end + + test "set format from header" do + conn = + :get + |> conn("/cofe") + |> put_private(:phoenix_format, "xml") + |> SetFormatPlug.call([]) + + assert %{format: "xml"} == conn.assigns + end + + test "doesn't set format" do + conn = + :get + |> conn("/cofe") + |> SetFormatPlug.call([]) + + refute conn.assigns[:format] + end +end diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 7811f7807..31c085e0d 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -614,6 +614,15 @@ def get( }} end + def get( + "https://social.heldscal.la/.well-known/webfinger?resource=invalid_content@social.heldscal.la", + _, + _, + Accept: "application/xrd+xml,application/jrd+json" + ) do + {:ok, %Tesla.Env{status: 200, body: ""}} + end + def get("http://framatube.org/.well-known/host-meta", _, _, _) do {:ok, %Tesla.Env{ diff --git a/test/web/web_finger/web_finger_controller_test.exs b/test/web/web_finger/web_finger_controller_test.exs index a14ed3126..7d861cbf5 100644 --- a/test/web/web_finger/web_finger_controller_test.exs +++ b/test/web/web_finger/web_finger_controller_test.exs @@ -19,6 +19,19 @@ defmodule Pleroma.Web.WebFinger.WebFingerControllerTest do :ok end + test "GET host-meta" do + response = + build_conn() + |> get("/.well-known/host-meta") + + assert response.status == 200 + + assert response.resp_body == + ~s() + end + test "Webfinger JRD" do user = insert(:user) @@ -30,6 +43,16 @@ test "Webfinger JRD" do assert json_response(response, 200)["subject"] == "acct:#{user.nickname}@localhost" end + test "it returns 404 when user isn't found (JSON)" do + result = + build_conn() + |> put_req_header("accept", "application/jrd+json") + |> get("/.well-known/webfinger?resource=acct:jimm@localhost") + |> json_response(404) + + assert result == "Couldn't find user" + end + test "Webfinger XML" do user = insert(:user) @@ -41,6 +64,26 @@ test "Webfinger XML" do assert response(response, 200) end + test "it returns 404 when user isn't found (XML)" do + result = + build_conn() + |> put_req_header("accept", "application/xrd+xml") + |> get("/.well-known/webfinger?resource=acct:jimm@localhost") + |> response(404) + + assert result == "Couldn't find user" + end + + test "Sends a 404 when invalid format" do + user = insert(:user) + + assert_raise Phoenix.NotAcceptableError, fn -> + build_conn() + |> put_req_header("accept", "text/html") + |> get("/.well-known/webfinger?resource=acct:#{user.nickname}@localhost") + end + end + test "Sends a 400 when resource param is missing" do response = build_conn() diff --git a/test/web/web_finger/web_finger_test.exs b/test/web/web_finger/web_finger_test.exs index 0578b4b8e..abf512604 100644 --- a/test/web/web_finger/web_finger_test.exs +++ b/test/web/web_finger/web_finger_test.exs @@ -40,6 +40,11 @@ test "works for ap_ids" do end describe "fingering" do + test "returns error when fails parse xml or json" do + user = "invalid_content@social.heldscal.la" + assert {:error, %Jason.DecodeError{}} = WebFinger.finger(user) + end + test "returns the info for an OStatus user" do user = "shp@social.heldscal.la" From ac27b94ffa49c15850eab591fc1e0e729ddb4167 Mon Sep 17 00:00:00 2001 From: kPherox Date: Wed, 24 Jul 2019 23:38:38 +0900 Subject: [PATCH 038/202] Change to not require `magic-public-key` on WebFinger --- lib/pleroma/web/web_finger/web_finger.ex | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/web_finger/web_finger.ex b/lib/pleroma/web/web_finger/web_finger.ex index fa34c7ced..ad3884c0e 100644 --- a/lib/pleroma/web/web_finger/web_finger.ex +++ b/lib/pleroma/web/web_finger/web_finger.ex @@ -86,11 +86,17 @@ def represent_user(user, "XML") do |> XmlBuilder.to_doc() end - defp get_magic_key(magic_key) do - "data:application/magic-public-key," <> magic_key = magic_key + defp get_magic_key("data:application/magic-public-key," <> magic_key) do {:ok, magic_key} - rescue - MatchError -> {:error, "Missing magic key data."} + end + + defp get_magic_key(nil) do + Logger.debug("Undefined magic key.") + {:ok, nil} + end + + defp get_magic_key(_) do + {:error, "Missing magic key data."} end defp webfinger_from_xml(doc) do From 84fca14c3c6b3a5a6f3b0894903867dfa50a78bb Mon Sep 17 00:00:00 2001 From: feld Date: Wed, 24 Jul 2019 15:35:25 +0000 Subject: [PATCH 039/202] Do not prepend /media/ when using base_url This ensures admin has full control over the path where media resides. --- lib/pleroma/upload.ex | 9 ++++++++- test/upload_test.exs | 46 ++++++++++++++++++++++++++----------------- 2 files changed, 36 insertions(+), 19 deletions(-) diff --git a/lib/pleroma/upload.ex b/lib/pleroma/upload.ex index c47d65241..9f0adde5b 100644 --- a/lib/pleroma/upload.ex +++ b/lib/pleroma/upload.ex @@ -228,7 +228,14 @@ defp url_from_spec(%__MODULE__{name: name}, base_url, {:file, path}) do "" end - [base_url, "media", path] + prefix = + if is_nil(Pleroma.Config.get([__MODULE__, :base_url])) do + "media" + else + "" + end + + [base_url, prefix, path] |> Path.join() end diff --git a/test/upload_test.exs b/test/upload_test.exs index 32c6977d1..95b16078b 100644 --- a/test/upload_test.exs +++ b/test/upload_test.exs @@ -122,24 +122,6 @@ test "returns a media url" do assert String.starts_with?(url, Pleroma.Web.base_url() <> "/media/") end - test "returns a media url with configured base_url" do - base_url = "https://cache.pleroma.social" - - File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") - - file = %Plug.Upload{ - content_type: "image/jpg", - path: Path.absname("test/fixtures/image_tmp.jpg"), - filename: "image.jpg" - } - - {:ok, data} = Upload.store(file, base_url: base_url) - - assert %{"url" => [%{"href" => url}]} = data - - assert String.starts_with?(url, base_url <> "/media/") - end - test "copies the file to the configured folder with deduping" do File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") @@ -266,4 +248,32 @@ test "escapes reserved uri characters" do "%3A%3F%23%5B%5D%40%21%24%26%5C%27%28%29%2A%2B%2C%3B%3D.jpg" end end + + describe "Setting a custom base_url for uploaded media" do + setup do + Pleroma.Config.put([Pleroma.Upload, :base_url], "https://cache.pleroma.social") + + on_exit(fn -> + Pleroma.Config.put([Pleroma.Upload, :base_url], nil) + end) + end + + test "returns a media url with configured base_url" do + base_url = Pleroma.Config.get([Pleroma.Upload, :base_url]) + + File.cp!("test/fixtures/image.jpg", "test/fixtures/image_tmp.jpg") + + file = %Plug.Upload{ + content_type: "image/jpg", + path: Path.absname("test/fixtures/image_tmp.jpg"), + filename: "image.jpg" + } + + {:ok, data} = Upload.store(file, base_url: base_url) + + assert %{"url" => [%{"href" => url}]} = data + + refute String.starts_with?(url, base_url <> "/media/") + end + end end From 8d9f43e1d1a55f445e4e6e4659b9493cd35d99d9 Mon Sep 17 00:00:00 2001 From: kPherox Date: Thu, 25 Jul 2019 01:27:34 +0900 Subject: [PATCH 040/202] Add WebFinger test for AP-only account --- test/fixtures/tesla_mock/kpherox@mstdn.jp.xml | 10 ++++++++++ test/support/http_request_mock.ex | 8 ++++++++ test/web/web_finger/web_finger_test.exs | 14 ++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 test/fixtures/tesla_mock/kpherox@mstdn.jp.xml diff --git a/test/fixtures/tesla_mock/kpherox@mstdn.jp.xml b/test/fixtures/tesla_mock/kpherox@mstdn.jp.xml new file mode 100644 index 000000000..2ec134eaa --- /dev/null +++ b/test/fixtures/tesla_mock/kpherox@mstdn.jp.xml @@ -0,0 +1,10 @@ + + + acct:kPherox@mstdn.jp + https://mstdn.jp/@kPherox + https://mstdn.jp/users/kPherox + + + + + diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 7811f7807..6684a36e7 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -915,6 +915,14 @@ def get("https://info.pleroma.site/activity3.json", _, _, _) do {:ok, %Tesla.Env{status: 404, body: ""}} end + def get("https://mstdn.jp/.well-known/webfinger?resource=acct:kpherox@mstdn.jp", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/kpherox@mstdn.jp.xml") + }} + end + def get(url, query, body, headers) do {:error, "Not implemented the mock response for get #{inspect(url)}, #{query}, #{inspect(body)}, #{ diff --git a/test/web/web_finger/web_finger_test.exs b/test/web/web_finger/web_finger_test.exs index 0578b4b8e..0d3ef6717 100644 --- a/test/web/web_finger/web_finger_test.exs +++ b/test/web/web_finger/web_finger_test.exs @@ -81,6 +81,20 @@ test "returns the correctly for json ostatus users" do assert data["subscribe_address"] == "https://gnusocial.de/main/ostatussub?profile={uri}" end + test "it work for AP-only user" do + user = "kpherox@mstdn.jp" + + {:ok, data} = WebFinger.finger(user) + + assert data["magic_key"] == nil + assert data["salmon"] == nil + + assert data["topic"] == "https://mstdn.jp/users/kPherox.atom" + assert data["subject"] == "acct:kPherox@mstdn.jp" + assert data["ap_id"] == "https://mstdn.jp/users/kPherox" + assert data["subscribe_address"] == "https://mstdn.jp/authorize_interaction?acct={uri}" + end + test "it works for friendica" do user = "lain@squeet.me" From b20020da160404f6f668eec2a2a42aaa2b6a09b2 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Wed, 24 Jul 2019 19:28:21 +0000 Subject: [PATCH 041/202] Show the url advertised in the Activity in the Status JSON response --- .../web/mastodon_api/views/status_view.ex | 2 +- .../tesla_mock/wedistribute-article.json | 18 +++++++++++ .../tesla_mock/wedistribute-user.json | 31 +++++++++++++++++++ test/object/fetcher_test.exs | 7 +++++ test/support/http_request_mock.ex | 16 ++++++++++ test/web/mastodon_api/status_view_test.exs | 10 ++++++ 6 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/tesla_mock/wedistribute-article.json create mode 100644 test/fixtures/tesla_mock/wedistribute-user.json diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index de9425959..80df9b2ac 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -222,7 +222,7 @@ def render("status.json", %{activity: %{data: %{"object" => _object}} = activity if user.local do Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, activity) else - object.data["external_url"] || object.data["id"] + object.data["url"] || object.data["external_url"] || object.data["id"] end %{ diff --git a/test/fixtures/tesla_mock/wedistribute-article.json b/test/fixtures/tesla_mock/wedistribute-article.json new file mode 100644 index 000000000..39dc1b982 --- /dev/null +++ b/test/fixtures/tesla_mock/wedistribute-article.json @@ -0,0 +1,18 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams" + ], + "type": "Article", + "name": "The end is near: Mastodon plans to drop OStatus support", + "content": "\n

The days of OStatus are numbered. The venerable protocol has served as a glue between many different types of servers since the early days of the Fediverse, connecting StatusNet (now GNU Social) to Friendica, Hubzilla, Mastodon, and Pleroma.

\n\n\n\n

Now that many fediverse platforms support ActivityPub as a successor protocol, Mastodon appears to be drawing a line in the sand. In a Patreon update, Eugen Rochko writes:

\n\n\n\n

...OStatus...has overstayed its welcome in the code...and now that most of the network uses ActivityPub, it's time for it to go.

Eugen Rochko, Mastodon creator
\n\n\n\n

The pull request to remove Pubsubhubbub and Salmon, two of the main components of OStatus, has already been merged into Mastodon's master branch.

\n\n\n\n

Some projects will be left in the dark as a side effect of this. GNU Social and PostActiv, for example, both only communicate using OStatus. While some discussion exists regarding adopting ActivityPub for GNU Social, and a plugin is in development, it hasn't been formally adopted yet. We just hope that the Free Software Foundation's instance gets updated in time!

\n", + "summary": "One of the largest platforms in the federated social web is dropping the protocol that it started with.", + "attributedTo": "https://wedistribute.org/wp-json/pterotype/v1/actor/-blog", + "url": "https://wedistribute.org/2019/07/mastodon-drops-ostatus/", + "to": [ + "https://www.w3.org/ns/activitystreams#Public", + "https://wedistribute.org/wp-json/pterotype/v1/actor/-blog/followers" + ], + "id": "https://wedistribute.org/wp-json/pterotype/v1/object/85810", + "likes": "https://wedistribute.org/wp-json/pterotype/v1/object/85810/likes", + "shares": "https://wedistribute.org/wp-json/pterotype/v1/object/85810/shares" +} diff --git a/test/fixtures/tesla_mock/wedistribute-user.json b/test/fixtures/tesla_mock/wedistribute-user.json new file mode 100644 index 000000000..fe2a15703 --- /dev/null +++ b/test/fixtures/tesla_mock/wedistribute-user.json @@ -0,0 +1,31 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers" + } + ], + "type": "Organization", + "id": "https://wedistribute.org/wp-json/pterotype/v1/actor/-blog", + "following": "https://wedistribute.org/wp-json/pterotype/v1/actor/-blog/following", + "followers": "https://wedistribute.org/wp-json/pterotype/v1/actor/-blog/followers", + "liked": "https://wedistribute.org/wp-json/pterotype/v1/actor/-blog/liked", + "inbox": "https://wedistribute.org/wp-json/pterotype/v1/actor/-blog/inbox", + "outbox": "https://wedistribute.org/wp-json/pterotype/v1/actor/-blog/outbox", + "name": "We Distribute", + "preferredUsername": "blog", + "summary": "

Connecting many threads in the federated web. We Distribute is an independent publication dedicated to the fediverse, decentralization, P2P technologies, and Free Software!

", + "url": "https://wedistribute.org/", + "publicKey": { + "id": "https://wedistribute.org/wp-json/pterotype/v1/actor/-blog#publicKey", + "owner": "https://wedistribute.org/wp-json/pterotype/v1/actor/-blog", + "publicKeyPem": "-----BEGIN PUBLIC KEY-----\r\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1bmUJ+y8PS8JFVi0KugN\r\nFl4pLvLog3V2lsV9ftmCXpveB/WJx66Tr1fQLsU3qYvQFc8UPGWD52zV4RENR1SN\r\nx0O6T2f97KUbRM+Ckow7Jyjtssgl+Mqq8UBZQ/+H8I/1Vpvt5E5hUykhFgwzx9qg\r\nzoIA3OK7alOpQbSoKXo0QcOh6yTRUnMSRMJAgUoZJzzXI/FmH/DtKr7ziQ1T2KWs\r\nVs8mWnTb/OlCxiheLuMlmJNMF+lPyVthvMIxF6Z5gV9d5QAmASSCI628e6uH2EUF\r\nDEEF5jo+Z5ffeNv28953lrnM+VB/wTjl3tYA+zCQeAmUPksX3E+YkXGxj+4rxBAY\r\n8wIDAQAB\r\n-----END PUBLIC KEY-----" + }, + "manuallyApprovesFollowers": false, + "icon": { + "url": "https://wedistribute.org/wp-content/uploads/2019/02/b067de423757a538.png", + "type": "Image", + "mediaType": "image/png" + } +} diff --git a/test/object/fetcher_test.exs b/test/object/fetcher_test.exs index 482252cff..0ca87f035 100644 --- a/test/object/fetcher_test.exs +++ b/test/object/fetcher_test.exs @@ -110,6 +110,13 @@ test "it can fetch peertube videos" do assert object end + test "it can fetch wedistribute articles" do + {:ok, object} = + Fetcher.fetch_object_from_id("https://wedistribute.org/wp-json/pterotype/v1/object/85810") + + assert object + end + test "all objects with fake directions are rejected by the object fetcher" do assert {:error, _} = Fetcher.fetch_and_contain_remote_object_from_id( diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 31c085e0d..55cfc1e00 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -301,6 +301,22 @@ def get("https://baptiste.gelez.xyz/~/PlumeDevelopment/this-month-in-plume-june- }} end + def get("https://wedistribute.org/wp-json/pterotype/v1/object/85810", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/wedistribute-article.json") + }} + end + + def get("https://wedistribute.org/wp-json/pterotype/v1/actor/-blog", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/tesla_mock/wedistribute-user.json") + }} + end + def get("http://mastodon.example.org/users/admin", _, _, Accept: "application/activity+json") do {:ok, %Tesla.Env{ diff --git a/test/web/mastodon_api/status_view_test.exs b/test/web/mastodon_api/status_view_test.exs index 3447c5b1f..0b167f839 100644 --- a/test/web/mastodon_api/status_view_test.exs +++ b/test/web/mastodon_api/status_view_test.exs @@ -300,6 +300,16 @@ test "attachments" do assert %{id: "2"} = StatusView.render("attachment.json", %{attachment: object}) end + test "put the url advertised in the Activity in to the url attribute" do + id = "https://wedistribute.org/wp-json/pterotype/v1/object/85810" + [activity] = Activity.search(nil, id) + + status = StatusView.render("status.json", %{activity: activity}) + + assert status.uri == id + assert status.url == "https://wedistribute.org/2019/07/mastodon-drops-ostatus/" + end + test "a reblog" do user = insert(:user) activity = insert(:note_activity) From 03c386614fabe9754ba82a1e89f6cf0ef518f61c Mon Sep 17 00:00:00 2001 From: Maksim Date: Thu, 25 Jul 2019 03:43:13 +0000 Subject: [PATCH 042/202] fixed test for elixir 1.7.4 --- test/upload/filter/dedupe_test.exs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/upload/filter/dedupe_test.exs b/test/upload/filter/dedupe_test.exs index fddd594dc..3de94dc20 100644 --- a/test/upload/filter/dedupe_test.exs +++ b/test/upload/filter/dedupe_test.exs @@ -25,7 +25,7 @@ test "adds shasum" do assert { :ok, - %Pleroma.Upload{id: @shasum, path: "#{@shasum}.jpg"} + %Pleroma.Upload{id: @shasum, path: @shasum <> ".jpg"} } = Dedupe.filter(upload) end end From 7c8abbcb1ccafa79372fd75fdec87db2207b61ec Mon Sep 17 00:00:00 2001 From: "Haelwenn (lanodan) Monnier" Date: Fri, 26 Jul 2019 15:33:25 +0200 Subject: [PATCH 043/202] CHANGELOG.md: Add entry for !1484 Related to: https://git.pleroma.social/pleroma/pleroma/merge_requests/1484 [ci skip] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3f54d19e..bd3048b19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] ### Changed - **Breaking:** Configuration: A setting to explicitly disable the mailer was added, defaulting to true, if you are using a mailer add `config :pleroma, Pleroma.Emails.Mailer, enabled: true` to your config +- **Breaking:** Configuration: `/media/` is now removed when `base_url` is configured, append `/media/` to your `base_url` config to keep the old behaviour if desired - Configuration: OpenGraph and TwitterCard providers enabled by default - Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text - Federation: Return 403 errors when trying to request pages from a user's follower/following collections if they have `hide_followers`/`hide_follows` set From 6b77a88365f3a58cf8d1f9c00ed04532f706b87c Mon Sep 17 00:00:00 2001 From: Maksim Date: Fri, 26 Jul 2019 20:27:38 +0000 Subject: [PATCH 044/202] [#1097] added redirect: /pleroma/admin -> /pleroma/admin/ --- .../web/fallback_redirect_controller.ex | 77 +++++++++++++++++++ lib/pleroma/web/router.ex | 65 ---------------- test/web/fallback_test.exs | 4 + 3 files changed, 81 insertions(+), 65 deletions(-) create mode 100644 lib/pleroma/web/fallback_redirect_controller.ex diff --git a/lib/pleroma/web/fallback_redirect_controller.ex b/lib/pleroma/web/fallback_redirect_controller.ex new file mode 100644 index 000000000..5fbf3695f --- /dev/null +++ b/lib/pleroma/web/fallback_redirect_controller.ex @@ -0,0 +1,77 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Fallback.RedirectController do + use Pleroma.Web, :controller + require Logger + alias Pleroma.User + alias Pleroma.Web.Metadata + + def api_not_implemented(conn, _params) do + conn + |> put_status(404) + |> json(%{error: "Not implemented"}) + end + + def redirector(conn, _params, code \\ 200) + + # redirect to admin section + # /pleroma/admin -> /pleroma/admin/ + # + def redirector(conn, %{"path" => ["pleroma", "admin"]} = _, _code) do + redirect(conn, to: "/pleroma/admin/") + end + + def redirector(conn, _params, code) do + conn + |> put_resp_content_type("text/html") + |> send_file(code, index_file_path()) + end + + def redirector_with_meta(conn, %{"maybe_nickname_or_id" => maybe_nickname_or_id} = params) do + with %User{} = user <- User.get_cached_by_nickname_or_id(maybe_nickname_or_id) do + redirector_with_meta(conn, %{user: user}) + else + nil -> + redirector(conn, params) + end + end + + def redirector_with_meta(conn, params) do + {:ok, index_content} = File.read(index_file_path()) + + tags = + try do + Metadata.build_tags(params) + rescue + e -> + Logger.error( + "Metadata rendering for #{conn.request_path} failed.\n" <> + Exception.format(:error, e, __STACKTRACE__) + ) + + "" + end + + response = String.replace(index_content, "", tags) + + conn + |> put_resp_content_type("text/html") + |> send_resp(200, response) + end + + def index_file_path do + Pleroma.Plugs.InstanceStatic.file_path("index.html") + end + + def registration_page(conn, params) do + redirector(conn, params) + end + + def empty(conn, _params) do + conn + |> put_status(204) + |> text("") + end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index a9f3826fc..47ee762dc 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -729,68 +729,3 @@ defmodule Pleroma.Web.Router do options("/*path", RedirectController, :empty) end end - -defmodule Fallback.RedirectController do - use Pleroma.Web, :controller - require Logger - alias Pleroma.User - alias Pleroma.Web.Metadata - - def api_not_implemented(conn, _params) do - conn - |> put_status(404) - |> json(%{error: "Not implemented"}) - end - - def redirector(conn, _params, code \\ 200) do - conn - |> put_resp_content_type("text/html") - |> send_file(code, index_file_path()) - end - - def redirector_with_meta(conn, %{"maybe_nickname_or_id" => maybe_nickname_or_id} = params) do - with %User{} = user <- User.get_cached_by_nickname_or_id(maybe_nickname_or_id) do - redirector_with_meta(conn, %{user: user}) - else - nil -> - redirector(conn, params) - end - end - - def redirector_with_meta(conn, params) do - {:ok, index_content} = File.read(index_file_path()) - - tags = - try do - Metadata.build_tags(params) - rescue - e -> - Logger.error( - "Metadata rendering for #{conn.request_path} failed.\n" <> - Exception.format(:error, e, __STACKTRACE__) - ) - - "" - end - - response = String.replace(index_content, "", tags) - - conn - |> put_resp_content_type("text/html") - |> send_resp(200, response) - end - - def index_file_path do - Pleroma.Plugs.InstanceStatic.file_path("index.html") - end - - def registration_page(conn, params) do - redirector(conn, params) - end - - def empty(conn, _params) do - conn - |> put_status(204) - |> text("") - end -end diff --git a/test/web/fallback_test.exs b/test/web/fallback_test.exs index cc78b3ae1..c13db9526 100644 --- a/test/web/fallback_test.exs +++ b/test/web/fallback_test.exs @@ -30,6 +30,10 @@ test "GET /api*path", %{conn: conn} do |> json_response(404) == %{"error" => "Not implemented"} end + test "GET /pleroma/admin -> /pleroma/admin/", %{conn: conn} do + assert redirected_to(get(conn, "/pleroma/admin")) =~ "/pleroma/admin/" + end + test "GET /*path", %{conn: conn} do assert conn |> get("/foo") From 961e7785314688b9e2445649c71e12023a982165 Mon Sep 17 00:00:00 2001 From: Thomas Sileo Date: Sun, 28 Jul 2019 14:17:56 +0200 Subject: [PATCH 045/202] Fix HTTP sig tweak on KeyId --- lib/pleroma/signature.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pleroma/signature.ex b/lib/pleroma/signature.ex index 0bf49fd7c..15bf3c317 100644 --- a/lib/pleroma/signature.ex +++ b/lib/pleroma/signature.ex @@ -15,7 +15,7 @@ def key_id_to_actor_id(key_id) do |> Map.put(:fragment, nil) uri = - if String.ends_with?(uri.path, "/publickey") do + if not is_nil(uri.path) and String.ends_with?(uri.path, "/publickey") do Map.put(uri, :path, String.replace(uri.path, "/publickey", "")) else uri From 02dc651828af00ba88a687570833333d4b939c7e Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Sun, 28 Jul 2019 20:24:39 +0000 Subject: [PATCH 046/202] Handle 303 redirects --- lib/pleroma/http/connection.ex | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/pleroma/http/connection.ex b/lib/pleroma/http/connection.ex index a1460d303..7e2c6f5e8 100644 --- a/lib/pleroma/http/connection.ex +++ b/lib/pleroma/http/connection.ex @@ -11,6 +11,7 @@ defmodule Pleroma.HTTP.Connection do connect_timeout: 10_000, recv_timeout: 20_000, follow_redirect: true, + force_redirect: true, pool: :federation ] @adapter Application.get_env(:tesla, :adapter) From 6a4b8b2681023dc355331999aeac6c24c5a21f7f Mon Sep 17 00:00:00 2001 From: Maksim Date: Sun, 28 Jul 2019 20:29:26 +0000 Subject: [PATCH 047/202] fixed User.update_and_set_cache for stale user --- lib/pleroma/user.ex | 2 +- test/user_test.exs | 24 ++++++++++++++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 974f6df18..6e2fd3af8 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -471,7 +471,7 @@ def set_cache(%User{} = user) do end def update_and_set_cache(changeset) do - with {:ok, user} <- Repo.update(changeset) do + with {:ok, user} <- Repo.update(changeset, stale_error_field: :id) do set_cache(user) else e -> e diff --git a/test/user_test.exs b/test/user_test.exs index 8a7b7537f..556df45fd 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -1369,4 +1369,28 @@ test "user with internal-prefixed nickname returns true" do assert User.is_internal_user?(user) end end + + describe "update_and_set_cache/1" do + test "returns error when user is stale instead Ecto.StaleEntryError" do + user = insert(:user) + + changeset = Ecto.Changeset.change(user, bio: "test") + + Repo.delete(user) + + assert {:error, %Ecto.Changeset{errors: [id: {"is stale", [stale: true]}], valid?: false}} = + User.update_and_set_cache(changeset) + end + + test "performs update cache if user updated" do + user = insert(:user) + assert {:ok, nil} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}") + + changeset = Ecto.Changeset.change(user, bio: "test-bio") + + assert {:ok, %User{bio: "test-bio"} = user} = User.update_and_set_cache(changeset) + assert {:ok, user} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}") + assert %User{bio: "test-bio"} = User.get_cached_by_ap_id(user.ap_id) + end + end end From 242f5c585ed797917ba8c61ceb5d266f4c670c90 Mon Sep 17 00:00:00 2001 From: Sachin Joshi Date: Sun, 28 Jul 2019 20:30:10 +0000 Subject: [PATCH 048/202] add account confirmation email resend in mastodon api --- CHANGELOG.md | 1 + config/config.exs | 3 +- docs/api/pleroma_api.md | 8 ++++ .../mastodon_api/mastodon_api_controller.ex | 14 +++++++ lib/pleroma/web/router.ex | 6 +++ .../mastodon_api_controller_test.exs | 41 +++++++++++++++++++ 6 files changed, 72 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd3048b19..20f4eea41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - ActivityPub: Add an internal service actor for fetching ActivityPub objects. - 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. ### Changed - Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text diff --git a/config/config.exs b/config/config.exs index 569411866..17770640a 100644 --- a/config/config.exs +++ b/config/config.exs @@ -534,7 +534,8 @@ relation_id_action: {60_000, 2}, statuses_actions: {10_000, 15}, status_id_action: {60_000, 3}, - password_reset: {1_800_000, 5} + password_reset: {1_800_000, 5}, + account_confirmation_resend: {8_640_000, 5} # Import environment specific config. This must remain at the bottom # of this file so it overrides the configuration defined above. diff --git a/docs/api/pleroma_api.md b/docs/api/pleroma_api.md index d83ebd734..5698e88ac 100644 --- a/docs/api/pleroma_api.md +++ b/docs/api/pleroma_api.md @@ -245,6 +245,14 @@ See [Admin-API](Admin-API.md) - PATCH `/api/v1/pleroma/accounts/update_banner`: Set/clear user banner image - PATCH `/api/v1/pleroma/accounts/update_background`: Set/clear user background image +## `/api/v1/pleroma/accounts/confirmation_resend` +### Resend confirmation email +* Method `POST` +* Params: + * `email`: email of that needs to be verified +* Authentication: not required +* Response: 204 No Content + ## `/api/v1/pleroma/mascot` ### Gets user mascot image * Method `GET` diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index d660f3f05..5bdbb709b 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -4,6 +4,9 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do use Pleroma.Web, :controller + + import Pleroma.Web.ControllerHelper, only: [json_response: 3] + alias Ecto.Changeset alias Pleroma.Activity alias Pleroma.Bookmark @@ -74,6 +77,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do plug(RateLimiter, :app_account_creation when action == :account_register) plug(RateLimiter, :search when action in [:search, :search2, :account_search]) plug(RateLimiter, :password_reset when action == :password_reset) + plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend) @local_mastodon_name "Mastodon-Local" @@ -1839,6 +1843,16 @@ def password_reset(conn, params) do end end + def account_confirmation_resend(conn, params) do + nickname_or_email = params["email"] || params["nickname"] + + with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email), + {:ok, _} <- User.try_send_confirmation_email(user) do + conn + |> json_response(:no_content, "") + end + end + def try_render(conn, target, params) when is_binary(target) do case render(conn, target, params) do diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 47ee762dc..4e1ab6c33 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -412,6 +412,12 @@ defmodule Pleroma.Web.Router do get("/accounts/search", SearchController, :account_search) + post( + "/pleroma/accounts/confirmation_resend", + MastodonAPIController, + :account_confirmation_resend + ) + scope [] do pipe_through(:oauth_read_or_public) diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index ce2e44499..d7f92fac2 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -3923,4 +3923,45 @@ test "it returns 400 when user is not local", %{conn: conn, user: user} do assert conn.resp_body == "" end end + + describe "POST /api/v1/pleroma/accounts/confirmation_resend" do + setup do + setting = Pleroma.Config.get([:instance, :account_activation_required]) + + unless setting do + Pleroma.Config.put([:instance, :account_activation_required], true) + on_exit(fn -> Pleroma.Config.put([:instance, :account_activation_required], setting) end) + end + + user = insert(:user) + info_change = User.Info.confirmation_changeset(user.info, need_confirmation: true) + + {:ok, user} = + user + |> Changeset.change() + |> Changeset.put_embed(:info, info_change) + |> Repo.update() + + assert user.info.confirmation_pending + + [user: user] + end + + test "resend account confirmation email", %{conn: conn, user: user} do + conn + |> assign(:user, user) + |> post("/api/v1/pleroma/accounts/confirmation_resend?email=#{user.email}") + |> json_response(:no_content) + + email = Pleroma.Emails.UserEmail.account_confirmation_email(user) + notify_email = Pleroma.Config.get([:instance, :notify_email]) + instance_name = Pleroma.Config.get([:instance, :name]) + + assert_email_sent( + from: {instance_name, notify_email}, + to: {user.name, user.email}, + html_body: email.html_body + ) + end + end end From 492d854e7aa29a2438dbbe2f95e509e43328eb7f Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Sun, 28 Jul 2019 21:29:10 +0000 Subject: [PATCH 049/202] transmogrifier: use User.delete() instead of handrolled user deletion code for remote users Closes #1104 --- CHANGELOG.md | 1 + .../web/activity_pub/transmogrifier.ex | 15 +---- test/notification_test.exs | 58 +++++++++++++++++++ 3 files changed, 60 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 20f4eea41..48379b757 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Rich Media: Parser failing when no TTL can be found by image TTL setters - Rich Media: The crawled URL is now spliced into the rich media data. - ActivityPub S2S: sharedInbox usage has been mostly aligned with the rules in the AP specification. +- ActivityPub S2S: remote user deletions now work the same as local user deletions. ### Added - MRF: Support for priming the mediaproxy cache (`Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 602ae48e1..7f06e6edd 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -656,20 +656,7 @@ def handle_incoming( nil -> case User.get_cached_by_ap_id(object_id) do %User{ap_id: ^actor} = user -> - {:ok, followers} = User.get_followers(user) - - Enum.each(followers, fn follower -> - User.unfollow(follower, user) - end) - - {:ok, friends} = User.get_friends(user) - - Enum.each(friends, fn followed -> - User.unfollow(user, followed) - end) - - User.invalidate_cache(user) - Repo.delete(user) + User.delete(user) nil -> :error diff --git a/test/notification_test.exs b/test/notification_test.exs index 28f8df49d..c88ac54bd 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -564,6 +564,64 @@ test "replying to a deleted post without tagging does not generate a notificatio assert Enum.empty?(Notification.for_user(user)) end + + test "notifications are deleted if a local user is deleted" do + user = insert(:user) + other_user = insert(:user) + + {:ok, _activity} = + CommonAPI.post(user, %{"status" => "hi @#{other_user.nickname}", "visibility" => "direct"}) + + refute Enum.empty?(Notification.for_user(other_user)) + + User.delete(user) + + assert Enum.empty?(Notification.for_user(other_user)) + end + + test "notifications are deleted if a remote user is deleted" do + remote_user = insert(:user) + local_user = insert(:user) + + dm_message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "type" => "Create", + "actor" => remote_user.ap_id, + "id" => remote_user.ap_id <> "/activities/test", + "to" => [local_user.ap_id], + "cc" => [], + "object" => %{ + "type" => "Note", + "content" => "Hello!", + "tag" => [ + %{ + "type" => "Mention", + "href" => local_user.ap_id, + "name" => "@#{local_user.nickname}" + } + ], + "to" => [local_user.ap_id], + "cc" => [], + "attributedTo" => remote_user.ap_id + } + } + + {:ok, _dm_activity} = Transmogrifier.handle_incoming(dm_message) + + refute Enum.empty?(Notification.for_user(local_user)) + + delete_user_message = %{ + "@context" => "https://www.w3.org/ns/activitystreams", + "id" => remote_user.ap_id <> "/activities/delete", + "actor" => remote_user.ap_id, + "type" => "Delete", + "object" => remote_user.ap_id + } + + {:ok, _delete_activity} = Transmogrifier.handle_incoming(delete_user_message) + + assert Enum.empty?(Notification.for_user(local_user)) + end end describe "for_user" do From 9d78b3b281ff7f758d4e0dce19fd74d938e47ccc Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Mon, 29 Jul 2019 02:12:35 +0000 Subject: [PATCH 050/202] mix: add ex_const dependency --- mix.exs | 1 + mix.lock | 1 + 2 files changed, 2 insertions(+) diff --git a/mix.exs b/mix.exs index e69940c5d..2a8fe2e9d 100644 --- a/mix.exs +++ b/mix.exs @@ -150,6 +150,7 @@ defp deps do {:benchee, "~> 1.0"}, {:esshd, "~> 0.1.0", runtime: Application.get_env(:esshd, :enabled, false)}, {:ex_rated, "~> 1.3"}, + {:ex_const, "~> 0.2"}, {:plug_static_index_html, "~> 1.0.0"}, {:excoveralls, "~> 0.11.1", only: :test}, {:mox, "~> 0.5", only: :test} diff --git a/mix.lock b/mix.lock index 5f20878d3..65da7be8b 100644 --- a/mix.lock +++ b/mix.lock @@ -27,6 +27,7 @@ "ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [: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_const": {:hex, :ex_const, "0.2.4", "d06e540c9d834865b012a17407761455efa71d0ce91e5831e86881b9c9d82448", [:mix], [], "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"}, "ex_machina": {:hex, :ex_machina, "2.3.0", "92a5ad0a8b10ea6314b876a99c8c9e3f25f4dde71a2a835845b136b9adaf199a", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm"}, "ex_rated": {:hex, :ex_rated, "1.3.3", "30ecbdabe91f7eaa9d37fa4e81c85ba420f371babeb9d1910adbcd79ec798d27", [:mix], [{:ex2ms, "~> 1.5", [hex: :ex2ms, repo: "hexpm", optional: false]}], "hexpm"}, From b93498eb5289dc92587b77c316ed9f697bb9e5c8 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Mon, 29 Jul 2019 02:43:19 +0000 Subject: [PATCH 051/202] constants: add as_public constant and use it everywhere --- lib/mix/tasks/pleroma/database.ex | 12 ++++++--- lib/pleroma/activity/search.ex | 4 ++- lib/pleroma/constants.ex | 9 +++++++ lib/pleroma/web/activity_pub/activity_pub.ex | 27 +++++++------------ .../web/activity_pub/mrf/hellthread_policy.ex | 11 +++++--- .../web/activity_pub/mrf/keyword_policy.ex | 8 +++--- .../web/activity_pub/mrf/reject_non_public.ex | 6 ++--- .../web/activity_pub/mrf/simple_policy.ex | 12 ++++----- .../web/activity_pub/mrf/tag_policy.ex | 15 ++++++----- lib/pleroma/web/activity_pub/publisher.ex | 6 ++--- .../web/activity_pub/transmogrifier.ex | 11 ++++---- lib/pleroma/web/activity_pub/utils.ex | 13 ++++----- lib/pleroma/web/activity_pub/visibility.ex | 8 +++--- lib/pleroma/web/auth/authenticator.ex | 3 +-- lib/pleroma/web/common_api/common_api.ex | 3 +-- lib/pleroma/web/common_api/utils.ex | 5 ++-- .../mastodon_api/mastodon_api_controller.ex | 6 ++--- lib/pleroma/web/oauth/oauth_controller.ex | 3 +-- lib/pleroma/web/oauth/token.ex | 3 +-- .../web/ostatus/activity_representer.ex | 3 ++- .../web/ostatus/handlers/note_handler.ex | 5 ++-- .../rich_media/parsers/ttl/aws_signed_url.ex | 3 +-- lib/pleroma/web/twitter_api/twitter_api.ex | 4 ++- .../web/twitter_api/views/activity_view.ex | 3 ++- .../twitter_api/views/notification_view.ex | 4 ++- test/web/push/impl_test.exs | 6 ++--- 26 files changed, 104 insertions(+), 89 deletions(-) create mode 100644 lib/pleroma/constants.ex diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex index e91fb31d1..8547a329a 100644 --- a/lib/mix/tasks/pleroma/database.ex +++ b/lib/mix/tasks/pleroma/database.ex @@ -8,6 +8,7 @@ defmodule Mix.Tasks.Pleroma.Database do alias Pleroma.Repo alias Pleroma.User require Logger + require Pleroma.Constants import Mix.Pleroma use Mix.Task @@ -99,10 +100,15 @@ def run(["prune_objects" | args]) do NaiveDateTime.utc_now() |> NaiveDateTime.add(-(deadline * 86_400)) - public = "https://www.w3.org/ns/activitystreams#Public" - from(o in Object, - where: fragment("?->'to' \\? ? OR ?->'cc' \\? ?", o.data, ^public, o.data, ^public), + where: + fragment( + "?->'to' \\? ? OR ?->'cc' \\? ?", + o.data, + ^Pleroma.Constants.as_public(), + o.data, + ^Pleroma.Constants.as_public() + ), where: o.inserted_at < ^time_deadline, where: fragment("split_part(?->>'actor', '/', 3) != ?", o.data, ^Pleroma.Web.Endpoint.host()) diff --git a/lib/pleroma/activity/search.ex b/lib/pleroma/activity/search.ex index 0cc3770a7..f847ac238 100644 --- a/lib/pleroma/activity/search.ex +++ b/lib/pleroma/activity/search.ex @@ -9,6 +9,8 @@ defmodule Pleroma.Activity.Search do alias Pleroma.User alias Pleroma.Web.ActivityPub.Visibility + require Pleroma.Constants + import Ecto.Query def search(user, search_query, options \\ []) do @@ -39,7 +41,7 @@ def maybe_restrict_author(query, _), do: query defp restrict_public(q) do from([a, o] in q, where: fragment("?->>'type' = 'Create'", a.data), - where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients + where: ^Pleroma.Constants.as_public() in a.recipients ) end diff --git a/lib/pleroma/constants.ex b/lib/pleroma/constants.ex new file mode 100644 index 000000000..ef1418543 --- /dev/null +++ b/lib/pleroma/constants.ex @@ -0,0 +1,9 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Constants do + use Const + + const(as_public, do: "https://www.w3.org/ns/activitystreams#Public") +end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index a42c50875..6fd7fef92 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -23,6 +23,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do import Pleroma.Web.ActivityPub.Visibility require Logger + require Pleroma.Constants # For Announce activities, we filter the recipients based on following status for any actors # that match actual users. See issue #164 for more information about why this is necessary. @@ -207,8 +208,6 @@ def stream_out_participations(%Object{data: %{"context" => context}}, user) do def stream_out_participations(_, _), do: :noop def stream_out(activity) do - public = "https://www.w3.org/ns/activitystreams#Public" - if activity.data["type"] in ["Create", "Announce", "Delete"] do object = Object.normalize(activity) # Do not stream out poll replies @@ -216,7 +215,7 @@ def stream_out(activity) do Pleroma.Web.Streamer.stream("user", activity) Pleroma.Web.Streamer.stream("list", activity) - if Enum.member?(activity.data["to"], public) do + if get_visibility(activity) == "public" do Pleroma.Web.Streamer.stream("public", activity) if activity.local do @@ -238,13 +237,8 @@ def stream_out(activity) do end end else - # TODO: Write test, replace with visibility test - if !Enum.member?(activity.data["cc"] || [], public) && - !Enum.member?( - activity.data["to"], - User.get_cached_by_ap_id(activity.data["actor"]).follower_address - ), - do: Pleroma.Web.Streamer.stream("direct", activity) + if get_visibility(activity) == "direct", + do: Pleroma.Web.Streamer.stream("direct", activity) end end end @@ -514,7 +508,7 @@ def flag( end defp fetch_activities_for_context_query(context, opts) do - public = ["https://www.w3.org/ns/activitystreams#Public"] + public = [Pleroma.Constants.as_public()] recipients = if opts["user"], do: [opts["user"].ap_id | opts["user"].following] ++ public, else: public @@ -555,7 +549,7 @@ def fetch_latest_activity_id_for_context(context, opts \\ %{}) do end def fetch_public_activities(opts \\ %{}) do - q = fetch_activities_query(["https://www.w3.org/ns/activitystreams#Public"], opts) + q = fetch_activities_query([Pleroma.Constants.as_public()], opts) q |> restrict_unlisted() @@ -646,10 +640,9 @@ defp user_activities_recipients(%{"godmode" => true}) do defp user_activities_recipients(%{"reading_user" => reading_user}) do if reading_user do - ["https://www.w3.org/ns/activitystreams#Public"] ++ - [reading_user.ap_id | reading_user.following] + [Pleroma.Constants.as_public()] ++ [reading_user.ap_id | reading_user.following] else - ["https://www.w3.org/ns/activitystreams#Public"] + [Pleroma.Constants.as_public()] end end @@ -834,7 +827,7 @@ defp restrict_unlisted(query) do fragment( "not (coalesce(?->'cc', '{}'::jsonb) \\?| ?)", activity.data, - ^["https://www.w3.org/ns/activitystreams#Public"] + ^[Pleroma.Constants.as_public()] ) ) end @@ -971,7 +964,7 @@ def fetch_activities_bounded_query(query, recipients, recipients_with_public) do where: fragment("? && ?", activity.recipients, ^recipients) or (fragment("? && ?", activity.recipients, ^recipients_with_public) and - "https://www.w3.org/ns/activitystreams#Public" in activity.recipients) + ^Pleroma.Constants.as_public() in activity.recipients) ) end diff --git a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex index a699f6a7e..377987cf2 100644 --- a/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/hellthread_policy.ex @@ -4,6 +4,9 @@ defmodule Pleroma.Web.ActivityPub.MRF.HellthreadPolicy do alias Pleroma.User + + require Pleroma.Constants + @moduledoc "Block messages with too much mentions (configurable)" @behaviour Pleroma.Web.ActivityPub.MRF @@ -19,12 +22,12 @@ defp delist_message(message, threshold) when threshold > 0 do when follower_collection? and recipients > threshold -> message |> Map.put("to", [follower_collection]) - |> Map.put("cc", ["https://www.w3.org/ns/activitystreams#Public"]) + |> Map.put("cc", [Pleroma.Constants.as_public()]) {:public, recipients} when recipients > threshold -> message |> Map.put("to", []) - |> Map.put("cc", ["https://www.w3.org/ns/activitystreams#Public"]) + |> Map.put("cc", [Pleroma.Constants.as_public()]) _ -> message @@ -51,10 +54,10 @@ defp get_recipient_count(message) do recipients = (message["to"] || []) ++ (message["cc"] || []) follower_collection = User.get_cached_by_ap_id(message["actor"]).follower_address - if Enum.member?(recipients, "https://www.w3.org/ns/activitystreams#Public") do + if Enum.member?(recipients, Pleroma.Constants.as_public()) do recipients = recipients - |> List.delete("https://www.w3.org/ns/activitystreams#Public") + |> List.delete(Pleroma.Constants.as_public()) |> List.delete(follower_collection) {:public, length(recipients)} diff --git a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex index d5c341433..4eec8b916 100644 --- a/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/keyword_policy.ex @@ -3,6 +3,8 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Web.ActivityPub.MRF.KeywordPolicy do + require Pleroma.Constants + @moduledoc "Reject or Word-Replace messages with a keyword or regex" @behaviour Pleroma.Web.ActivityPub.MRF @@ -31,12 +33,12 @@ defp check_reject(%{"object" => %{"content" => content, "summary" => summary}} = defp check_ftl_removal( %{"to" => to, "object" => %{"content" => content, "summary" => summary}} = message ) do - if "https://www.w3.org/ns/activitystreams#Public" in to and + if Pleroma.Constants.as_public() in to and Enum.any?(Pleroma.Config.get([:mrf_keyword, :federated_timeline_removal]), fn pattern -> string_matches?(content, pattern) or string_matches?(summary, pattern) end) do - to = List.delete(to, "https://www.w3.org/ns/activitystreams#Public") - cc = ["https://www.w3.org/ns/activitystreams#Public" | message["cc"] || []] + to = List.delete(to, Pleroma.Constants.as_public()) + cc = [Pleroma.Constants.as_public() | message["cc"] || []] message = message diff --git a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex index da13fd7c7..457b6ee10 100644 --- a/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex +++ b/lib/pleroma/web/activity_pub/mrf/reject_non_public.ex @@ -10,7 +10,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.RejectNonPublic do @behaviour Pleroma.Web.ActivityPub.MRF - @public "https://www.w3.org/ns/activitystreams#Public" + require Pleroma.Constants @impl true def filter(%{"type" => "Create"} = object) do @@ -19,8 +19,8 @@ def filter(%{"type" => "Create"} = object) do # Determine visibility visibility = cond do - @public in object["to"] -> "public" - @public in object["cc"] -> "unlisted" + Pleroma.Constants.as_public() in object["to"] -> "public" + Pleroma.Constants.as_public() in object["cc"] -> "unlisted" user.follower_address in object["to"] -> "followers" true -> "direct" end diff --git a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex index 2cf63d3db..f266457e3 100644 --- a/lib/pleroma/web/activity_pub/mrf/simple_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/simple_policy.ex @@ -8,6 +8,8 @@ defmodule Pleroma.Web.ActivityPub.MRF.SimplePolicy do @moduledoc "Filter activities depending on their origin instance" @behaviour MRF + require Pleroma.Constants + defp check_accept(%{host: actor_host} = _actor_info, object) do accepts = Pleroma.Config.get([:mrf_simple, :accept]) @@ -89,14 +91,10 @@ defp check_ftl_removal(%{host: actor_host} = _actor_info, object) do object = with true <- MRF.subdomain_match?(timeline_removal, actor_host), user <- User.get_cached_by_ap_id(object["actor"]), - true <- "https://www.w3.org/ns/activitystreams#Public" in object["to"] do - to = - List.delete(object["to"], "https://www.w3.org/ns/activitystreams#Public") ++ - [user.follower_address] + true <- Pleroma.Constants.as_public() in object["to"] do + to = List.delete(object["to"], Pleroma.Constants.as_public()) ++ [user.follower_address] - cc = - List.delete(object["cc"], user.follower_address) ++ - ["https://www.w3.org/ns/activitystreams#Public"] + cc = List.delete(object["cc"], user.follower_address) ++ [Pleroma.Constants.as_public()] object |> Map.put("to", to) diff --git a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex index b42c4ed76..70edf4f7f 100644 --- a/lib/pleroma/web/activity_pub/mrf/tag_policy.ex +++ b/lib/pleroma/web/activity_pub/mrf/tag_policy.ex @@ -19,7 +19,7 @@ defmodule Pleroma.Web.ActivityPub.MRF.TagPolicy do - `mrf_tag:disable-any-subscription`: Reject any follow requests """ - @public "https://www.w3.org/ns/activitystreams#Public" + require Pleroma.Constants defp get_tags(%User{tags: tags}) when is_list(tags), do: tags defp get_tags(_), do: [] @@ -70,9 +70,9 @@ defp process_tag( ) do user = User.get_cached_by_ap_id(actor) - if Enum.member?(to, @public) do - to = List.delete(to, @public) ++ [user.follower_address] - cc = List.delete(cc, user.follower_address) ++ [@public] + if Enum.member?(to, Pleroma.Constants.as_public()) do + to = List.delete(to, Pleroma.Constants.as_public()) ++ [user.follower_address] + cc = List.delete(cc, user.follower_address) ++ [Pleroma.Constants.as_public()] object = object @@ -103,9 +103,10 @@ defp process_tag( ) do user = User.get_cached_by_ap_id(actor) - if Enum.member?(to, @public) or Enum.member?(cc, @public) do - to = List.delete(to, @public) ++ [user.follower_address] - cc = List.delete(cc, @public) + if Enum.member?(to, Pleroma.Constants.as_public()) or + Enum.member?(cc, Pleroma.Constants.as_public()) do + to = List.delete(to, Pleroma.Constants.as_public()) ++ [user.follower_address] + cc = List.delete(cc, Pleroma.Constants.as_public()) object = object diff --git a/lib/pleroma/web/activity_pub/publisher.ex b/lib/pleroma/web/activity_pub/publisher.ex index 016d78216..46edab0bd 100644 --- a/lib/pleroma/web/activity_pub/publisher.ex +++ b/lib/pleroma/web/activity_pub/publisher.ex @@ -11,6 +11,8 @@ defmodule Pleroma.Web.ActivityPub.Publisher do alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Transmogrifier + require Pleroma.Constants + import Pleroma.Web.ActivityPub.Visibility @behaviour Pleroma.Web.Federator.Publisher @@ -117,8 +119,6 @@ defp get_cc_ap_ids(ap_id, recipients) do |> Enum.map(& &1.ap_id) end - @as_public "https://www.w3.org/ns/activitystreams#Public" - defp maybe_use_sharedinbox(%User{info: %{source_data: data}}), do: (is_map(data["endpoints"]) && Map.get(data["endpoints"], "sharedInbox")) || data["inbox"] @@ -145,7 +145,7 @@ def determine_inbox( type == "Delete" -> maybe_use_sharedinbox(user) - @as_public in to || @as_public in cc -> + Pleroma.Constants.as_public() in to || Pleroma.Constants.as_public() in cc -> maybe_use_sharedinbox(user) length(to) + length(cc) > 1 -> diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 7f06e6edd..44bb1cb9a 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -19,6 +19,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do import Ecto.Query require Logger + require Pleroma.Constants @doc """ Modifies an incoming AP object (mastodon format) to our internal format. @@ -102,8 +103,7 @@ def fix_explicit_addressing(object) do follower_collection = User.get_cached_by_ap_id(Containment.get_actor(object)).follower_address - explicit_mentions = - explicit_mentions ++ ["https://www.w3.org/ns/activitystreams#Public", follower_collection] + explicit_mentions = explicit_mentions ++ [Pleroma.Constants.as_public(), follower_collection] fix_explicit_addressing(object, explicit_mentions, follower_collection) end @@ -115,11 +115,11 @@ def fix_implicit_addressing(%{"to" => to, "cc" => cc} = object, followers_collec if followers_collection not in recipients do cond do - "https://www.w3.org/ns/activitystreams#Public" in cc -> + Pleroma.Constants.as_public() in cc -> to = to ++ [followers_collection] Map.put(object, "to", to) - "https://www.w3.org/ns/activitystreams#Public" in to -> + Pleroma.Constants.as_public() in to -> cc = cc ++ [followers_collection] Map.put(object, "cc", cc) @@ -480,8 +480,7 @@ def handle_incoming( {:ok, %User{} = follower} <- User.get_or_fetch_by_ap_id(follower), {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do with deny_follow_blocked <- Pleroma.Config.get([:user, :deny_follow_blocked]), - {_, false} <- - {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked}, + {_, false} <- {:user_blocked, User.blocks?(followed, follower) && deny_follow_blocked}, {_, false} <- {:user_locked, User.locked?(followed)}, {_, {:ok, follower}} <- {:follow, User.follow(follower, followed)}, {_, {:ok, _}} <- diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex index c146f59d4..39074888b 100644 --- a/lib/pleroma/web/activity_pub/utils.ex +++ b/lib/pleroma/web/activity_pub/utils.ex @@ -18,6 +18,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do import Ecto.Query require Logger + require Pleroma.Constants @supported_object_types ["Article", "Note", "Video", "Page", "Question", "Answer"] @supported_report_states ~w(open closed resolved) @@ -418,7 +419,7 @@ def make_follow_data( "type" => "Follow", "actor" => follower_id, "to" => [followed_id], - "cc" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [Pleroma.Constants.as_public()], "object" => followed_id, "state" => "pending" } @@ -510,7 +511,7 @@ def make_announce_data( "actor" => ap_id, "object" => id, "to" => [user.follower_address, object.data["actor"]], - "cc" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [Pleroma.Constants.as_public()], "context" => object.data["context"] } @@ -530,7 +531,7 @@ def make_unannounce_data( "actor" => ap_id, "object" => activity.data, "to" => [user.follower_address, activity.data["actor"]], - "cc" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [Pleroma.Constants.as_public()], "context" => context } @@ -547,7 +548,7 @@ def make_unlike_data( "actor" => ap_id, "object" => activity.data, "to" => [user.follower_address, activity.data["actor"]], - "cc" => ["https://www.w3.org/ns/activitystreams#Public"], + "cc" => [Pleroma.Constants.as_public()], "context" => context } @@ -556,7 +557,7 @@ def make_unlike_data( def add_announce_to_object( %Activity{ - data: %{"actor" => actor, "cc" => ["https://www.w3.org/ns/activitystreams#Public"]} + data: %{"actor" => actor, "cc" => [Pleroma.Constants.as_public()]} }, object ) do @@ -765,7 +766,7 @@ defp get_updated_targets( ) do cc = Map.get(data, "cc", []) follower_address = User.get_cached_by_ap_id(data["actor"]).follower_address - public = "https://www.w3.org/ns/activitystreams#Public" + public = Pleroma.Constants.as_public() case visibility do "public" -> diff --git a/lib/pleroma/web/activity_pub/visibility.ex b/lib/pleroma/web/activity_pub/visibility.ex index 097fceb08..dfb166b65 100644 --- a/lib/pleroma/web/activity_pub/visibility.ex +++ b/lib/pleroma/web/activity_pub/visibility.ex @@ -8,14 +8,14 @@ defmodule Pleroma.Web.ActivityPub.Visibility do alias Pleroma.Repo alias Pleroma.User - @public "https://www.w3.org/ns/activitystreams#Public" + require Pleroma.Constants @spec is_public?(Object.t() | Activity.t() | map()) :: boolean() def is_public?(%Object{data: %{"type" => "Tombstone"}}), do: false def is_public?(%Object{data: data}), do: is_public?(data) def is_public?(%Activity{data: data}), do: is_public?(data) def is_public?(%{"directMessage" => true}), do: false - def is_public?(data), do: @public in (data["to"] ++ (data["cc"] || [])) + def is_public?(data), do: Pleroma.Constants.as_public() in (data["to"] ++ (data["cc"] || [])) def is_private?(activity) do with false <- is_public?(activity), @@ -73,10 +73,10 @@ def get_visibility(object) do cc = object.data["cc"] || [] cond do - @public in to -> + Pleroma.Constants.as_public() in to -> "public" - @public in cc -> + Pleroma.Constants.as_public() in cc -> "unlisted" # this should use the sql for the object's activity diff --git a/lib/pleroma/web/auth/authenticator.ex b/lib/pleroma/web/auth/authenticator.ex index d4e0ffa80..dd49987f7 100644 --- a/lib/pleroma/web/auth/authenticator.ex +++ b/lib/pleroma/web/auth/authenticator.ex @@ -21,8 +21,7 @@ def get_user(plug), do: implementation().get_user(plug) def create_from_registration(plug, registration), do: implementation().create_from_registration(plug, registration) - @callback get_registration(Plug.Conn.t()) :: - {:ok, Registration.t()} | {:error, any()} + @callback get_registration(Plug.Conn.t()) :: {:ok, Registration.t()} | {:error, any()} def get_registration(plug), do: implementation().get_registration(plug) @callback handle_error(Plug.Conn.t(), any()) :: any() diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 44af6a773..2db58324b 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -300,8 +300,7 @@ def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do } } = activity <- get_by_id_or_ap_id(id_or_ap_id), true <- Visibility.is_public?(activity), - %{valid?: true} = info_changeset <- - User.Info.add_pinnned_activity(user.info, activity), + %{valid?: true} = info_changeset <- User.Info.add_pinnned_activity(user.info, activity), changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_changeset), {:ok, _user} <- User.update_and_set_cache(changeset) do diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 94462c3dd..d80fffa26 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -19,6 +19,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do alias Pleroma.Web.MediaProxy require Logger + require Pleroma.Constants # This is a hack for twidere. def get_by_id_or_ap_id(id) do @@ -66,7 +67,7 @@ def attachments_from_ids_descs(ids, descs_str) do @spec get_to_and_cc(User.t(), list(String.t()), Activity.t() | nil, String.t()) :: {list(String.t()), list(String.t())} def get_to_and_cc(user, mentioned_users, inReplyTo, "public") do - to = ["https://www.w3.org/ns/activitystreams#Public" | mentioned_users] + to = [Pleroma.Constants.as_public() | mentioned_users] cc = [user.follower_address] if inReplyTo do @@ -78,7 +79,7 @@ def get_to_and_cc(user, mentioned_users, inReplyTo, "public") do def get_to_and_cc(user, mentioned_users, inReplyTo, "unlisted") do to = [user.follower_address | mentioned_users] - cc = ["https://www.w3.org/ns/activitystreams#Public"] + cc = [Pleroma.Constants.as_public()] if inReplyTo do {Enum.uniq([inReplyTo.data["actor"] | to]), cc} diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 5bdbb709b..174e93468 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -49,6 +49,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do import Ecto.Query require Logger + require Pleroma.Constants @rate_limited_relations_actions ~w(follow unfollow)a @@ -1224,10 +1225,9 @@ def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params recipients = if for_user do - ["https://www.w3.org/ns/activitystreams#Public"] ++ - [for_user.ap_id | for_user.following] + [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following] else - ["https://www.w3.org/ns/activitystreams#Public"] + [Pleroma.Constants.as_public()] end activities = diff --git a/lib/pleroma/web/oauth/oauth_controller.ex b/lib/pleroma/web/oauth/oauth_controller.ex index ef53b7ae3..81eae2c8b 100644 --- a/lib/pleroma/web/oauth/oauth_controller.ex +++ b/lib/pleroma/web/oauth/oauth_controller.ex @@ -365,8 +365,7 @@ def registration_details(%Plug.Conn{} = conn, %{"authorization" => auth_attrs}) def register(%Plug.Conn{} = conn, %{"authorization" => _, "op" => "connect"} = params) do with registration_id when not is_nil(registration_id) <- get_session_registration_id(conn), %Registration{} = registration <- Repo.get(Registration, registration_id), - {_, {:ok, auth}} <- - {:create_authorization, do_create_authorization(conn, params)}, + {_, {:ok, auth}} <- {:create_authorization, do_create_authorization(conn, params)}, %User{} = user <- Repo.preload(auth, :user).user, {:ok, _updated_registration} <- Registration.bind_to_user(registration, user) do conn diff --git a/lib/pleroma/web/oauth/token.ex b/lib/pleroma/web/oauth/token.ex index 90c304487..40f131b57 100644 --- a/lib/pleroma/web/oauth/token.ex +++ b/lib/pleroma/web/oauth/token.ex @@ -44,8 +44,7 @@ def get_by_refresh_token(%App{id: app_id} = _app, token) do |> Repo.find_resource() end - @spec exchange_token(App.t(), Authorization.t()) :: - {:ok, Token.t()} | {:error, Changeset.t()} + @spec exchange_token(App.t(), Authorization.t()) :: {:ok, Token.t()} | {:error, Changeset.t()} def exchange_token(app, auth) do with {:ok, auth} <- Authorization.use_token(auth), true <- auth.app_id == app.id do diff --git a/lib/pleroma/web/ostatus/activity_representer.ex b/lib/pleroma/web/ostatus/activity_representer.ex index 95037125d..760345301 100644 --- a/lib/pleroma/web/ostatus/activity_representer.ex +++ b/lib/pleroma/web/ostatus/activity_representer.ex @@ -9,6 +9,7 @@ defmodule Pleroma.Web.OStatus.ActivityRepresenter do alias Pleroma.Web.OStatus.UserRepresenter require Logger + require Pleroma.Constants defp get_href(id) do with %Object{data: %{"external_url" => external_url}} <- Object.get_cached_by_ap_id(id) do @@ -34,7 +35,7 @@ defp get_mentions(to) do Enum.map(to, fn id -> cond do # Special handling for the AP/Ostatus public collections - "https://www.w3.org/ns/activitystreams#Public" == id -> + Pleroma.Constants.as_public() == id -> {:link, [ rel: "mentioned", diff --git a/lib/pleroma/web/ostatus/handlers/note_handler.ex b/lib/pleroma/web/ostatus/handlers/note_handler.ex index 8e0adad91..3005e8f57 100644 --- a/lib/pleroma/web/ostatus/handlers/note_handler.ex +++ b/lib/pleroma/web/ostatus/handlers/note_handler.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.OStatus.NoteHandler do require Logger + require Pleroma.Constants alias Pleroma.Activity alias Pleroma.Object @@ -49,7 +50,7 @@ def get_people_mentions(entry) do def get_collection_mentions(entry) do transmogrify = fn "http://activityschema.org/collection/public" -> - "https://www.w3.org/ns/activitystreams#Public" + Pleroma.Constants.as_public() group -> group @@ -126,7 +127,7 @@ def handle_note(entry, doc \\ nil, options \\ []) do to <- make_to_list(actor, mentions), date <- XML.string_from_xpath("//published", entry), unlisted <- XML.string_from_xpath("//mastodon:scope", entry) == "unlisted", - cc <- if(unlisted, do: ["https://www.w3.org/ns/activitystreams#Public"], else: []), + cc <- if(unlisted, do: [Pleroma.Constants.as_public()], else: []), note <- CommonAPI.Utils.make_note_data( actor.ap_id, diff --git a/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex b/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex index 014c0935f..0dc1efdaf 100644 --- a/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex +++ b/lib/pleroma/web/rich_media/parsers/ttl/aws_signed_url.ex @@ -19,8 +19,7 @@ defp is_aws_signed_url(nil), do: nil defp is_aws_signed_url(image) when is_binary(image) do %URI{host: host, query: query} = URI.parse(image) - if String.contains?(host, "amazonaws.com") and - String.contains?(query, "X-Amz-Expires") do + if String.contains?(host, "amazonaws.com") and String.contains?(query, "X-Amz-Expires") do image else nil diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index bb5dda204..80082ea84 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -15,6 +15,8 @@ defmodule Pleroma.Web.TwitterAPI.TwitterAPI do import Ecto.Query + require Pleroma.Constants + def create_status(%User{} = user, %{"status" => _} = data) do CommonAPI.post(user, data) end @@ -286,7 +288,7 @@ def search(_user, %{"q" => query} = params) do from( [a, o] in Activity.with_preloaded_object(Activity), where: fragment("?->>'type' = 'Create'", a.data), - where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients, + where: ^Pleroma.Constants.as_public() in a.recipients, where: fragment( "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)", diff --git a/lib/pleroma/web/twitter_api/views/activity_view.ex b/lib/pleroma/web/twitter_api/views/activity_view.ex index e84af84dc..abae63877 100644 --- a/lib/pleroma/web/twitter_api/views/activity_view.ex +++ b/lib/pleroma/web/twitter_api/views/activity_view.ex @@ -19,6 +19,7 @@ defmodule Pleroma.Web.TwitterAPI.ActivityView do import Ecto.Query require Logger + require Pleroma.Constants defp query_context_ids([]), do: [] @@ -91,7 +92,7 @@ defp get_user(ap_id, opts) do String.ends_with?(ap_id, "/followers") -> nil - ap_id == "https://www.w3.org/ns/activitystreams#Public" -> + ap_id == Pleroma.Constants.as_public() -> nil user = User.get_cached_by_ap_id(ap_id) -> diff --git a/lib/pleroma/web/twitter_api/views/notification_view.ex b/lib/pleroma/web/twitter_api/views/notification_view.ex index e7c7a7496..085cd5aa3 100644 --- a/lib/pleroma/web/twitter_api/views/notification_view.ex +++ b/lib/pleroma/web/twitter_api/views/notification_view.ex @@ -10,6 +10,8 @@ defmodule Pleroma.Web.TwitterAPI.NotificationView do alias Pleroma.Web.TwitterAPI.ActivityView alias Pleroma.Web.TwitterAPI.UserView + require Pleroma.Constants + defp get_user(ap_id, opts) do cond do user = opts[:users][ap_id] -> @@ -18,7 +20,7 @@ defp get_user(ap_id, opts) do String.ends_with?(ap_id, "/followers") -> nil - ap_id == "https://www.w3.org/ns/activitystreams#Public" -> + ap_id == Pleroma.Constants.as_public() -> nil true -> diff --git a/test/web/push/impl_test.exs b/test/web/push/impl_test.exs index 1e948086a..e2f89f40a 100644 --- a/test/web/push/impl_test.exs +++ b/test/web/push/impl_test.exs @@ -124,8 +124,7 @@ test "renders body for follow activity" do {:ok, _, _, activity} = CommonAPI.follow(user, other_user) object = Object.normalize(activity) - assert Impl.format_body(%{activity: activity}, user, object) == - "@Bob has followed you" + assert Impl.format_body(%{activity: activity}, user, object) == "@Bob has followed you" end test "renders body for announce activity" do @@ -156,7 +155,6 @@ test "renders body for like activity" do {:ok, activity, _} = CommonAPI.favorite(activity.id, user) object = Object.normalize(activity) - assert Impl.format_body(%{activity: activity}, user, object) == - "@Bob has favorited your post" + assert Impl.format_body(%{activity: activity}, user, object) == "@Bob has favorited your post" end end From 159bbec570c308bf3d71487726737a91eb569178 Mon Sep 17 00:00:00 2001 From: Maksim Date: Mon, 29 Jul 2019 05:02:20 +0000 Subject: [PATCH 052/202] added tests for OstatusController --- lib/pleroma/web/ostatus/ostatus_controller.ex | 170 +++-- test/web/ostatus/ostatus_controller_test.exs | 630 ++++++++++++++---- 2 files changed, 585 insertions(+), 215 deletions(-) diff --git a/lib/pleroma/web/ostatus/ostatus_controller.ex b/lib/pleroma/web/ostatus/ostatus_controller.ex index 372d52899..c70063b84 100644 --- a/lib/pleroma/web/ostatus/ostatus_controller.ex +++ b/lib/pleroma/web/ostatus/ostatus_controller.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Web.OStatus.OStatusController do use Pleroma.Web, :controller + alias Fallback.RedirectController alias Pleroma.Activity alias Pleroma.Object alias Pleroma.User @@ -12,42 +13,44 @@ defmodule Pleroma.Web.OStatus.OStatusController do alias Pleroma.Web.ActivityPub.ActivityPubController alias Pleroma.Web.ActivityPub.ObjectView alias Pleroma.Web.ActivityPub.Visibility + alias Pleroma.Web.Endpoint alias Pleroma.Web.Federator + alias Pleroma.Web.Metadata.PlayerView alias Pleroma.Web.OStatus alias Pleroma.Web.OStatus.ActivityRepresenter alias Pleroma.Web.OStatus.FeedRepresenter + alias Pleroma.Web.Router alias Pleroma.Web.XML plug(Pleroma.Web.FederatingPlug when action in [:salmon_incoming]) + plug( + Pleroma.Plugs.SetFormatPlug + when action in [:feed_redirect, :object, :activity, :notice] + ) + action_fallback(:errors) + def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname}) do + with {_, %User{} = user} <- + {:fetch_user, User.get_cached_by_nickname_or_id(nickname)} do + RedirectController.redirector_with_meta(conn, %{user: user}) + end + end + + def feed_redirect(%{assigns: %{format: format}} = conn, _params) + when format in ["json", "activity+json"] do + ActivityPubController.call(conn, :user) + end + def feed_redirect(conn, %{"nickname" => nickname}) do - case get_format(conn) do - "html" -> - with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do - Fallback.RedirectController.redirector_with_meta(conn, %{user: user}) - else - nil -> {:error, :not_found} - end - - "activity+json" -> - ActivityPubController.call(conn, :user) - - "json" -> - ActivityPubController.call(conn, :user) - - _ -> - with %User{} = user <- User.get_cached_by_nickname(nickname) do - redirect(conn, external: OStatus.feed_path(user)) - else - nil -> {:error, :not_found} - end + with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do + redirect(conn, external: OStatus.feed_path(user)) end end def feed(conn, %{"nickname" => nickname} = params) do - with %User{} = user <- User.get_cached_by_nickname(nickname) do + with {_, %User{} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do query_params = Map.take(params, ["max_id"]) |> Map.merge(%{"whole_db" => true, "actor_id" => user.ap_id}) @@ -65,8 +68,6 @@ def feed(conn, %{"nickname" => nickname} = params) do conn |> put_resp_content_type("application/atom+xml") |> send_resp(200, response) - else - nil -> {:error, :not_found} end end @@ -97,93 +98,82 @@ def salmon_incoming(conn, _) do |> send_resp(200, "") end - def object(conn, %{"uuid" => uuid}) do - if get_format(conn) in ["activity+json", "json"] do - ActivityPubController.call(conn, :object) - else - with id <- o_status_url(conn, :object, uuid), - {_, %Activity{} = activity} <- - {:activity, Activity.get_create_by_object_ap_id_with_object(id)}, - {_, true} <- {:public?, Visibility.is_public?(activity)}, - %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do - case get_format(conn) do - "html" -> redirect(conn, to: "/notice/#{activity.id}") - _ -> represent_activity(conn, nil, activity, user) - end - else - {:public?, false} -> - {:error, :not_found} + def object(%{assigns: %{format: format}} = conn, %{"uuid" => _uuid}) + when format in ["json", "activity+json"] do + ActivityPubController.call(conn, :object) + end - {:activity, nil} -> - {:error, :not_found} - - e -> - e + def object(%{assigns: %{format: format}} = conn, %{"uuid" => uuid}) do + with id <- o_status_url(conn, :object, uuid), + {_, %Activity{} = activity} <- + {:activity, Activity.get_create_by_object_ap_id_with_object(id)}, + {_, true} <- {:public?, Visibility.is_public?(activity)}, + %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do + case format do + "html" -> redirect(conn, to: "/notice/#{activity.id}") + _ -> represent_activity(conn, nil, activity, user) end + else + reason when reason in [{:public?, false}, {:activity, nil}] -> + {:error, :not_found} + + e -> + e end end - def activity(conn, %{"uuid" => uuid}) do - if get_format(conn) in ["activity+json", "json"] do - ActivityPubController.call(conn, :activity) - else - with id <- o_status_url(conn, :activity, uuid), - {_, %Activity{} = activity} <- {:activity, Activity.normalize(id)}, - {_, true} <- {:public?, Visibility.is_public?(activity)}, - %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do - case format = get_format(conn) do - "html" -> redirect(conn, to: "/notice/#{activity.id}") - _ -> represent_activity(conn, format, activity, user) - end - else - {:public?, false} -> - {:error, :not_found} + def activity(%{assigns: %{format: format}} = conn, %{"uuid" => _uuid}) + when format in ["json", "activity+json"] do + ActivityPubController.call(conn, :activity) + end - {:activity, nil} -> - {:error, :not_found} - - e -> - e + def activity(%{assigns: %{format: format}} = conn, %{"uuid" => uuid}) do + with id <- o_status_url(conn, :activity, uuid), + {_, %Activity{} = activity} <- {:activity, Activity.normalize(id)}, + {_, true} <- {:public?, Visibility.is_public?(activity)}, + %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do + case format do + "html" -> redirect(conn, to: "/notice/#{activity.id}") + _ -> represent_activity(conn, format, activity, user) end + else + reason when reason in [{:public?, false}, {:activity, nil}] -> + {:error, :not_found} + + e -> + e end end - def notice(conn, %{"id" => id}) do + def notice(%{assigns: %{format: format}} = conn, %{"id" => id}) do with {_, %Activity{} = activity} <- {:activity, Activity.get_by_id_with_object(id)}, {_, true} <- {:public?, Visibility.is_public?(activity)}, %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do - case format = get_format(conn) do - "html" -> - if activity.data["type"] == "Create" do - %Object{} = object = Object.normalize(activity) + cond do + format == "html" && activity.data["type"] == "Create" -> + %Object{} = object = Object.normalize(activity) - Fallback.RedirectController.redirector_with_meta(conn, %{ + RedirectController.redirector_with_meta( + conn, + %{ activity_id: activity.id, object: object, - url: - Pleroma.Web.Router.Helpers.o_status_url( - Pleroma.Web.Endpoint, - :notice, - activity.id - ), + url: Router.Helpers.o_status_url(Endpoint, :notice, activity.id), user: user - }) - else - Fallback.RedirectController.redirector(conn, nil) - end + } + ) - _ -> + format == "html" -> + RedirectController.redirector(conn, nil) + + true -> represent_activity(conn, format, activity, user) end else - {:public?, false} -> + reason when reason in [{:public?, false}, {:activity, nil}] -> conn |> put_status(404) - |> Fallback.RedirectController.redirector(nil, 404) - - {:activity, nil} -> - conn - |> Fallback.RedirectController.redirector(nil, 404) + |> RedirectController.redirector(nil, 404) e -> e @@ -204,13 +194,13 @@ def notice_player(conn, %{"id" => id}) do "content-security-policy", "default-src 'none';style-src 'self' 'unsafe-inline';img-src 'self' data: https:; media-src 'self' https:;" ) - |> put_view(Pleroma.Web.Metadata.PlayerView) + |> put_view(PlayerView) |> render("player.html", url) else _error -> conn |> put_status(404) - |> Fallback.RedirectController.redirector(nil, 404) + |> RedirectController.redirector(nil, 404) end end @@ -248,6 +238,8 @@ def errors(conn, {:error, :not_found}) do render_error(conn, :not_found, "Not found") end + def errors(conn, {:fetch_user, nil}), do: errors(conn, {:error, :not_found}) + def errors(conn, _) do render_error(conn, :internal_server_error, "Something went wrong") end diff --git a/test/web/ostatus/ostatus_controller_test.exs b/test/web/ostatus/ostatus_controller_test.exs index bb7648bdd..9f756effb 100644 --- a/test/web/ostatus/ostatus_controller_test.exs +++ b/test/web/ostatus/ostatus_controller_test.exs @@ -101,160 +101,538 @@ test "returns 404 for a missing feed", %{conn: conn} do assert response(conn, 404) end - test "gets an object", %{conn: conn} do - note_activity = insert(:note_activity) - object = Object.normalize(note_activity) - user = User.get_cached_by_ap_id(note_activity.data["actor"]) - [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"])) - url = "/objects/#{uuid}" + describe "GET object/2" do + test "gets an object", %{conn: conn} do + note_activity = insert(:note_activity) + object = Object.normalize(note_activity) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) + [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"])) + url = "/objects/#{uuid}" + + conn = + conn + |> put_req_header("accept", "application/xml") + |> get(url) + + expected = + ActivityRepresenter.to_simple_form(note_activity, user, true) + |> ActivityRepresenter.wrap_with_entry() + |> :xmerl.export_simple(:xmerl_xml) + |> to_string + + assert response(conn, 200) == expected + end + + test "redirects to /notice/id for html format", %{conn: conn} do + note_activity = insert(:note_activity) + object = Object.normalize(note_activity) + [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"])) + url = "/objects/#{uuid}" + + conn = + conn + |> put_req_header("accept", "text/html") + |> get(url) + + assert redirected_to(conn) == "/notice/#{note_activity.id}" + end + + test "500s when user not found", %{conn: conn} do + note_activity = insert(:note_activity) + object = Object.normalize(note_activity) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) + User.invalidate_cache(user) + Pleroma.Repo.delete(user) + [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"])) + url = "/objects/#{uuid}" + + conn = + conn + |> put_req_header("accept", "application/xml") + |> get(url) + + assert response(conn, 500) == ~S({"error":"Something went wrong"}) + end + + test "404s on private objects", %{conn: conn} do + note_activity = insert(:direct_note_activity) + object = Object.normalize(note_activity) + [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"])) + + conn + |> get("/objects/#{uuid}") + |> response(404) + end + + test "404s on nonexisting objects", %{conn: conn} do + conn + |> get("/objects/123") + |> response(404) + end + end + + describe "GET activity/2" do + test "gets an activity in xml format", %{conn: conn} do + note_activity = insert(:note_activity) + [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"])) - conn = conn |> put_req_header("accept", "application/xml") - |> get(url) + |> get("/activities/#{uuid}") + |> response(200) + end - expected = - ActivityRepresenter.to_simple_form(note_activity, user, true) - |> ActivityRepresenter.wrap_with_entry() - |> :xmerl.export_simple(:xmerl_xml) - |> to_string + test "redirects to /notice/id for html format", %{conn: conn} do + note_activity = insert(:note_activity) + [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"])) - assert response(conn, 200) == expected + conn = + conn + |> put_req_header("accept", "text/html") + |> get("/activities/#{uuid}") + + assert redirected_to(conn) == "/notice/#{note_activity.id}" + end + + test "505s when user not found", %{conn: conn} do + note_activity = insert(:note_activity) + [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"])) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) + User.invalidate_cache(user) + Pleroma.Repo.delete(user) + + conn = + conn + |> put_req_header("accept", "text/html") + |> get("/activities/#{uuid}") + + assert response(conn, 500) == ~S({"error":"Something went wrong"}) + end + + test "404s on deleted objects", %{conn: conn} do + note_activity = insert(:note_activity) + object = Object.normalize(note_activity) + [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"])) + + conn + |> put_req_header("accept", "application/xml") + |> get("/objects/#{uuid}") + |> response(200) + + Object.delete(object) + + conn + |> put_req_header("accept", "application/xml") + |> get("/objects/#{uuid}") + |> response(404) + end + + test "404s on private activities", %{conn: conn} do + note_activity = insert(:direct_note_activity) + [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"])) + + conn + |> get("/activities/#{uuid}") + |> response(404) + end + + test "404s on nonexistent activities", %{conn: conn} do + conn + |> get("/activities/123") + |> response(404) + end + + test "gets an activity in AS2 format", %{conn: conn} do + note_activity = insert(:note_activity) + [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"])) + url = "/activities/#{uuid}" + + conn = + conn + |> put_req_header("accept", "application/activity+json") + |> get(url) + + assert json_response(conn, 200) + end end - test "404s on private objects", %{conn: conn} do - note_activity = insert(:direct_note_activity) - object = Object.normalize(note_activity) - [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"])) + describe "GET notice/2" do + test "gets a notice in xml format", %{conn: conn} do + note_activity = insert(:note_activity) - conn - |> get("/objects/#{uuid}") - |> response(404) - end + conn + |> get("/notice/#{note_activity.id}") + |> response(200) + end - test "404s on nonexisting objects", %{conn: conn} do - conn - |> get("/objects/123") - |> response(404) - end + test "gets a notice in AS2 format", %{conn: conn} do + note_activity = insert(:note_activity) - test "gets an activity in xml format", %{conn: conn} do - note_activity = insert(:note_activity) - [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"])) - - conn - |> put_req_header("accept", "application/xml") - |> get("/activities/#{uuid}") - |> response(200) - end - - test "404s on deleted objects", %{conn: conn} do - note_activity = insert(:note_activity) - object = Object.normalize(note_activity) - [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"])) - - conn - |> put_req_header("accept", "application/xml") - |> get("/objects/#{uuid}") - |> response(200) - - Object.delete(object) - - conn - |> put_req_header("accept", "application/xml") - |> get("/objects/#{uuid}") - |> response(404) - end - - test "404s on private activities", %{conn: conn} do - note_activity = insert(:direct_note_activity) - [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"])) - - conn - |> get("/activities/#{uuid}") - |> response(404) - end - - test "404s on nonexistent activities", %{conn: conn} do - conn - |> get("/activities/123") - |> response(404) - end - - test "gets a notice in xml format", %{conn: conn} do - note_activity = insert(:note_activity) - - conn - |> get("/notice/#{note_activity.id}") - |> response(200) - end - - test "gets a notice in AS2 format", %{conn: conn} do - note_activity = insert(:note_activity) - - conn - |> put_req_header("accept", "application/activity+json") - |> get("/notice/#{note_activity.id}") - |> json_response(200) - end - - test "only gets a notice in AS2 format for Create messages", %{conn: conn} do - note_activity = insert(:note_activity) - url = "/notice/#{note_activity.id}" - - conn = conn |> put_req_header("accept", "application/activity+json") - |> get(url) + |> get("/notice/#{note_activity.id}") + |> json_response(200) + end - assert json_response(conn, 200) + test "500s when actor not found", %{conn: conn} do + note_activity = insert(:note_activity) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) + User.invalidate_cache(user) + Pleroma.Repo.delete(user) - user = insert(:user) + conn = + conn + |> get("/notice/#{note_activity.id}") - {:ok, like_activity, _} = CommonAPI.favorite(note_activity.id, user) - url = "/notice/#{like_activity.id}" + assert response(conn, 500) == ~S({"error":"Something went wrong"}) + end - assert like_activity.data["type"] == "Like" + test "only gets a notice in AS2 format for Create messages", %{conn: conn} do + note_activity = insert(:note_activity) + url = "/notice/#{note_activity.id}" - conn = - build_conn() - |> put_req_header("accept", "application/activity+json") - |> get(url) + conn = + conn + |> put_req_header("accept", "application/activity+json") + |> get(url) - assert response(conn, 404) + assert json_response(conn, 200) + + user = insert(:user) + + {:ok, like_activity, _} = CommonAPI.favorite(note_activity.id, user) + url = "/notice/#{like_activity.id}" + + assert like_activity.data["type"] == "Like" + + conn = + build_conn() + |> put_req_header("accept", "application/activity+json") + |> get(url) + + assert response(conn, 404) + end + + test "render html for redirect for html format", %{conn: conn} do + note_activity = insert(:note_activity) + + resp = + conn + |> put_req_header("accept", "text/html") + |> get("/notice/#{note_activity.id}") + |> response(200) + + assert resp =~ + "" + + user = insert(:user) + + {:ok, like_activity, _} = CommonAPI.favorite(note_activity.id, user) + + assert like_activity.data["type"] == "Like" + + resp = + conn + |> put_req_header("accept", "text/html") + |> get("/notice/#{like_activity.id}") + |> response(200) + + assert resp =~ "" + end + + test "404s a private notice", %{conn: conn} do + note_activity = insert(:direct_note_activity) + url = "/notice/#{note_activity.id}" + + conn = + conn + |> get(url) + + assert response(conn, 404) + end + + test "404s a nonexisting notice", %{conn: conn} do + url = "/notice/123" + + conn = + conn + |> get(url) + + assert response(conn, 404) + end end - test "gets an activity in AS2 format", %{conn: conn} do - note_activity = insert(:note_activity) - [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"])) - url = "/activities/#{uuid}" + describe "feed_redirect" do + test "undefined format. it redirects to feed", %{conn: conn} do + note_activity = insert(:note_activity) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) - conn = - conn - |> put_req_header("accept", "application/activity+json") - |> get(url) + response = + conn + |> put_req_header("accept", "application/xml") + |> get("/users/#{user.nickname}") + |> response(302) - assert json_response(conn, 200) + assert response == + "You are being redirected." + end + + test "undefined format. it returns error when user not found", %{conn: conn} do + response = + conn + |> put_req_header("accept", "application/xml") + |> get("/users/jimm") + |> response(404) + + assert response == ~S({"error":"Not found"}) + end + + test "activity+json format. it redirects on actual feed of user", %{conn: conn} do + note_activity = insert(:note_activity) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) + + response = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/users/#{user.nickname}") + |> json_response(200) + + assert response["endpoints"] == %{ + "oauthAuthorizationEndpoint" => "#{Pleroma.Web.base_url()}/oauth/authorize", + "oauthRegistrationEndpoint" => "#{Pleroma.Web.base_url()}/api/v1/apps", + "oauthTokenEndpoint" => "#{Pleroma.Web.base_url()}/oauth/token", + "sharedInbox" => "#{Pleroma.Web.base_url()}/inbox" + } + + assert response["@context"] == [ + "https://www.w3.org/ns/activitystreams", + "http://localhost:4001/schemas/litepub-0.1.jsonld", + %{"@language" => "und"} + ] + + assert Map.take(response, [ + "followers", + "following", + "id", + "inbox", + "manuallyApprovesFollowers", + "name", + "outbox", + "preferredUsername", + "summary", + "tag", + "type", + "url" + ]) == %{ + "followers" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/followers", + "following" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/following", + "id" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}", + "inbox" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/inbox", + "manuallyApprovesFollowers" => false, + "name" => user.name, + "outbox" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/outbox", + "preferredUsername" => user.nickname, + "summary" => user.bio, + "tag" => [], + "type" => "Person", + "url" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}" + } + end + + test "activity+json format. it returns error whe use not found", %{conn: conn} do + response = + conn + |> put_req_header("accept", "application/activity+json") + |> get("/users/jimm") + |> json_response(404) + + assert response == "Not found" + end + + test "json format. it redirects on actual feed of user", %{conn: conn} do + note_activity = insert(:note_activity) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) + + response = + conn + |> put_req_header("accept", "application/json") + |> get("/users/#{user.nickname}") + |> json_response(200) + + assert response["endpoints"] == %{ + "oauthAuthorizationEndpoint" => "#{Pleroma.Web.base_url()}/oauth/authorize", + "oauthRegistrationEndpoint" => "#{Pleroma.Web.base_url()}/api/v1/apps", + "oauthTokenEndpoint" => "#{Pleroma.Web.base_url()}/oauth/token", + "sharedInbox" => "#{Pleroma.Web.base_url()}/inbox" + } + + assert response["@context"] == [ + "https://www.w3.org/ns/activitystreams", + "http://localhost:4001/schemas/litepub-0.1.jsonld", + %{"@language" => "und"} + ] + + assert Map.take(response, [ + "followers", + "following", + "id", + "inbox", + "manuallyApprovesFollowers", + "name", + "outbox", + "preferredUsername", + "summary", + "tag", + "type", + "url" + ]) == %{ + "followers" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/followers", + "following" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/following", + "id" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}", + "inbox" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/inbox", + "manuallyApprovesFollowers" => false, + "name" => user.name, + "outbox" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}/outbox", + "preferredUsername" => user.nickname, + "summary" => user.bio, + "tag" => [], + "type" => "Person", + "url" => "#{Pleroma.Web.base_url()}/users/#{user.nickname}" + } + end + + test "json format. it returns error whe use not found", %{conn: conn} do + response = + conn + |> put_req_header("accept", "application/json") + |> get("/users/jimm") + |> json_response(404) + + assert response == "Not found" + end + + test "html format. it redirects on actual feed of user", %{conn: conn} do + note_activity = insert(:note_activity) + user = User.get_cached_by_ap_id(note_activity.data["actor"]) + + response = + conn + |> get("/users/#{user.nickname}") + |> response(200) + + assert response == + Fallback.RedirectController.redirector_with_meta( + conn, + %{user: user} + ).resp_body + end + + test "html format. it returns error when user not found", %{conn: conn} do + response = + conn + |> get("/users/jimm") + |> json_response(404) + + assert response == %{"error" => "Not found"} + end end - test "404s a private notice", %{conn: conn} do - note_activity = insert(:direct_note_activity) - url = "/notice/#{note_activity.id}" + describe "GET /notice/:id/embed_player" do + test "render embed player", %{conn: conn} do + note_activity = insert(:note_activity) + object = Pleroma.Object.normalize(note_activity) - conn = - conn - |> get(url) + object_data = + Map.put(object.data, "attachment", [ + %{ + "url" => [ + %{ + "href" => + "https://peertube.moe/static/webseed/df5f464b-be8d-46fb-ad81-2d4c2d1630e3-480.mp4", + "mediaType" => "video/mp4", + "type" => "Link" + } + ] + } + ]) - assert response(conn, 404) - end + object + |> Ecto.Changeset.change(data: object_data) + |> Pleroma.Repo.update() - test "404s a nonexisting notice", %{conn: conn} do - url = "/notice/123" + conn = + conn + |> get("/notice/#{note_activity.id}/embed_player") - conn = - conn - |> get(url) + assert Plug.Conn.get_resp_header(conn, "x-frame-options") == ["ALLOW"] - assert response(conn, 404) + assert Plug.Conn.get_resp_header( + conn, + "content-security-policy" + ) == [ + "default-src 'none';style-src 'self' 'unsafe-inline';img-src 'self' data: https:; media-src 'self' https:;" + ] + + assert response(conn, 200) =~ + "" + end + + test "404s when activity isn't create", %{conn: conn} do + note_activity = insert(:note_activity, data_attrs: %{"type" => "Like"}) + + assert conn + |> get("/notice/#{note_activity.id}/embed_player") + |> response(404) + end + + test "404s when activity is direct message", %{conn: conn} do + note_activity = insert(:note_activity, data_attrs: %{"directMessage" => true}) + + assert conn + |> get("/notice/#{note_activity.id}/embed_player") + |> response(404) + end + + test "404s when attachment is empty", %{conn: conn} do + note_activity = insert(:note_activity) + object = Pleroma.Object.normalize(note_activity) + object_data = Map.put(object.data, "attachment", []) + + object + |> Ecto.Changeset.change(data: object_data) + |> Pleroma.Repo.update() + + assert conn + |> get("/notice/#{note_activity.id}/embed_player") + |> response(404) + end + + test "404s when attachment isn't audio or video", %{conn: conn} do + note_activity = insert(:note_activity) + object = Pleroma.Object.normalize(note_activity) + + object_data = + Map.put(object.data, "attachment", [ + %{ + "url" => [ + %{ + "href" => "https://peertube.moe/static/webseed/480.jpg", + "mediaType" => "image/jpg", + "type" => "Link" + } + ] + } + ]) + + object + |> Ecto.Changeset.change(data: object_data) + |> Pleroma.Repo.update() + + assert conn + |> get("/notice/#{note_activity.id}/embed_player") + |> response(404) + end end end From c0e258cf21395fa2d5338ee238e4fcf4f3b3bf30 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Mon, 29 Jul 2019 16:17:22 +0000 Subject: [PATCH 053/202] Redirect not logged-in users to the MastoFE login page on private instances --- CHANGELOG.md | 1 + lib/pleroma/web/router.ex | 2 +- .../mastodon_api/mastodon_api_controller_test.exs | 15 +++++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48379b757..5416d452e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Rich Media: The crawled URL is now spliced into the rich media data. - ActivityPub S2S: sharedInbox usage has been mostly aligned with the rules in the AP specification. - ActivityPub S2S: remote user deletions now work the same as local user deletions. +- Not being able to access the Mastodon FE login page on private instances ### Added - MRF: Support for priming the mediaproxy cache (`Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 4e1ab6c33..0689d69fb 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -698,7 +698,7 @@ defmodule Pleroma.Web.Router do post("/auth/password", MastodonAPIController, :password_reset) scope [] do - pipe_through(:oauth_read_or_public) + pipe_through(:oauth_read) get("/web/*path", MastodonAPIController, :index) 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 d7f92fac2..66016c886 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -3154,6 +3154,21 @@ test "redirects not logged-in users to the login page", %{conn: conn, path: path assert redirected_to(conn) == "/web/login" end + test "redirects not logged-in users to the login page on private instances", %{ + conn: conn, + path: path + } do + is_public = Pleroma.Config.get([:instance, :public]) + Pleroma.Config.put([:instance, :public], false) + + conn = get(conn, path) + + assert conn.status == 302 + assert redirected_to(conn) == "/web/login" + + Pleroma.Config.put([:instance, :public], is_public) + end + test "does not redirect logged in users to the login page", %{conn: conn, path: path} do token = insert(:oauth_token) From 0bee2131ce55ffd702ddc92800499b01b86d3765 Mon Sep 17 00:00:00 2001 From: Eugenij Date: Mon, 29 Jul 2019 16:17:40 +0000 Subject: [PATCH 054/202] Add `mailerEnabled` to the NodeInfo metadata --- CHANGELOG.md | 1 + lib/pleroma/web/nodeinfo/nodeinfo_controller.ex | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5416d452e..e77fe4f3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text - Federation: Return 403 errors when trying to request pages from a user's follower/following collections if they have `hide_followers`/`hide_follows` set - NodeInfo: Return `skipThreadContainment` in `metadata` for the `skip_thread_containment` option +- NodeInfo: Return `mailerEnabled` in `metadata` - Mastodon API: Unsubscribe followers when they unfollow a user - AdminAPI: Add "godmode" while fetching user statuses (i.e. admin can see private statuses) diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex index a1d7fcc7d..54f89e65c 100644 --- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex +++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex @@ -165,6 +165,7 @@ def raw_nodeinfo do }, accountActivationRequired: Config.get([:instance, :account_activation_required], false), invitesEnabled: Config.get([:instance, :invites_enabled], false), + mailerEnabled: Config.get([Pleroma.Emails.Mailer, :enabled], false), features: features, restrictedNicknames: Config.get([Pleroma.User, :restricted_nicknames]), skipThreadContainment: Config.get([:instance, :skip_thread_containment], false) From 5795a890e9d14a9e51e2613d26620899b2171623 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Mon, 29 Jul 2019 19:09:58 +0000 Subject: [PATCH 055/202] markdown: clean up html generated by earmark --- lib/pleroma/web/common_api/utils.ex | 3 +++ test/web/common_api/common_api_utils_test.exs | 10 +++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index d80fffa26..6d42ae8ae 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -300,6 +300,9 @@ def format_input(text, "text/markdown", options) do |> Earmark.as_html!() |> Formatter.linkify(options) |> Formatter.html_escape("text/html") + |> (fn {text, mentions, tags} -> + {String.replace(text, ~r/\r?\n/, ""), mentions, tags} + end).() end def make_note_data( diff --git a/test/web/common_api/common_api_utils_test.exs b/test/web/common_api/common_api_utils_test.exs index af320f31f..38b2319ac 100644 --- a/test/web/common_api/common_api_utils_test.exs +++ b/test/web/common_api/common_api_utils_test.exs @@ -99,14 +99,14 @@ test "works for bare text/html" do test "works for bare text/markdown" do text = "**hello world**" - expected = "

hello world

\n" + expected = "

hello world

" {output, [], []} = Utils.format_input(text, "text/markdown") assert output == expected text = "**hello world**\n\n*another paragraph*" - expected = "

hello world

\n

another paragraph

\n" + expected = "

hello world

another paragraph

" {output, [], []} = Utils.format_input(text, "text/markdown") @@ -118,7 +118,7 @@ test "works for bare text/markdown" do by someone """ - expected = "

cool quote

\n
\n

by someone

\n" + expected = "

cool quote

by someone

" {output, [], []} = Utils.format_input(text, "text/markdown") @@ -157,11 +157,11 @@ test "works for text/markdown with mentions" do text = "**hello world**\n\n*another @user__test and @user__test google.com paragraph*" expected = - "

hello world

\n

another hello world

another @user__test and @user__test google.com paragraph

\n" + }\" class=\"u-url mention\" href=\"http://foo.com/user__test\">@user__test google.com paragraph

" {output, _, _} = Utils.format_input(text, "text/markdown") From 5835069215b880ad261a006cf310da624a82ca4a Mon Sep 17 00:00:00 2001 From: kaniini Date: Mon, 29 Jul 2019 19:42:26 +0000 Subject: [PATCH 056/202] Revert "Merge branch 'bugfix/clean-up-markdown-rendering' into 'develop'" This reverts merge request !1504 --- lib/pleroma/web/common_api/utils.ex | 3 --- test/web/common_api/common_api_utils_test.exs | 10 +++++----- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index 6d42ae8ae..d80fffa26 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -300,9 +300,6 @@ def format_input(text, "text/markdown", options) do |> Earmark.as_html!() |> Formatter.linkify(options) |> Formatter.html_escape("text/html") - |> (fn {text, mentions, tags} -> - {String.replace(text, ~r/\r?\n/, ""), mentions, tags} - end).() end def make_note_data( diff --git a/test/web/common_api/common_api_utils_test.exs b/test/web/common_api/common_api_utils_test.exs index 38b2319ac..af320f31f 100644 --- a/test/web/common_api/common_api_utils_test.exs +++ b/test/web/common_api/common_api_utils_test.exs @@ -99,14 +99,14 @@ test "works for bare text/html" do test "works for bare text/markdown" do text = "**hello world**" - expected = "

hello world

" + expected = "

hello world

\n" {output, [], []} = Utils.format_input(text, "text/markdown") assert output == expected text = "**hello world**\n\n*another paragraph*" - expected = "

hello world

another paragraph

" + expected = "

hello world

\n

another paragraph

\n" {output, [], []} = Utils.format_input(text, "text/markdown") @@ -118,7 +118,7 @@ test "works for bare text/markdown" do by someone """ - expected = "

cool quote

by someone

" + expected = "

cool quote

\n
\n

by someone

\n" {output, [], []} = Utils.format_input(text, "text/markdown") @@ -157,11 +157,11 @@ test "works for text/markdown with mentions" do text = "**hello world**\n\n*another @user__test and @user__test google.com paragraph*" expected = - "

hello world

another hello world

\n

another @user__test and @user__test google.com paragraph

" + }\" class=\"u-url mention\" href=\"http://foo.com/user__test\">@user__test google.com paragraph

\n" {output, _, _} = Utils.format_input(text, "text/markdown") From 3850812503ebfe0e1eaf84a4067e11e052a8206e Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Mon, 29 Jul 2019 20:00:57 +0000 Subject: [PATCH 057/202] twitter api: utils: rework do_remote_follow() to use CommonAPI Closes #1138 --- lib/pleroma/web/twitter_api/controllers/util_controller.ex | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 39bc6147c..5c73a615d 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -15,7 +15,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do alias Pleroma.Plugs.AuthenticationPlug alias Pleroma.User alias Pleroma.Web - alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.CommonAPI alias Pleroma.Web.WebFinger @@ -100,8 +99,7 @@ def do_remote_follow(conn, %{ with %User{} = user <- User.get_cached_by_nickname(username), true <- AuthenticationPlug.checkpw(password, user.password_hash), %User{} = _followed <- User.get_cached_by_id(id), - {:ok, follower} <- User.follow(user, followee), - {:ok, _activity} <- ActivityPub.follow(follower, followee) do + {:ok, _follower, _followee, _activity} <- CommonAPI.follow(user, followee) do conn |> render("followed.html", %{error: false}) else @@ -122,8 +120,7 @@ def do_remote_follow(conn, %{ def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}}) do with %User{} = followee <- User.get_cached_by_id(id), - {:ok, follower} <- User.follow(user, followee), - {:ok, _activity} <- ActivityPub.follow(follower, followee) do + {:ok, _follower, _followee, _activity} <- CommonAPI.follow(user, followee) do conn |> render("followed.html", %{error: false}) else From 51b3b6d8164de9196159dc7de8d5abf0c4fa1bce Mon Sep 17 00:00:00 2001 From: Alexander Strizhakov Date: Tue, 30 Jul 2019 16:36:05 +0000 Subject: [PATCH 058/202] Admin changes --- CHANGELOG.md | 1 + docs/api/admin_api.md | 23 ++++++++++++ lib/mix/tasks/pleroma/config.ex | 2 +- .../web/admin_api/admin_api_controller.ex | 10 +++++ lib/pleroma/web/router.ex | 2 + .../admin_api/admin_api_controller_test.exs | 37 +++++++++++++++++++ 6 files changed, 74 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e77fe4f3d..acd55362d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,6 +53,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Admin API: Return avatar and display name when querying users - Admin API: Allow querying user by ID - Admin API: Added support for `tuples`. +- Admin API: Added endpoints to run mix tasks pleroma.config migrate_to_db & pleroma.config migrate_from_db - Added synchronization of following/followers counters for external users - Configuration: `enabled` option for `Pleroma.Emails.Mailer`, defaulting to `false`. - Configuration: Pleroma.Plugs.RateLimiter `bucket_name`, `params` options. diff --git a/docs/api/admin_api.md b/docs/api/admin_api.md index ca9303227..22873dde9 100644 --- a/docs/api/admin_api.md +++ b/docs/api/admin_api.md @@ -575,6 +575,29 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret - 404 Not Found `"Not found"` - On success: 200 OK `{}` + +## `/api/pleroma/admin/config/migrate_to_db` +### Run mix task pleroma.config migrate_to_db +Copy settings on key `:pleroma` to DB. +- Method `GET` +- Params: none +- Response: + +```json +{} +``` + +## `/api/pleroma/admin/config/migrate_from_db` +### Run mix task pleroma.config migrate_from_db +Copy all settings from DB to `config/prod.exported_from_db.secret.exs` with deletion from DB. +- Method `GET` +- Params: none +- Response: + +```json +{} +``` + ## `/api/pleroma/admin/config` ### List config settings List config settings only works with `:pleroma => :instance => :dynamic_configuration` setting to `true`. diff --git a/lib/mix/tasks/pleroma/config.ex b/lib/mix/tasks/pleroma/config.ex index a7d0fac5d..462940e7e 100644 --- a/lib/mix/tasks/pleroma/config.ex +++ b/lib/mix/tasks/pleroma/config.ex @@ -15,7 +15,7 @@ defmodule Mix.Tasks.Pleroma.Config do mix pleroma.config migrate_to_db - ## Transfers config from DB to file. + ## Transfers config from DB to file `config/env.exported_from_db.secret.exs` mix pleroma.config migrate_from_db ENV """ diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 1ae5acd91..fcda57b3e 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -379,6 +379,16 @@ def status_delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do end end + def migrate_to_db(conn, _params) do + Mix.Tasks.Pleroma.Config.run(["migrate_to_db"]) + json(conn, %{}) + end + + def migrate_from_db(conn, _params) do + Mix.Tasks.Pleroma.Config.run(["migrate_from_db", Pleroma.Config.get(:env), "true"]) + json(conn, %{}) + end + def config_show(conn, _params) do configs = Pleroma.Repo.all(Config) diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 0689d69fb..d475fc973 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -196,6 +196,8 @@ defmodule Pleroma.Web.Router do get("/config", AdminAPIController, :config_show) post("/config", AdminAPIController, :config_update) + get("/config/migrate_to_db", AdminAPIController, :migrate_to_db) + get("/config/migrate_from_db", AdminAPIController, :migrate_from_db) end scope "/", Pleroma.Web.TwitterAPI do diff --git a/test/web/admin_api/admin_api_controller_test.exs b/test/web/admin_api/admin_api_controller_test.exs index 6dda4ae51..824ad23e6 100644 --- a/test/web/admin_api/admin_api_controller_test.exs +++ b/test/web/admin_api/admin_api_controller_test.exs @@ -1916,6 +1916,43 @@ test "queues key as atom", %{conn: conn} do end end + describe "config mix tasks run" do + setup %{conn: conn} do + admin = insert(:user, info: %{is_admin: true}) + + temp_file = "config/test.exported_from_db.secret.exs" + + on_exit(fn -> + :ok = File.rm(temp_file) + end) + + dynamic = Pleroma.Config.get([:instance, :dynamic_configuration]) + + Pleroma.Config.put([:instance, :dynamic_configuration], true) + + on_exit(fn -> + Pleroma.Config.put([:instance, :dynamic_configuration], dynamic) + end) + + %{conn: assign(conn, :user, admin), admin: admin} + end + + test "transfer settings to DB and to file", %{conn: conn, admin: admin} do + assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) == [] + conn = get(conn, "/api/pleroma/admin/config/migrate_to_db") + assert json_response(conn, 200) == %{} + assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) > 0 + + conn = + build_conn() + |> assign(:user, admin) + |> get("/api/pleroma/admin/config/migrate_from_db") + + assert json_response(conn, 200) == %{} + assert Pleroma.Repo.all(Pleroma.Web.AdminAPI.Config) == [] + end + end + describe "GET /api/pleroma/admin/users/:nickname/statuses" do setup do admin = insert(:user, info: %{is_admin: true}) From f42719506c539a4058c52d3a6e4a828948ac74ce Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 31 Jul 2019 14:20:34 +0300 Subject: [PATCH 059/202] 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 7483679a7b6ff63c9c61c3df3e9e37f2c24012ff Mon Sep 17 00:00:00 2001 From: lain Date: Wed, 31 Jul 2019 15:12:29 +0200 Subject: [PATCH 060/202] StatusView: Return direct conversation id. --- lib/pleroma/conversation/participation.ex | 8 ++++++++ .../web/mastodon_api/views/status_view.ex | 18 +++++++++++++++++- test/web/mastodon_api/status_view_test.exs | 18 +++++++++++++++++- 3 files changed, 42 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex index 5883e4183..77b3f61e9 100644 --- a/lib/pleroma/conversation/participation.ex +++ b/lib/pleroma/conversation/participation.ex @@ -65,6 +65,14 @@ def for_user(user, params \\ %{}) do |> Pleroma.Pagination.fetch_paginated(params) end + def for_user_and_conversation(user, conversation) do + from(p in __MODULE__, + where: p.user_id == ^user.id, + where: p.conversation_id == ^conversation.id + ) + |> Repo.one() + end + def for_user_with_last_activity_id(user, params \\ %{}) do for_user(user, params) |> Enum.map(fn participation -> diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 80df9b2ac..a862554b1 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -6,6 +6,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do use Pleroma.Web, :view alias Pleroma.Activity + alias Pleroma.Conversation + alias Pleroma.Conversation.Participation alias Pleroma.HTML alias Pleroma.Object alias Pleroma.Repo @@ -225,6 +227,19 @@ def render("status.json", %{activity: %{data: %{"object" => _object}} = activity object.data["url"] || object.data["external_url"] || object.data["id"] end + direct_conversation_id = + with {_, true} <- {:include_id, opts[:with_direct_conversation_id]}, + {_, %User{} = for_user} <- {:for_user, opts[:for]}, + %{data: %{"context" => context}} when is_binary(context) <- activity, + %Conversation{} = conversation <- Conversation.get_for_ap_id(context), + %Participation{id: participation_id} <- + Participation.for_user_and_conversation(for_user, conversation) do + participation_id + else + _e -> + nil + end + %{ id: to_string(activity.id), uri: object.data["id"], @@ -262,7 +277,8 @@ def render("status.json", %{activity: %{data: %{"object" => _object}} = activity conversation_id: get_context_id(activity), in_reply_to_account_acct: reply_to_user && reply_to_user.nickname, content: %{"text/plain" => content_plaintext}, - spoiler_text: %{"text/plain" => summary_plaintext} + spoiler_text: %{"text/plain" => summary_plaintext}, + direct_conversation_id: direct_conversation_id } } end diff --git a/test/web/mastodon_api/status_view_test.exs b/test/web/mastodon_api/status_view_test.exs index 0b167f839..c983b494f 100644 --- a/test/web/mastodon_api/status_view_test.exs +++ b/test/web/mastodon_api/status_view_test.exs @@ -23,6 +23,21 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do :ok end + test "returns the direct conversation id when given the `with_conversation_id` option" do + user = insert(:user) + + {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"}) + + status = + StatusView.render("status.json", + activity: activity, + with_direct_conversation_id: true, + for: user + ) + + assert status[:pleroma][:direct_conversation_id] + end + test "returns a temporary ap_id based user for activities missing db users" do user = insert(:user) @@ -133,7 +148,8 @@ test "a note activity" do conversation_id: convo_id, in_reply_to_account_acct: nil, content: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["content"])}, - spoiler_text: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["summary"])} + spoiler_text: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["summary"])}, + direct_conversation_id: nil } } From 58443d0cd683c227199eb34d660191292e487a14 Mon Sep 17 00:00:00 2001 From: Maksim Date: Wed, 31 Jul 2019 15:14:36 +0000 Subject: [PATCH 061/202] tests for TwitterApi/UtilController --- lib/pleroma/user.ex | 2 + .../controllers/util_controller.ex | 186 ++++---- test/support/http_request_mock.ex | 4 + test/web/twitter_api/util_controller_test.exs | 404 +++++++++++++++++- 4 files changed, 503 insertions(+), 93 deletions(-) diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 6e2fd3af8..1adb82f32 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -226,6 +226,7 @@ def password_update_changeset(struct, params) do |> put_password_hash end + @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()} def reset_password(%User{id: user_id} = user, data) do multi = Multi.new() @@ -330,6 +331,7 @@ def needs_update?(%User{local: false} = user) do def needs_update?(_), do: true + @spec maybe_direct_follow(User.t(), User.t()) :: {:ok, User.t()} | {:error, String.t()} def maybe_direct_follow(%User{} = follower, %User{local: true, info: %{locked: true}}) do {:ok, follower} end diff --git a/lib/pleroma/web/twitter_api/controllers/util_controller.ex b/lib/pleroma/web/twitter_api/controllers/util_controller.ex index 5c73a615d..3405bd3b7 100644 --- a/lib/pleroma/web/twitter_api/controllers/util_controller.ex +++ b/lib/pleroma/web/twitter_api/controllers/util_controller.ex @@ -18,6 +18,8 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do alias Pleroma.Web.CommonAPI alias Pleroma.Web.WebFinger + plug(Pleroma.Plugs.SetFormatPlug when action in [:config, :version]) + def help_test(conn, _params) do json(conn, "ok") end @@ -58,27 +60,25 @@ def remote_follow(%{assigns: %{user: user}} = conn, %{"acct" => acct}) do %Activity{id: activity_id} = Activity.get_create_by_object_ap_id(object.data["id"]) redirect(conn, to: "/notice/#{activity_id}") else - {err, followee} = User.get_or_fetch(acct) - avatar = User.avatar_url(followee) - name = followee.nickname - id = followee.id - - if !!user do + with {:ok, followee} <- User.get_or_fetch(acct) do conn - |> render("follow.html", %{error: err, acct: acct, avatar: avatar, name: name, id: id}) - else - conn - |> render("follow_login.html", %{ + |> render(follow_template(user), %{ error: false, acct: acct, - avatar: avatar, - name: name, - id: id + avatar: User.avatar_url(followee), + name: followee.nickname, + id: followee.id }) + else + {:error, _reason} -> + render(conn, follow_template(user), %{error: :error}) end end end + defp follow_template(%User{} = _user), do: "follow.html" + defp follow_template(_), do: "follow_login.html" + defp is_status?(acct) do case Pleroma.Object.Fetcher.fetch_and_contain_remote_object_from_id(acct) do {:ok, %{"type" => type}} when type in ["Article", "Note", "Video", "Page", "Question"] -> @@ -92,48 +92,53 @@ defp is_status?(acct) do def do_remote_follow(conn, %{ "authorization" => %{"name" => username, "password" => password, "id" => id} }) do - followee = User.get_cached_by_id(id) - avatar = User.avatar_url(followee) - name = followee.nickname - - with %User{} = user <- User.get_cached_by_nickname(username), - true <- AuthenticationPlug.checkpw(password, user.password_hash), - %User{} = _followed <- User.get_cached_by_id(id), + with %User{} = followee <- User.get_cached_by_id(id), + {_, %User{} = user, _} <- {:auth, User.get_cached_by_nickname(username), followee}, + {_, true, _} <- { + :auth, + AuthenticationPlug.checkpw(password, user.password_hash), + followee + }, {:ok, _follower, _followee, _activity} <- CommonAPI.follow(user, followee) do conn |> render("followed.html", %{error: false}) else # Was already following user {:error, "Could not follow user:" <> _rest} -> - render(conn, "followed.html", %{error: false}) + render(conn, "followed.html", %{error: "Error following account"}) - _e -> + {:auth, _, followee} -> conn |> render("follow_login.html", %{ error: "Wrong username or password", id: id, - name: name, - avatar: avatar + name: followee.nickname, + avatar: User.avatar_url(followee) }) + + e -> + Logger.debug("Remote follow failed with error #{inspect(e)}") + render(conn, "followed.html", %{error: "Something went wrong."}) end end def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}}) do - with %User{} = followee <- User.get_cached_by_id(id), + with {:fetch_user, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)}, {:ok, _follower, _followee, _activity} <- CommonAPI.follow(user, followee) do conn |> render("followed.html", %{error: false}) else # Was already following user {:error, "Could not follow user:" <> _rest} -> - conn - |> render("followed.html", %{error: false}) + render(conn, "followed.html", %{error: "Error following account"}) + + {:fetch_user, error} -> + Logger.debug("Remote follow failed with error #{inspect(error)}") + render(conn, "followed.html", %{error: "Could not find user"}) e -> Logger.debug("Remote follow failed with error #{inspect(e)}") - - conn - |> render("followed.html", %{error: inspect(e)}) + render(conn, "followed.html", %{error: "Something went wrong."}) end end @@ -148,67 +153,70 @@ def notifications_read(%{assigns: %{user: user}} = conn, %{"id" => notification_ end end + def config(%{assigns: %{format: "xml"}} = conn, _params) do + instance = Pleroma.Config.get(:instance) + + response = """ + + + #{Keyword.get(instance, :name)} + #{Web.base_url()} + #{Keyword.get(instance, :limit)} + #{!Keyword.get(instance, :registrations_open)} + + + """ + + conn + |> put_resp_content_type("application/xml") + |> send_resp(200, response) + end + def config(conn, _params) do instance = Pleroma.Config.get(:instance) - case get_format(conn) do - "xml" -> - response = """ - - - #{Keyword.get(instance, :name)} - #{Web.base_url()} - #{Keyword.get(instance, :limit)} - #{!Keyword.get(instance, :registrations_open)} - - - """ + vapid_public_key = Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) - conn - |> put_resp_content_type("application/xml") - |> send_resp(200, response) + uploadlimit = %{ + uploadlimit: to_string(Keyword.get(instance, :upload_limit)), + avatarlimit: to_string(Keyword.get(instance, :avatar_upload_limit)), + backgroundlimit: to_string(Keyword.get(instance, :background_upload_limit)), + bannerlimit: to_string(Keyword.get(instance, :banner_upload_limit)) + } - _ -> - vapid_public_key = Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) + data = %{ + name: Keyword.get(instance, :name), + description: Keyword.get(instance, :description), + server: Web.base_url(), + textlimit: to_string(Keyword.get(instance, :limit)), + uploadlimit: uploadlimit, + closed: bool_to_val(Keyword.get(instance, :registrations_open), "0", "1"), + private: bool_to_val(Keyword.get(instance, :public, true), "0", "1"), + vapidPublicKey: vapid_public_key, + accountActivationRequired: + bool_to_val(Keyword.get(instance, :account_activation_required, false)), + invitesEnabled: bool_to_val(Keyword.get(instance, :invites_enabled, false)), + safeDMMentionsEnabled: bool_to_val(Pleroma.Config.get([:instance, :safe_dm_mentions])) + } - uploadlimit = %{ - uploadlimit: to_string(Keyword.get(instance, :upload_limit)), - avatarlimit: to_string(Keyword.get(instance, :avatar_upload_limit)), - backgroundlimit: to_string(Keyword.get(instance, :background_upload_limit)), - bannerlimit: to_string(Keyword.get(instance, :banner_upload_limit)) - } - - data = %{ - name: Keyword.get(instance, :name), - description: Keyword.get(instance, :description), - server: Web.base_url(), - textlimit: to_string(Keyword.get(instance, :limit)), - uploadlimit: uploadlimit, - closed: if(Keyword.get(instance, :registrations_open), do: "0", else: "1"), - private: if(Keyword.get(instance, :public, true), do: "0", else: "1"), - vapidPublicKey: vapid_public_key, - accountActivationRequired: - if(Keyword.get(instance, :account_activation_required, false), do: "1", else: "0"), - invitesEnabled: if(Keyword.get(instance, :invites_enabled, false), do: "1", else: "0"), - safeDMMentionsEnabled: - if(Pleroma.Config.get([:instance, :safe_dm_mentions]), do: "1", else: "0") - } + managed_config = Keyword.get(instance, :managed_config) + data = + if managed_config do pleroma_fe = Pleroma.Config.get([:frontend_configurations, :pleroma_fe]) + Map.put(data, "pleromafe", pleroma_fe) + else + data + end - managed_config = Keyword.get(instance, :managed_config) - - data = - if managed_config do - data |> Map.put("pleromafe", pleroma_fe) - else - data - end - - json(conn, %{site: data}) - end + json(conn, %{site: data}) end + defp bool_to_val(true), do: "1" + defp bool_to_val(_), do: "0" + defp bool_to_val(true, val, _), do: val + defp bool_to_val(_, _, val), do: val + def frontend_configurations(conn, _params) do config = Pleroma.Config.get(:frontend_configurations, %{}) @@ -217,20 +225,16 @@ def frontend_configurations(conn, _params) do json(conn, config) end - def version(conn, _params) do + def version(%{assigns: %{format: "xml"}} = conn, _params) do version = Pleroma.Application.named_version() - case get_format(conn) do - "xml" -> - response = "#{version}" + conn + |> put_resp_content_type("application/xml") + |> send_resp(200, "#{version}") + end - conn - |> put_resp_content_type("application/xml") - |> send_resp(200, response) - - _ -> - json(conn, version) - end + def version(conn, _params) do + json(conn, Pleroma.Application.named_version()) end def emoji(conn, _params) do diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 2ed5f5042..d767ab9d4 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -51,6 +51,10 @@ def get("https://mastodon.social/users/emelie", _, _, _) do }} end + def get("https://mastodon.social/users/not_found", _, _, _) do + {:ok, %Tesla.Env{status: 404}} + end + def get("https://mastodon.sdf.org/users/rinpatch", _, _, _) do {:ok, %Tesla.Env{ diff --git a/test/web/twitter_api/util_controller_test.exs b/test/web/twitter_api/util_controller_test.exs index 3d699e1df..640579c09 100644 --- a/test/web/twitter_api/util_controller_test.exs +++ b/test/web/twitter_api/util_controller_test.exs @@ -14,6 +14,17 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do setup do Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end) + + instance_config = Pleroma.Config.get([:instance]) + pleroma_fe = Pleroma.Config.get([:frontend_configurations, :pleroma_fe]) + deny_follow_blocked = Pleroma.Config.get([:user, :deny_follow_blocked]) + + on_exit(fn -> + Pleroma.Config.put([:instance], instance_config) + Pleroma.Config.put([:frontend_configurations, :pleroma_fe], pleroma_fe) + Pleroma.Config.put([:user, :deny_follow_blocked], deny_follow_blocked) + end) + :ok end @@ -31,6 +42,35 @@ test "it returns HTTP 200", %{conn: conn} do assert response == "job started" end + test "it imports follow lists from file", %{conn: conn} do + user1 = insert(:user) + user2 = insert(:user) + + with_mocks([ + {File, [], + read!: fn "follow_list.txt" -> + "Account address,Show boosts\n#{user2.ap_id},true" + end}, + {PleromaJobQueue, [:passthrough], []} + ]) do + response = + conn + |> assign(:user, user1) + |> post("/api/pleroma/follow_import", %{"list" => %Plug.Upload{path: "follow_list.txt"}}) + |> json_response(:ok) + + assert called( + PleromaJobQueue.enqueue( + :background, + User, + [:follow_import, user1, [user2.ap_id]] + ) + ) + + assert response == "job started" + end + end + test "it imports new-style mastodon follow lists", %{conn: conn} do user1 = insert(:user) user2 = insert(:user) @@ -79,6 +119,33 @@ test "it returns HTTP 200", %{conn: conn} do assert response == "job started" end + + test "it imports blocks users from file", %{conn: conn} do + user1 = insert(:user) + user2 = insert(:user) + user3 = insert(:user) + + with_mocks([ + {File, [], read!: fn "blocks_list.txt" -> "#{user2.ap_id} #{user3.ap_id}" end}, + {PleromaJobQueue, [:passthrough], []} + ]) do + response = + conn + |> assign(:user, user1) + |> post("/api/pleroma/blocks_import", %{"list" => %Plug.Upload{path: "blocks_list.txt"}}) + |> json_response(:ok) + + assert called( + PleromaJobQueue.enqueue( + :background, + User, + [:blocks_import, user1, [user2.ap_id, user3.ap_id]] + ) + ) + + assert response == "job started" + end + end end describe "POST /api/pleroma/notifications/read" do @@ -98,6 +165,18 @@ test "it marks a single notification as read", %{conn: conn} do assert Repo.get(Notification, notification1.id).seen refute Repo.get(Notification, notification2.id).seen end + + test "it returns error when notification not found", %{conn: conn} do + user1 = insert(:user) + + response = + conn + |> assign(:user, user1) + |> post("/api/pleroma/notifications/read", %{"id" => "22222222222222"}) + |> json_response(403) + + assert response == %{"error" => "Cannot get notification"} + end end describe "PUT /api/pleroma/notification_settings" do @@ -123,7 +202,63 @@ test "it updates notification settings", %{conn: conn} do end end - describe "GET /api/statusnet/config.json" do + describe "GET /api/statusnet/config" do + test "it returns config in xml format", %{conn: conn} do + instance = Pleroma.Config.get(:instance) + + response = + conn + |> put_req_header("accept", "application/xml") + |> get("/api/statusnet/config") + |> response(:ok) + + assert response == + "\n\n#{Keyword.get(instance, :name)}\n#{ + Pleroma.Web.base_url() + }\n#{Keyword.get(instance, :limit)}\n#{ + !Keyword.get(instance, :registrations_open) + }\n\n\n" + end + + test "it returns config in json format", %{conn: conn} do + instance = Pleroma.Config.get(:instance) + Pleroma.Config.put([:instance, :managed_config], true) + Pleroma.Config.put([:instance, :registrations_open], false) + Pleroma.Config.put([:instance, :invites_enabled], true) + Pleroma.Config.put([:instance, :public], false) + Pleroma.Config.put([:frontend_configurations, :pleroma_fe], %{theme: "asuka-hospital"}) + + response = + conn + |> put_req_header("accept", "application/json") + |> get("/api/statusnet/config") + |> json_response(:ok) + + expected_data = %{ + "site" => %{ + "accountActivationRequired" => "0", + "closed" => "1", + "description" => Keyword.get(instance, :description), + "invitesEnabled" => "1", + "name" => Keyword.get(instance, :name), + "pleromafe" => %{"theme" => "asuka-hospital"}, + "private" => "1", + "safeDMMentionsEnabled" => "0", + "server" => Pleroma.Web.base_url(), + "textlimit" => to_string(Keyword.get(instance, :limit)), + "uploadlimit" => %{ + "avatarlimit" => to_string(Keyword.get(instance, :avatar_upload_limit)), + "backgroundlimit" => to_string(Keyword.get(instance, :background_upload_limit)), + "bannerlimit" => to_string(Keyword.get(instance, :banner_upload_limit)), + "uploadlimit" => to_string(Keyword.get(instance, :upload_limit)) + }, + "vapidPublicKey" => Keyword.get(Pleroma.Web.Push.vapid_config(), :public_key) + } + } + + assert response == expected_data + end + test "returns the state of safe_dm_mentions flag", %{conn: conn} do option = Pleroma.Config.get([:instance, :safe_dm_mentions]) Pleroma.Config.put([:instance, :safe_dm_mentions], true) @@ -210,7 +345,7 @@ test "returns json with custom emoji with tags", %{conn: conn} do end end - describe "GET /ostatus_subscribe?acct=...." do + describe "GET /ostatus_subscribe - remote_follow/2" do test "adds status to pleroma instance if the `acct` is a status", %{conn: conn} do conn = get( @@ -230,6 +365,172 @@ test "show follow account page if the `acct` is a account link", %{conn: conn} d assert html_response(response, 200) =~ "Log in to follow" end + + test "show follow page if the `acct` is a account link", %{conn: conn} do + user = insert(:user) + + response = + conn + |> assign(:user, user) + |> get("/ostatus_subscribe?acct=https://mastodon.social/users/emelie") + + assert html_response(response, 200) =~ "Remote follow" + end + + test "show follow page with error when user cannot fecth by `acct` link", %{conn: conn} do + user = insert(:user) + + response = + conn + |> assign(:user, user) + |> get("/ostatus_subscribe?acct=https://mastodon.social/users/not_found") + + assert html_response(response, 200) =~ "Error fetching user" + end + end + + describe "POST /ostatus_subscribe - do_remote_follow/2 with assigned user " do + test "follows user", %{conn: conn} do + user = insert(:user) + user2 = insert(:user) + + response = + conn + |> assign(:user, user) + |> post("/ostatus_subscribe", %{"user" => %{"id" => user2.id}}) + |> response(200) + + assert response =~ "Account followed!" + assert user2.follower_address in refresh_record(user).following + end + + test "returns error when user is deactivated", %{conn: conn} do + user = insert(:user, info: %{deactivated: true}) + user2 = insert(:user) + + response = + conn + |> assign(:user, user) + |> post("/ostatus_subscribe", %{"user" => %{"id" => user2.id}}) + |> response(200) + + assert response =~ "Error following account" + end + + test "returns error when user is blocked", %{conn: conn} do + Pleroma.Config.put([:user, :deny_follow_blocked], true) + user = insert(:user) + user2 = insert(:user) + + {:ok, _user} = Pleroma.User.block(user2, user) + + response = + conn + |> assign(:user, user) + |> post("/ostatus_subscribe", %{"user" => %{"id" => user2.id}}) + |> response(200) + + assert response =~ "Error following account" + end + + test "returns error when followee not found", %{conn: conn} do + user = insert(:user) + + response = + conn + |> assign(:user, user) + |> post("/ostatus_subscribe", %{"user" => %{"id" => "jimm"}}) + |> response(200) + + assert response =~ "Error following account" + end + + test "returns success result when user already in followers", %{conn: conn} do + user = insert(:user) + user2 = insert(:user) + {:ok, _, _, _} = CommonAPI.follow(user, user2) + + response = + conn + |> assign(:user, refresh_record(user)) + |> post("/ostatus_subscribe", %{"user" => %{"id" => user2.id}}) + |> response(200) + + assert response =~ "Account followed!" + end + end + + describe "POST /ostatus_subscribe - do_remote_follow/2 without assigned user " do + test "follows", %{conn: conn} do + user = insert(:user) + user2 = insert(:user) + + response = + conn + |> post("/ostatus_subscribe", %{ + "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id} + }) + |> response(200) + + assert response =~ "Account followed!" + assert user2.follower_address in refresh_record(user).following + end + + test "returns error when followee not found", %{conn: conn} do + user = insert(:user) + + response = + conn + |> post("/ostatus_subscribe", %{ + "authorization" => %{"name" => user.nickname, "password" => "test", "id" => "jimm"} + }) + |> response(200) + + assert response =~ "Error following account" + end + + test "returns error when login invalid", %{conn: conn} do + user = insert(:user) + + response = + conn + |> post("/ostatus_subscribe", %{ + "authorization" => %{"name" => "jimm", "password" => "test", "id" => user.id} + }) + |> response(200) + + assert response =~ "Wrong username or password" + end + + test "returns error when password invalid", %{conn: conn} do + user = insert(:user) + user2 = insert(:user) + + response = + conn + |> post("/ostatus_subscribe", %{ + "authorization" => %{"name" => user.nickname, "password" => "42", "id" => user2.id} + }) + |> response(200) + + assert response =~ "Wrong username or password" + end + + test "returns error when user is blocked", %{conn: conn} do + Pleroma.Config.put([:user, :deny_follow_blocked], true) + user = insert(:user) + user2 = insert(:user) + {:ok, _user} = Pleroma.User.block(user2, user) + + response = + conn + |> post("/ostatus_subscribe", %{ + "authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id} + }) + |> response(200) + + assert response =~ "Error following account" + end end describe "GET /api/pleroma/healthcheck" do @@ -311,5 +612,104 @@ test "it returns HTTP 200", %{conn: conn} do assert user.info.deactivated == true end + + test "it returns returns when password invalid", %{conn: conn} do + user = insert(:user) + + response = + conn + |> assign(:user, user) + |> post("/api/pleroma/disable_account", %{"password" => "test1"}) + |> json_response(:ok) + + assert response == %{"error" => "Invalid password."} + user = User.get_cached_by_id(user.id) + + refute user.info.deactivated + end + end + + describe "GET /api/statusnet/version" do + test "it returns version in xml format", %{conn: conn} do + response = + conn + |> put_req_header("accept", "application/xml") + |> get("/api/statusnet/version") + |> response(:ok) + + assert response == "#{Pleroma.Application.named_version()}" + end + + test "it returns version in json format", %{conn: conn} do + response = + conn + |> put_req_header("accept", "application/json") + |> get("/api/statusnet/version") + |> json_response(:ok) + + assert response == "#{Pleroma.Application.named_version()}" + end + end + + describe "POST /main/ostatus - remote_subscribe/2" do + test "renders subscribe form", %{conn: conn} do + user = insert(:user) + + response = + conn + |> post("/main/ostatus", %{"nickname" => user.nickname, "profile" => ""}) + |> response(:ok) + + refute response =~ "Could not find user" + assert response =~ "Remotely follow #{user.nickname}" + end + + test "renders subscribe form with error when user not found", %{conn: conn} do + response = + conn + |> post("/main/ostatus", %{"nickname" => "nickname", "profile" => ""}) + |> response(:ok) + + assert response =~ "Could not find user" + refute response =~ "Remotely follow" + end + + test "it redirect to webfinger url", %{conn: conn} do + user = insert(:user) + user2 = insert(:user, ap_id: "shp@social.heldscal.la") + + conn = + conn + |> post("/main/ostatus", %{ + "user" => %{"nickname" => user.nickname, "profile" => user2.ap_id} + }) + + assert redirected_to(conn) == + "https://social.heldscal.la/main/ostatussub?profile=#{user.ap_id}" + end + + test "it renders form with error when use not found", %{conn: conn} do + user2 = insert(:user, ap_id: "shp@social.heldscal.la") + + response = + conn + |> post("/main/ostatus", %{"user" => %{"nickname" => "jimm", "profile" => user2.ap_id}}) + |> response(:ok) + + assert response =~ "Something went wrong." + end + end + + test "it returns new captcha", %{conn: conn} do + with_mock Pleroma.Captcha, + new: fn -> "test_captcha" end do + resp = + conn + |> get("/api/pleroma/captcha") + |> response(200) + + assert resp == "\"test_captcha\"" + assert called(Pleroma.Captcha.new()) + end end end From 301ea0dc0466371032f44f3e936d1b951ed9784c Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 31 Jul 2019 19:37:55 +0300 Subject: [PATCH 062/202] Add tests for counters being updated on follow --- lib/pleroma/user.ex | 2 +- .../masto_closed_followers_page.json | 1 + .../masto_closed_following_page.json | 1 + test/support/http_request_mock.ex | 16 ++++ test/user_test.exs | 74 +++++++++++++++++++ test/web/activity_pub/activity_pub_test.exs | 20 ----- 6 files changed, 93 insertions(+), 21 deletions(-) create mode 100644 test/fixtures/users_mock/masto_closed_followers_page.json create mode 100644 test/fixtures/users_mock/masto_closed_following_page.json diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 7acf1e53c..69835f3dd 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -741,7 +741,7 @@ def fetch_follow_information(user) do end def update_follower_count(%User{} = user) do - unless !user.local and Pleroma.Config.get([:instance, :external_user_synchronization]) do + if user.local or !Pleroma.Config.get([:instance, :external_user_synchronization]) do follower_count_query = User.Query.build(%{followers: user, deactivated: false}) |> select([u], %{count: count(u.id)}) diff --git a/test/fixtures/users_mock/masto_closed_followers_page.json b/test/fixtures/users_mock/masto_closed_followers_page.json new file mode 100644 index 000000000..04ab0c4d3 --- /dev/null +++ b/test/fixtures/users_mock/masto_closed_followers_page.json @@ -0,0 +1 @@ +{"@context":"https://www.w3.org/ns/activitystreams","id":"http://localhost:4001/users/masto_closed/followers?page=1","type":"OrderedCollectionPage","totalItems":437,"next":"http://localhost:4001/users/masto_closed/followers?page=2","partOf":"http://localhost:4001/users/masto_closed/followers","orderedItems":["https://testing.uguu.ltd/users/rin","https://patch.cx/users/rin","https://letsalllovela.in/users/xoxo","https://pleroma.site/users/crushv","https://aria.company/users/boris","https://kawen.space/users/crushv","https://freespeech.host/users/cvcvcv","https://pleroma.site/users/picpub","https://pixelfed.social/users/nosleep","https://boopsnoot.gq/users/5c1896d162f7d337f90492a3","https://pikachu.rocks/users/waifu","https://royal.crablettesare.life/users/crablettes"]} diff --git a/test/fixtures/users_mock/masto_closed_following_page.json b/test/fixtures/users_mock/masto_closed_following_page.json new file mode 100644 index 000000000..8d8324699 --- /dev/null +++ b/test/fixtures/users_mock/masto_closed_following_page.json @@ -0,0 +1 @@ +{"@context":"https://www.w3.org/ns/activitystreams","id":"http://localhost:4001/users/masto_closed/following?page=1","type":"OrderedCollectionPage","totalItems":152,"next":"http://localhost:4001/users/masto_closed/following?page=2","partOf":"http://localhost:4001/users/masto_closed/following","orderedItems":["https://testing.uguu.ltd/users/rin","https://patch.cx/users/rin","https://letsalllovela.in/users/xoxo","https://pleroma.site/users/crushv","https://aria.company/users/boris","https://kawen.space/users/crushv","https://freespeech.host/users/cvcvcv","https://pleroma.site/users/picpub","https://pixelfed.social/users/nosleep","https://boopsnoot.gq/users/5c1896d162f7d337f90492a3","https://pikachu.rocks/users/waifu","https://royal.crablettesare.life/users/crablettes"]} diff --git a/test/support/http_request_mock.ex b/test/support/http_request_mock.ex index 2ed5f5042..bdfe43b28 100644 --- a/test/support/http_request_mock.ex +++ b/test/support/http_request_mock.ex @@ -792,6 +792,14 @@ def get("http://localhost:4001/users/masto_closed/followers", _, _, _) do }} end + def get("http://localhost:4001/users/masto_closed/followers?page=1", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/users_mock/masto_closed_followers_page.json") + }} + end + def get("http://localhost:4001/users/masto_closed/following", _, _, _) do {:ok, %Tesla.Env{ @@ -800,6 +808,14 @@ def get("http://localhost:4001/users/masto_closed/following", _, _, _) do }} end + def get("http://localhost:4001/users/masto_closed/following?page=1", _, _, _) do + {:ok, + %Tesla.Env{ + status: 200, + body: File.read!("test/fixtures/users_mock/masto_closed_following_page.json") + }} + end + def get("http://localhost:4001/users/fuser2/followers", _, _, _) do {:ok, %Tesla.Env{ diff --git a/test/user_test.exs b/test/user_test.exs index 556df45fd..7ec241c25 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -1393,4 +1393,78 @@ test "performs update cache if user updated" do assert %User{bio: "test-bio"} = User.get_cached_by_ap_id(user.ap_id) end end + + describe "following/followers synchronization" do + setup do + sync = Pleroma.Config.get([:instance, :external_user_synchronization]) + on_exit(fn -> Pleroma.Config.put([:instance, :external_user_synchronization], sync) end) + end + + test "updates the counters normally on following/getting a follow when disabled" do + Pleroma.Config.put([:instance, :external_user_synchronization], false) + user = insert(:user) + + other_user = + insert(:user, + local: false, + follower_address: "http://localhost:4001/users/masto_closed/followers", + following_address: "http://localhost:4001/users/masto_closed/following", + info: %{ap_enabled: true} + ) + + assert User.user_info(other_user).following_count == 0 + assert User.user_info(other_user).follower_count == 0 + + {:ok, user} = Pleroma.User.follow(user, other_user) + other_user = Pleroma.User.get_by_id(other_user.id) + + assert User.user_info(user).following_count == 1 + assert User.user_info(other_user).follower_count == 1 + end + + test "syncronizes the counters with the remote instance for the followed when enabled" do + Pleroma.Config.put([:instance, :external_user_synchronization], false) + + user = insert(:user) + + other_user = + insert(:user, + local: false, + follower_address: "http://localhost:4001/users/masto_closed/followers", + following_address: "http://localhost:4001/users/masto_closed/following", + info: %{ap_enabled: true} + ) + + assert User.user_info(other_user).following_count == 0 + assert User.user_info(other_user).follower_count == 0 + + Pleroma.Config.put([:instance, :external_user_synchronization], true) + {:ok, _user} = User.follow(user, other_user) + other_user = User.get_by_id(other_user.id) + + assert User.user_info(other_user).follower_count == 437 + end + + test "syncronizes the counters with the remote instance for the follower when enabled" do + Pleroma.Config.put([:instance, :external_user_synchronization], false) + + user = insert(:user) + + other_user = + insert(:user, + local: false, + follower_address: "http://localhost:4001/users/masto_closed/followers", + following_address: "http://localhost:4001/users/masto_closed/following", + info: %{ap_enabled: true} + ) + + assert User.user_info(other_user).following_count == 0 + assert User.user_info(other_user).follower_count == 0 + + Pleroma.Config.put([:instance, :external_user_synchronization], true) + {:ok, other_user} = User.follow(other_user, user) + + assert User.user_info(other_user).following_count == 152 + end + end end diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 853c93ab5..3d9a678dd 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -1149,16 +1149,6 @@ test "detects hidden followers" do "http://localhost:4001/users/masto_closed/followers?page=1" -> %Tesla.Env{status: 403, body: ""} - "http://localhost:4001/users/masto_closed/following?page=1" -> - %Tesla.Env{ - status: 200, - body: - Jason.encode!(%{ - "id" => "http://localhost:4001/users/masto_closed/following?page=1", - "type" => "OrderedCollectionPage" - }) - } - _ -> apply(HttpRequestMock, :request, [env]) end @@ -1182,16 +1172,6 @@ test "detects hidden follows" do "http://localhost:4001/users/masto_closed/following?page=1" -> %Tesla.Env{status: 403, body: ""} - "http://localhost:4001/users/masto_closed/followers?page=1" -> - %Tesla.Env{ - status: 200, - body: - Jason.encode!(%{ - "id" => "http://localhost:4001/users/masto_closed/followers?page=1", - "type" => "OrderedCollectionPage" - }) - } - _ -> apply(HttpRequestMock, :request, [env]) end From f72e0b7caddd96da67269552db3102733e4a2581 Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Wed, 31 Jul 2019 17:23:16 +0000 Subject: [PATCH 063/202] ostatus: explicitly disallow protocol downgrade from activitypub This closes embargoed bug #1135. --- CHANGELOG.md | 3 ++ .../web/ostatus/handlers/follow_handler.ex | 2 +- .../web/ostatus/handlers/note_handler.ex | 2 +- .../web/ostatus/handlers/unfollow_handler.ex | 2 +- lib/pleroma/web/ostatus/ostatus.ex | 17 +++++-- test/web/ostatus/ostatus_test.exs | 48 +++++++++++++++++-- 6 files changed, 63 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acd55362d..b02ed243b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] +### Security +- OStatus: eliminate the possibility of a protocol downgrade attack. + ### Changed - **Breaking:** Configuration: A setting to explicitly disable the mailer was added, defaulting to true, if you are using a mailer add `config :pleroma, Pleroma.Emails.Mailer, enabled: true` to your config - **Breaking:** Configuration: `/media/` is now removed when `base_url` is configured, append `/media/` to your `base_url` config to keep the old behaviour if desired diff --git a/lib/pleroma/web/ostatus/handlers/follow_handler.ex b/lib/pleroma/web/ostatus/handlers/follow_handler.ex index 263d3b2dc..03e4cbbb0 100644 --- a/lib/pleroma/web/ostatus/handlers/follow_handler.ex +++ b/lib/pleroma/web/ostatus/handlers/follow_handler.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Web.OStatus.FollowHandler do alias Pleroma.Web.XML def handle(entry, doc) do - with {:ok, actor} <- OStatus.find_make_or_update_user(doc), + with {:ok, actor} <- OStatus.find_make_or_update_actor(doc), id when not is_nil(id) <- XML.string_from_xpath("/entry/id", entry), followed_uri when not is_nil(followed_uri) <- XML.string_from_xpath("/entry/activity:object/id", entry), diff --git a/lib/pleroma/web/ostatus/handlers/note_handler.ex b/lib/pleroma/web/ostatus/handlers/note_handler.ex index 3005e8f57..7fae14f7b 100644 --- a/lib/pleroma/web/ostatus/handlers/note_handler.ex +++ b/lib/pleroma/web/ostatus/handlers/note_handler.ex @@ -111,7 +111,7 @@ def handle_note(entry, doc \\ nil, options \\ []) do with id <- XML.string_from_xpath("//id", entry), activity when is_nil(activity) <- Activity.get_create_by_object_ap_id_with_object(id), [author] <- :xmerl_xpath.string('//author[1]', doc), - {:ok, actor} <- OStatus.find_make_or_update_user(author), + {:ok, actor} <- OStatus.find_make_or_update_actor(author), content_html <- OStatus.get_content(entry), cw <- OStatus.get_cw(entry), in_reply_to <- XML.string_from_xpath("//thr:in-reply-to[1]/@ref", entry), diff --git a/lib/pleroma/web/ostatus/handlers/unfollow_handler.ex b/lib/pleroma/web/ostatus/handlers/unfollow_handler.ex index 6596ada3b..2062432e3 100644 --- a/lib/pleroma/web/ostatus/handlers/unfollow_handler.ex +++ b/lib/pleroma/web/ostatus/handlers/unfollow_handler.ex @@ -9,7 +9,7 @@ defmodule Pleroma.Web.OStatus.UnfollowHandler do alias Pleroma.Web.XML def handle(entry, doc) do - with {:ok, actor} <- OStatus.find_make_or_update_user(doc), + with {:ok, actor} <- OStatus.find_make_or_update_actor(doc), id when not is_nil(id) <- XML.string_from_xpath("/entry/id", entry), followed_uri when not is_nil(followed_uri) <- XML.string_from_xpath("/entry/activity:object/id", entry), diff --git a/lib/pleroma/web/ostatus/ostatus.ex b/lib/pleroma/web/ostatus/ostatus.ex index 502410c83..331cbc0b7 100644 --- a/lib/pleroma/web/ostatus/ostatus.ex +++ b/lib/pleroma/web/ostatus/ostatus.ex @@ -56,7 +56,7 @@ def remote_follow_path do def handle_incoming(xml_string, options \\ []) do with doc when doc != :error <- parse_document(xml_string) do - with {:ok, actor_user} <- find_make_or_update_user(doc), + with {:ok, actor_user} <- find_make_or_update_actor(doc), do: Pleroma.Instances.set_reachable(actor_user.ap_id) entries = :xmerl_xpath.string('//entry', doc) @@ -120,7 +120,7 @@ def handle_incoming(xml_string, options \\ []) do end def make_share(entry, doc, retweeted_activity) do - with {:ok, actor} <- find_make_or_update_user(doc), + with {:ok, actor} <- find_make_or_update_actor(doc), %Object{} = object <- Object.normalize(retweeted_activity), id when not is_nil(id) <- string_from_xpath("/entry/id", entry), {:ok, activity, _object} = ActivityPub.announce(actor, object, id, false) do @@ -138,7 +138,7 @@ def handle_share(entry, doc) do end def make_favorite(entry, doc, favorited_activity) do - with {:ok, actor} <- find_make_or_update_user(doc), + with {:ok, actor} <- find_make_or_update_actor(doc), %Object{} = object <- Object.normalize(favorited_activity), id when not is_nil(id) <- string_from_xpath("/entry/id", entry), {:ok, activity, _object} = ActivityPub.like(actor, object, id, false) do @@ -264,11 +264,18 @@ def maybe_update_ostatus(doc, user) do end end - def find_make_or_update_user(doc) do + def find_make_or_update_actor(doc) do uri = string_from_xpath("//author/uri[1]", doc) - with {:ok, user} <- find_or_make_user(uri) do + with {:ok, %User{} = user} <- find_or_make_user(uri), + {:ap_enabled, false} <- {:ap_enabled, User.ap_enabled?(user)} do maybe_update(doc, user) + else + {:ap_enabled, true} -> + {:error, :invalid_protocol} + + _ -> + {:error, :unknown_user} end end diff --git a/test/web/ostatus/ostatus_test.exs b/test/web/ostatus/ostatus_test.exs index 4e8f3a0fc..d244dbcf7 100644 --- a/test/web/ostatus/ostatus_test.exs +++ b/test/web/ostatus/ostatus_test.exs @@ -426,7 +426,7 @@ test "find_or_make_user sets all the nessary input fields" do } end - test "find_make_or_update_user takes an author element and returns an updated user" do + test "find_make_or_update_actor takes an author element and returns an updated user" do uri = "https://social.heldscal.la/user/23211" {:ok, user} = OStatus.find_or_make_user(uri) @@ -439,14 +439,56 @@ test "find_make_or_update_user takes an author element and returns an updated us doc = XML.parse_document(File.read!("test/fixtures/23211.atom")) [author] = :xmerl_xpath.string('//author[1]', doc) - {:ok, user} = OStatus.find_make_or_update_user(author) + {:ok, user} = OStatus.find_make_or_update_actor(author) assert user.avatar["type"] == "Image" assert user.name == old_name assert user.bio == old_bio - {:ok, user_again} = OStatus.find_make_or_update_user(author) + {:ok, user_again} = OStatus.find_make_or_update_actor(author) assert user_again == user end + + test "find_or_make_user disallows protocol downgrade" do + user = insert(:user, %{local: true}) + {:ok, user} = OStatus.find_or_make_user(user.ap_id) + + assert User.ap_enabled?(user) + + user = + insert(:user, %{ + ap_id: "https://social.heldscal.la/user/23211", + info: %{ap_enabled: true}, + local: false + }) + + assert User.ap_enabled?(user) + + {:ok, user} = OStatus.find_or_make_user(user.ap_id) + assert User.ap_enabled?(user) + end + + test "find_make_or_update_actor disallows protocol downgrade" do + user = insert(:user, %{local: true}) + {:ok, user} = OStatus.find_or_make_user(user.ap_id) + + assert User.ap_enabled?(user) + + user = + insert(:user, %{ + ap_id: "https://social.heldscal.la/user/23211", + info: %{ap_enabled: true}, + local: false + }) + + assert User.ap_enabled?(user) + + {:ok, user} = OStatus.find_or_make_user(user.ap_id) + assert User.ap_enabled?(user) + + doc = XML.parse_document(File.read!("test/fixtures/23211.atom")) + [author] = :xmerl_xpath.string('//author[1]', doc) + {:error, :invalid_protocol} = OStatus.find_make_or_update_actor(author) + end end describe "gathering user info from a user id" do From 6eb33e73035789fd9160e697617feb30a3070589 Mon Sep 17 00:00:00 2001 From: Maksim Date: Wed, 31 Jul 2019 18:35:15 +0000 Subject: [PATCH 064/202] test for Pleroma.Web.CommonAPI.Utils.get_by_id_or_ap_id --- lib/pleroma/flake_id.ex | 10 ++++++++++ lib/pleroma/web/common_api/utils.ex | 7 ++++++- test/flake_id_test.exs | 5 +++++ test/web/common_api/common_api_utils_test.exs | 20 +++++++++++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) diff --git a/lib/pleroma/flake_id.ex b/lib/pleroma/flake_id.ex index 58ab3650d..ca0610abc 100644 --- a/lib/pleroma/flake_id.ex +++ b/lib/pleroma/flake_id.ex @@ -66,6 +66,16 @@ def from_integer(integer) do @spec get :: binary def get, do: to_string(:gen_server.call(:flake, :get)) + # checks that ID is is valid FlakeID + # + @spec is_flake_id?(String.t()) :: boolean + def is_flake_id?(id), do: is_flake_id?(String.to_charlist(id), true) + defp is_flake_id?([c | cs], true) when c >= ?0 and c <= ?9, do: is_flake_id?(cs, true) + defp is_flake_id?([c | cs], true) when c >= ?A and c <= ?Z, do: is_flake_id?(cs, true) + defp is_flake_id?([c | cs], true) when c >= ?a and c <= ?z, do: is_flake_id?(cs, true) + defp is_flake_id?([], true), do: true + defp is_flake_id?(_, _), do: false + # -- Ecto.Type API @impl Ecto.Type def type, do: :uuid diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index d80fffa26..c8a743e8e 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -24,7 +24,12 @@ defmodule Pleroma.Web.CommonAPI.Utils do # This is a hack for twidere. def get_by_id_or_ap_id(id) do activity = - Activity.get_by_id_with_object(id) || Activity.get_create_by_object_ap_id_with_object(id) + with true <- Pleroma.FlakeId.is_flake_id?(id), + %Activity{} = activity <- Activity.get_by_id_with_object(id) do + activity + else + _ -> Activity.get_create_by_object_ap_id_with_object(id) + end activity && if activity.data["type"] == "Create" do diff --git a/test/flake_id_test.exs b/test/flake_id_test.exs index ca2338041..85ed5bbdf 100644 --- a/test/flake_id_test.exs +++ b/test/flake_id_test.exs @@ -39,4 +39,9 @@ test "ecto type behaviour" do assert dump(flake_s) == {:ok, flake} assert dump(flake) == {:ok, flake} end + + test "is_flake_id?/1" do + assert is_flake_id?("9eoozpwTul5mjSEDRI") + refute is_flake_id?("http://example.com/activities/3ebbadd1-eb14-4e20-8118-b6f79c0c7b0b") + end end diff --git a/test/web/common_api/common_api_utils_test.exs b/test/web/common_api/common_api_utils_test.exs index af320f31f..4b5666c29 100644 --- a/test/web/common_api/common_api_utils_test.exs +++ b/test/web/common_api/common_api_utils_test.exs @@ -360,4 +360,24 @@ test "for direct posts, a reply" do assert third_user.ap_id in to end end + + describe "get_by_id_or_ap_id/1" do + test "get activity by id" do + activity = insert(:note_activity) + %Pleroma.Activity{} = note = Utils.get_by_id_or_ap_id(activity.id) + assert note.id == activity.id + end + + test "get activity by ap_id" do + activity = insert(:note_activity) + %Pleroma.Activity{} = note = Utils.get_by_id_or_ap_id(activity.data["object"]) + assert note.id == activity.id + end + + test "get activity by object when type isn't `Create` " do + activity = insert(:like_activity) + %Pleroma.Activity{} = like = Utils.get_by_id_or_ap_id(activity.id) + assert like.data["object"] == activity.data["object"] + end + end end From 813c686dd77e6d441c235b2f7a57ac7911e249af Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 31 Jul 2019 22:05:12 +0300 Subject: [PATCH 065/202] Disallow following locked accounts over OStatus --- lib/pleroma/web/ostatus/handlers/follow_handler.ex | 4 ++++ test/web/ostatus/ostatus_test.exs | 8 ++++++++ 2 files changed, 12 insertions(+) diff --git a/lib/pleroma/web/ostatus/handlers/follow_handler.ex b/lib/pleroma/web/ostatus/handlers/follow_handler.ex index 03e4cbbb0..24513972e 100644 --- a/lib/pleroma/web/ostatus/handlers/follow_handler.ex +++ b/lib/pleroma/web/ostatus/handlers/follow_handler.ex @@ -14,9 +14,13 @@ def handle(entry, doc) do followed_uri when not is_nil(followed_uri) <- XML.string_from_xpath("/entry/activity:object/id", entry), {:ok, followed} <- OStatus.find_or_make_user(followed_uri), + {:locked, false} <- {:locked, followed.info.locked}, {:ok, activity} <- ActivityPub.follow(actor, followed, id, false) do User.follow(actor, followed) {:ok, activity} + else + {:locked, true} -> + {:error, "It's not possible to follow locked accounts over OStatus"} end end end diff --git a/test/web/ostatus/ostatus_test.exs b/test/web/ostatus/ostatus_test.exs index d244dbcf7..f8d389020 100644 --- a/test/web/ostatus/ostatus_test.exs +++ b/test/web/ostatus/ostatus_test.exs @@ -326,6 +326,14 @@ test "handle incoming follows" do assert User.following?(follower, followed) end + test "refuse following over OStatus if the followed's account is locked" do + incoming = File.read!("test/fixtures/follow.xml") + _user = insert(:user, info: %{locked: true}, ap_id: "https://pawoo.net/users/pekorino") + + {:ok, [{:error, "It's not possible to follow locked accounts over OStatus"}]} = + OStatus.handle_incoming(incoming) + end + test "handle incoming unfollows with existing follow" do incoming_follow = File.read!("test/fixtures/follow.xml") {:ok, [_activity]} = OStatus.handle_incoming(incoming_follow) From def0c49ead94d21a63bdc7323521b6d73ad4c0b2 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Wed, 31 Jul 2019 23:03:06 +0300 Subject: [PATCH 066/202] Add a changelog entry for disallowing locked accounts follows over OStatus --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b02ed243b..bd64b2259 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## [Unreleased] ### Security - OStatus: eliminate the possibility of a protocol downgrade attack. +- OStatus: prevent following locked accounts, bypassing the approval process. ### Changed - **Breaking:** Configuration: A setting to explicitly disable the mailer was added, defaulting to true, if you are using a mailer add `config :pleroma, Pleroma.Emails.Mailer, enabled: true` to your config From 9ca45063556f3b75860d516577776a00536e90a8 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 1 Aug 2019 15:53:37 +0700 Subject: [PATCH 067/202] Add configurable length limits for `User.bio` and `User.name` --- config/config.exs | 2 ++ docs/config.md | 2 ++ lib/pleroma/user.ex | 38 +++++++++++++++++++++----------------- test/user_test.exs | 5 ++++- 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/config/config.exs b/config/config.exs index 17770640a..aa4cdf409 100644 --- a/config/config.exs +++ b/config/config.exs @@ -253,6 +253,8 @@ skip_thread_containment: true, limit_to_local_content: :unauthenticated, dynamic_configuration: false, + user_bio_length: 5000, + user_name_length: 100, external_user_synchronization: true config :pleroma, :markup, diff --git a/docs/config.md b/docs/config.md index 02f86dc16..8f58eaf06 100644 --- a/docs/config.md +++ b/docs/config.md @@ -125,6 +125,8 @@ config :pleroma, Pleroma.Emails.Mailer, * `safe_dm_mentions`: If set to true, only mentions at the beginning of a post will be used to address people in direct messages. This is to prevent accidental mentioning of people when talking about them (e.g. "@friend hey i really don't like @enemy"). Default: `false`. * `healthcheck`: If set to true, system data will be shown on ``/api/pleroma/healthcheck``. * `remote_post_retention_days`: The default amount of days to retain remote posts when pruning the database. +* `user_bio_length`: A user bio maximum length (default: `5000`) +* `user_name_length`: A user name maximum length (default: `100`) * `skip_thread_containment`: Skip filter out broken threads. The default is `false`. * `limit_to_local_content`: Limit unauthenticated users to search for local statutes and users only. Possible values: `:unauthenticated`, `:all` and `false`. The default is `:unauthenticated`. * `dynamic_configuration`: Allow transferring configuration to DB with the subsequent customization from Admin api. diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 1adb82f32..776dbbe6d 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -149,10 +149,10 @@ def following_count(%User{} = user) do end def remote_user_creation(params) do - params = - params - |> Map.put(:info, params[:info] || %{}) + bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) + name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) + params = Map.put(params, :info, params[:info] || %{}) info_cng = User.Info.remote_user_creation(%User.Info{}, params[:info]) changes = @@ -161,8 +161,8 @@ def remote_user_creation(params) do |> validate_required([:name, :ap_id]) |> unique_constraint(:nickname) |> validate_format(:nickname, @email_regex) - |> validate_length(:bio, max: 5000) - |> validate_length(:name, max: 100) + |> validate_length(:bio, max: bio_limit) + |> validate_length(:name, max: name_limit) |> put_change(:local, false) |> put_embed(:info, info_cng) @@ -185,22 +185,23 @@ def remote_user_creation(params) do end def update_changeset(struct, params \\ %{}) do + bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) + name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) + struct |> cast(params, [:bio, :name, :avatar, :following]) |> unique_constraint(:nickname) |> validate_format(:nickname, local_nickname_regex()) - |> validate_length(:bio, max: 5000) - |> validate_length(:name, min: 1, max: 100) + |> validate_length(:bio, max: bio_limit) + |> validate_length(:name, min: 1, max: name_limit) end def upgrade_changeset(struct, params \\ %{}) do - params = - params - |> Map.put(:last_refreshed_at, NaiveDateTime.utc_now()) + bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) + name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) - info_cng = - struct.info - |> User.Info.user_upgrade(params[:info]) + params = Map.put(params, :last_refreshed_at, NaiveDateTime.utc_now()) + info_cng = User.Info.user_upgrade(struct.info, params[:info]) struct |> cast(params, [ @@ -213,8 +214,8 @@ def upgrade_changeset(struct, params \\ %{}) do ]) |> unique_constraint(:nickname) |> validate_format(:nickname, local_nickname_regex()) - |> validate_length(:bio, max: 5000) - |> validate_length(:name, max: 100) + |> validate_length(:bio, max: bio_limit) + |> validate_length(:name, max: name_limit) |> put_embed(:info, info_cng) end @@ -241,6 +242,9 @@ def reset_password(%User{id: user_id} = user, data) do end def register_changeset(struct, params \\ %{}, opts \\ []) do + bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) + name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) + need_confirmation? = if is_nil(opts[:need_confirmation]) do Pleroma.Config.get([:instance, :account_activation_required]) @@ -261,8 +265,8 @@ def register_changeset(struct, params \\ %{}, opts \\ []) do |> validate_exclusion(:nickname, Pleroma.Config.get([User, :restricted_nicknames])) |> validate_format(:nickname, local_nickname_regex()) |> validate_format(:email, @email_regex) - |> validate_length(:bio, max: 1000) - |> validate_length(:name, min: 1, max: 100) + |> validate_length(:bio, max: bio_limit) + |> validate_length(:name, min: 1, max: name_limit) |> put_change(:info, info_change) changeset = diff --git a/test/user_test.exs b/test/user_test.exs index 556df45fd..dfa91a106 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -525,7 +525,10 @@ test "it has required fields" do end test "it restricts some sizes" do - [bio: 5000, name: 100] + bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) + name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) + + [bio: bio_limit, name: name_limit] |> Enum.each(fn {field, size} -> string = String.pad_leading(".", size) cs = User.remote_user_creation(Map.put(@valid_remote, field, string)) From f98235f2adfff290d95c7353c63225c07e5f86ff Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 1 Aug 2019 16:33:36 +0700 Subject: [PATCH 068/202] 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 069/202] 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 fd4b7239cd6f44a25c9aa4195750e94e0612a3b1 Mon Sep 17 00:00:00 2001 From: lain Date: Thu, 1 Aug 2019 17:25:46 +0200 Subject: [PATCH 070/202] nothing From f88560accd801ac88c60344cef93ef00cf136069 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 2 Aug 2019 11:55:41 +0200 Subject: [PATCH 071/202] Conversations: Add recipient list to conversation participation. This enables to address the same group of people every time. --- lib/pleroma/conversation.ex | 11 ++++++ lib/pleroma/conversation/participation.ex | 4 +++ .../participation_recipient_ship.ex | 34 +++++++++++++++++++ lib/pleroma/user.ex | 7 ++++ .../20190205114625_create_thread_mutes.exs | 2 +- ...ersation_participation_recipient_ships.exs | 13 +++++++ test/conversation/participation_test.exs | 30 ++++++++++++++++ 7 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 lib/pleroma/conversation/participation_recipient_ship.ex create mode 100644 priv/repo/migrations/20190801154554_create_conversation_participation_recipient_ships.exs diff --git a/lib/pleroma/conversation.ex b/lib/pleroma/conversation.ex index bc97b39ca..fb0dfedca 100644 --- a/lib/pleroma/conversation.ex +++ b/lib/pleroma/conversation.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Conversation do alias Pleroma.Conversation.Participation + alias Pleroma.Conversation.Participation.RecipientShip alias Pleroma.Repo alias Pleroma.User use Ecto.Schema @@ -39,6 +40,15 @@ def get_for_ap_id(ap_id) do Repo.get_by(__MODULE__, ap_id: ap_id) end + def maybe_set_recipients(participation, activity) do + participation = Repo.preload(participation, :recipients) + + if participation.recipients |> Enum.empty?() do + recipients = User.get_all_by_ap_id(activity.recipients) + RecipientShip.create(recipients, participation) + end + end + @doc """ This will 1. Create a conversation if there isn't one already @@ -60,6 +70,7 @@ def create_or_bump_for(activity, opts \\ []) do {:ok, participation} = Participation.create_for_user_and_conversation(user, conversation, opts) + maybe_set_recipients(participation, activity) participation end) diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex index 77b3f61e9..121efb671 100644 --- a/lib/pleroma/conversation/participation.ex +++ b/lib/pleroma/conversation/participation.ex @@ -5,6 +5,7 @@ defmodule Pleroma.Conversation.Participation do use Ecto.Schema alias Pleroma.Conversation + alias Pleroma.Conversation.Participation.RecipientShip alias Pleroma.Repo alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub @@ -17,6 +18,9 @@ defmodule Pleroma.Conversation.Participation do field(:read, :boolean, default: false) field(:last_activity_id, Pleroma.FlakeId, virtual: true) + has_many(:recipient_ships, RecipientShip) + has_many(:recipients, through: [:recipient_ships, :user]) + timestamps() end diff --git a/lib/pleroma/conversation/participation_recipient_ship.ex b/lib/pleroma/conversation/participation_recipient_ship.ex new file mode 100644 index 000000000..27c0c89cd --- /dev/null +++ b/lib/pleroma/conversation/participation_recipient_ship.ex @@ -0,0 +1,34 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Conversation.Participation.RecipientShip do + use Ecto.Schema + + alias Pleroma.Conversation.Participation + alias Pleroma.User + alias Pleroma.Repo + + import Ecto.Changeset + + schema "conversation_participation_recipient_ships" do + belongs_to(:user, User, type: Pleroma.FlakeId) + belongs_to(:participation, Participation) + end + + def creation_cng(struct, params) do + struct + |> cast(params, [:user_id, :participation_id]) + |> validate_required([:user_id, :participation_id]) + end + + def create(%User{} = user, participation), do: create([user], participation) + + def create(users, participation) do + Enum.each(users, fn user -> + %__MODULE__{} + |> creation_cng(%{user_id: user.id, participation_id: participation.id}) + |> Repo.insert!() + end) + end +end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 6e2fd3af8..a021e77f0 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -450,6 +450,13 @@ def get_by_ap_id(ap_id) do Repo.get_by(User, ap_id: ap_id) end + def get_all_by_ap_id(ap_ids) do + from(u in __MODULE__, + where: u.ap_id in ^ap_ids + ) + |> Repo.all() + end + # This is mostly an SPC migration fix. This guesses the user nickname by taking the last part # of the ap_id and the domain and tries to get that user def get_by_guessed_nickname(ap_id) do diff --git a/priv/repo/migrations/20190205114625_create_thread_mutes.exs b/priv/repo/migrations/20190205114625_create_thread_mutes.exs index 7e44db121..baaf07253 100644 --- a/priv/repo/migrations/20190205114625_create_thread_mutes.exs +++ b/priv/repo/migrations/20190205114625_create_thread_mutes.exs @@ -6,7 +6,7 @@ def change do add :user_id, references(:users, type: :uuid, on_delete: :delete_all) add :context, :string end - + create_if_not_exists unique_index(:thread_mutes, [:user_id, :context], name: :unique_index) end end diff --git a/priv/repo/migrations/20190801154554_create_conversation_participation_recipient_ships.exs b/priv/repo/migrations/20190801154554_create_conversation_participation_recipient_ships.exs new file mode 100644 index 000000000..c6e3469d5 --- /dev/null +++ b/priv/repo/migrations/20190801154554_create_conversation_participation_recipient_ships.exs @@ -0,0 +1,13 @@ +defmodule Pleroma.Repo.Migrations.CreateConversationParticipationRecipientShips do + use Ecto.Migration + + def change do + create_if_not_exists table(:conversation_participation_recipient_ships) do + add(:user_id, references(:users, type: :uuid, on_delete: :delete_all)) + add(:participation_id, references(:conversation_participations, on_delete: :delete_all)) + end + + create_if_not_exists index(:conversation_participation_recipient_ships, [:user_id]) + create_if_not_exists index(:conversation_participation_recipient_ships, [:participation_id]) + end +end diff --git a/test/conversation/participation_test.exs b/test/conversation/participation_test.exs index 2a03e5d67..4a3c397bd 100644 --- a/test/conversation/participation_test.exs +++ b/test/conversation/participation_test.exs @@ -8,6 +8,36 @@ defmodule Pleroma.Conversation.ParticipationTest do alias Pleroma.Conversation.Participation alias Pleroma.Web.CommonAPI + test "for a new conversation, it sets the recipents of the participation" do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{"status" => "Hey @#{other_user.nickname}.", "visibility" => "direct"}) + + [participation] = Participation.for_user(user) + participation = Pleroma.Repo.preload(participation, :recipients) + + assert length(participation.recipients) == 2 + assert user in participation.recipients + assert other_user in participation.recipients + + # Mentioning another user in the same conversation will not add a new recipients. + + {:ok, _activity} = + CommonAPI.post(user, %{ + "in_reply_to_status_id" => activity.id, + "status" => "Hey @#{third_user.nickname}.", + "visibility" => "direct" + }) + + [participation] = Participation.for_user(user) + participation = Pleroma.Repo.preload(participation, :recipients) + + assert length(participation.recipients) == 2 + end + test "it creates a participation for a conversation and a user" do user = insert(:user) conversation = insert(:conversation) From 56b1c3af13c9519e13da688bdbbfdd8d69cda4ac Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 2 Aug 2019 15:05:27 +0200 Subject: [PATCH 072/202] CommonAPI: Extend api with conversation replies. --- lib/pleroma/conversation/participation.ex | 6 +++++ lib/pleroma/web/common_api/common_api.ex | 20 ++++++++++----- lib/pleroma/web/common_api/utils.ex | 27 ++++++++++++++------ test/web/common_api/common_api_test.exs | 30 +++++++++++++++++++++++ 4 files changed, 70 insertions(+), 13 deletions(-) diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex index 121efb671..f1e1a6958 100644 --- a/lib/pleroma/conversation/participation.ex +++ b/lib/pleroma/conversation/participation.ex @@ -93,4 +93,10 @@ def for_user_with_last_activity_id(user, params \\ %{}) do end) |> Enum.filter(& &1.last_activity_id) end + + def get(nil), do: nil + + def get(id) do + Repo.get(__MODULE__, id) + end end diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 2db58324b..86e95cd0f 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.CommonAPI do alias Pleroma.Activity + alias Pleroma.Conversation.Participation alias Pleroma.Formatter alias Pleroma.Object alias Pleroma.ThreadMute @@ -171,21 +172,25 @@ defp normalize_and_validate_choice_indices(choices, count) do end) end - def get_visibility(%{"visibility" => visibility}, in_reply_to) + def get_visibility(_, _, %Participation{}) do + {"direct", "direct"} + end + + def get_visibility(%{"visibility" => visibility}, in_reply_to, _) when visibility in ~w{public unlisted private direct}, do: {visibility, get_replied_to_visibility(in_reply_to)} - def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to) do + def get_visibility(%{"visibility" => "list:" <> list_id}, in_reply_to, _) do visibility = {:list, String.to_integer(list_id)} {visibility, get_replied_to_visibility(in_reply_to)} end - def get_visibility(_, in_reply_to) when not is_nil(in_reply_to) do + def get_visibility(_, in_reply_to, _) when not is_nil(in_reply_to) do visibility = get_replied_to_visibility(in_reply_to) {visibility, visibility} end - def get_visibility(_, in_reply_to), do: {"public", get_replied_to_visibility(in_reply_to)} + def get_visibility(_, in_reply_to, _), do: {"public", get_replied_to_visibility(in_reply_to)} def get_replied_to_visibility(nil), do: nil @@ -201,7 +206,9 @@ def post(user, %{"status" => status} = data) do with status <- String.trim(status), attachments <- attachments_from_ids(data), in_reply_to <- get_replied_to_activity(data["in_reply_to_status_id"]), - {visibility, in_reply_to_visibility} <- get_visibility(data, in_reply_to), + in_reply_to_conversation <- Participation.get(data["in_reply_to_conversation_id"]), + {visibility, in_reply_to_visibility} <- + get_visibility(data, in_reply_to, in_reply_to_conversation), {_, false} <- {:private_to_public, in_reply_to_visibility == "direct" && visibility != "direct"}, {content_html, mentions, tags} <- @@ -214,7 +221,8 @@ def post(user, %{"status" => status} = data) do mentioned_users <- for({_, mentioned_user} <- mentions, do: mentioned_user.ap_id), addressed_users <- get_addressed_users(mentioned_users, data["to"]), {poll, poll_emoji} <- make_poll_data(data), - {to, cc} <- get_to_and_cc(user, addressed_users, in_reply_to, visibility), + {to, cc} <- + get_to_and_cc(user, addressed_users, in_reply_to, visibility, in_reply_to_conversation), context <- make_context(in_reply_to), cw <- data["spoiler_text"] || "", sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}), diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index d80fffa26..e70ba7d43 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -8,6 +8,7 @@ defmodule Pleroma.Web.CommonAPI.Utils do alias Calendar.Strftime alias Pleroma.Activity alias Pleroma.Config + alias Pleroma.Conversation.Participation alias Pleroma.Formatter alias Pleroma.Object alias Pleroma.Plugs.AuthenticationPlug @@ -64,9 +65,21 @@ def attachments_from_ids_descs(ids, descs_str) do end) end - @spec get_to_and_cc(User.t(), list(String.t()), Activity.t() | nil, String.t()) :: + @spec get_to_and_cc( + User.t(), + list(String.t()), + Activity.t() | nil, + String.t(), + Participation.t() | nil + ) :: {list(String.t()), list(String.t())} - def get_to_and_cc(user, mentioned_users, inReplyTo, "public") do + + def get_to_and_cc(_, _, _, _, %Participation{} = participation) do + participation = Repo.preload(participation, :recipients) + {Enum.map(participation.recipients, & &1.ap_id), []} + end + + def get_to_and_cc(user, mentioned_users, inReplyTo, "public", _) do to = [Pleroma.Constants.as_public() | mentioned_users] cc = [user.follower_address] @@ -77,7 +90,7 @@ def get_to_and_cc(user, mentioned_users, inReplyTo, "public") do end end - def get_to_and_cc(user, mentioned_users, inReplyTo, "unlisted") do + def get_to_and_cc(user, mentioned_users, inReplyTo, "unlisted", _) do to = [user.follower_address | mentioned_users] cc = [Pleroma.Constants.as_public()] @@ -88,12 +101,12 @@ def get_to_and_cc(user, mentioned_users, inReplyTo, "unlisted") do end end - def get_to_and_cc(user, mentioned_users, inReplyTo, "private") do - {to, cc} = get_to_and_cc(user, mentioned_users, inReplyTo, "direct") + def get_to_and_cc(user, mentioned_users, inReplyTo, "private", _) do + {to, cc} = get_to_and_cc(user, mentioned_users, inReplyTo, "direct", nil) {[user.follower_address | to], cc} end - def get_to_and_cc(_user, mentioned_users, inReplyTo, "direct") do + def get_to_and_cc(_user, mentioned_users, inReplyTo, "direct", _) do if inReplyTo do {Enum.uniq([inReplyTo.data["actor"] | mentioned_users]), []} else @@ -101,7 +114,7 @@ def get_to_and_cc(_user, mentioned_users, inReplyTo, "direct") do end end - def get_to_and_cc(_user, mentions, _inReplyTo, {:list, _}), do: {mentions, []} + def get_to_and_cc(_user, mentions, _inReplyTo, {:list, _}, _), do: {mentions, []} def get_addressed_users(_, to) when is_list(to) do User.get_ap_ids_by_nicknames(to) diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index 16b3f121d..e2a5bf117 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -5,6 +5,7 @@ defmodule Pleroma.Web.CommonAPITest do use Pleroma.DataCase alias Pleroma.Activity + alias Pleroma.Conversation.Participation alias Pleroma.Object alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub @@ -12,6 +13,35 @@ defmodule Pleroma.Web.CommonAPITest do import Pleroma.Factory + test "when replying to a conversation / participation, it only mentions the recipients explicitly declared in the participation" do + har = insert(:user) + jafnhar = insert(:user) + tridi = insert(:user) + + {:ok, activity} = + CommonAPI.post(har, %{ + "status" => "@#{jafnhar.nickname} hey", + "visibility" => "direct" + }) + + assert har.ap_id in activity.recipients + assert jafnhar.ap_id in activity.recipients + + [participation] = Participation.for_user(har) + + {:ok, activity} = + CommonAPI.post(har, %{ + "status" => "I don't really like @#{tridi.nickname}", + "visibility" => "direct", + "in_reply_to_status_id" => activity.id, + "in_reply_to_conversation_id" => participation.id + }) + + assert har.ap_id in activity.recipients + assert jafnhar.ap_id in activity.recipients + refute tridi.ap_id in activity.recipients + end + test "with the safe_dm_mention option set, it does not mention people beyond the initial tags" do har = insert(:user) jafnhar = insert(:user) From d93d7779151c811e991e99098e64c1da2c783d68 Mon Sep 17 00:00:00 2001 From: feld Date: Fri, 2 Aug 2019 17:07:09 +0000 Subject: [PATCH 073/202] 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 eee98aaa738c1aa5f2e4203a61b67648d62965c8 Mon Sep 17 00:00:00 2001 From: lain Date: Fri, 2 Aug 2019 19:53:08 +0200 Subject: [PATCH 074/202] Pleroma API: Add endpoint to get conversation statuses. --- lib/pleroma/web/controller_helper.ex | 76 +++++++++++++++++++ .../mastodon_api/mastodon_api_controller.ex | 68 +---------------- .../web/pleroma_api/pleroma_api_controller.ex | 49 ++++++++++++ lib/pleroma/web/router.ex | 9 +++ test/web/common_api/common_api_utils_test.exs | 16 ++-- .../pleroma_api_controller_test.exs | 45 +++++++++++ 6 files changed, 189 insertions(+), 74 deletions(-) create mode 100644 lib/pleroma/web/pleroma_api/pleroma_api_controller.ex create mode 100644 test/web/pleroma_api/pleroma_api_controller_test.exs diff --git a/lib/pleroma/web/controller_helper.ex b/lib/pleroma/web/controller_helper.ex index 8a753bb4f..eeac9f503 100644 --- a/lib/pleroma/web/controller_helper.ex +++ b/lib/pleroma/web/controller_helper.ex @@ -33,4 +33,80 @@ defp param_to_integer(val, default) when is_binary(val) do end defp param_to_integer(_, default), do: default + + def add_link_headers( + conn, + method, + activities, + param \\ nil, + params \\ %{}, + func3 \\ nil, + func4 \\ nil + ) do + params = + conn.params + |> Map.drop(["since_id", "max_id", "min_id"]) + |> Map.merge(params) + + last = List.last(activities) + + func3 = func3 || (&mastodon_api_url/3) + func4 = func4 || (&mastodon_api_url/4) + + if last do + max_id = last.id + + limit = + params + |> Map.get("limit", "20") + |> String.to_integer() + + min_id = + if length(activities) <= limit do + activities + |> List.first() + |> Map.get(:id) + else + activities + |> Enum.at(limit * -1) + |> Map.get(:id) + end + + {next_url, prev_url} = + if param do + { + func4.( + Pleroma.Web.Endpoint, + method, + param, + Map.merge(params, %{max_id: max_id}) + ), + func4.( + Pleroma.Web.Endpoint, + method, + param, + Map.merge(params, %{min_id: min_id}) + ) + } + else + { + func3.( + Pleroma.Web.Endpoint, + method, + Map.merge(params, %{max_id: max_id}) + ), + func3.( + Pleroma.Web.Endpoint, + method, + Map.merge(params, %{min_id: min_id}) + ) + } + end + + conn + |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"") + else + conn + end + end end diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 174e93468..0deeab2be 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -5,7 +5,8 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do use Pleroma.Web, :controller - import Pleroma.Web.ControllerHelper, only: [json_response: 3] + import Pleroma.Web.ControllerHelper, + only: [json_response: 3, add_link_headers: 5, add_link_headers: 4, add_link_headers: 3] alias Ecto.Changeset alias Pleroma.Activity @@ -342,71 +343,6 @@ def custom_emojis(conn, _params) do json(conn, mastodon_emoji) end - defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do - params = - conn.params - |> Map.drop(["since_id", "max_id", "min_id"]) - |> Map.merge(params) - - last = List.last(activities) - - if last do - max_id = last.id - - limit = - params - |> Map.get("limit", "20") - |> String.to_integer() - - min_id = - if length(activities) <= limit do - activities - |> List.first() - |> Map.get(:id) - else - activities - |> Enum.at(limit * -1) - |> Map.get(:id) - end - - {next_url, prev_url} = - if param do - { - mastodon_api_url( - Pleroma.Web.Endpoint, - method, - param, - Map.merge(params, %{max_id: max_id}) - ), - mastodon_api_url( - Pleroma.Web.Endpoint, - method, - param, - Map.merge(params, %{min_id: min_id}) - ) - } - else - { - mastodon_api_url( - Pleroma.Web.Endpoint, - method, - Map.merge(params, %{max_id: max_id}) - ), - mastodon_api_url( - Pleroma.Web.Endpoint, - method, - Map.merge(params, %{min_id: min_id}) - ) - } - end - - conn - |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"") - else - conn - end - end - def home_timeline(%{assigns: %{user: user}} = conn, params) do params = params diff --git a/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex new file mode 100644 index 000000000..b677892ed --- /dev/null +++ b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex @@ -0,0 +1,49 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do + use Pleroma.Web, :controller + + import Pleroma.Web.ControllerHelper, only: [add_link_headers: 7] + + alias Pleroma.Conversation.Participation + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.MastodonAPI.StatusView + alias Pleroma.Repo + + def conversation_statuses( + %{assigns: %{user: user}} = conn, + %{"id" => participation_id} = params + ) do + params = + params + |> Map.put("blocking_user", user) + |> Map.put("muting_user", user) + |> Map.put("user", user) + + participation = + participation_id + |> Participation.get() + |> Repo.preload(:conversation) + + if user.id == participation.user_id do + activities = + participation.conversation.ap_id + |> ActivityPub.fetch_activities_for_context(params) + |> Enum.reverse() + + conn + |> add_link_headers( + :conversation_statuses, + activities, + participation_id, + params, + nil, + &pleroma_api_url/4 + ) + |> put_view(StatusView) + |> render("index.json", %{activities: activities, for: user, as: :activity}) + end + end +end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 0689d69fb..40298538a 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -257,6 +257,15 @@ defmodule Pleroma.Web.Router do end end + scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do + pipe_through(:authenticated_api) + + scope [] do + pipe_through(:oauth_write) + get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses) + end + end + scope "/api/v1", Pleroma.Web.MastodonAPI do pipe_through(:authenticated_api) diff --git a/test/web/common_api/common_api_utils_test.exs b/test/web/common_api/common_api_utils_test.exs index af320f31f..7510c8def 100644 --- a/test/web/common_api/common_api_utils_test.exs +++ b/test/web/common_api/common_api_utils_test.exs @@ -239,7 +239,7 @@ test "for public posts, not a reply" do mentioned_user = insert(:user) mentions = [mentioned_user.ap_id] - {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "public") + {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "public", nil) assert length(to) == 2 assert length(cc) == 1 @@ -256,7 +256,7 @@ test "for public posts, a reply" do {:ok, activity} = CommonAPI.post(third_user, %{"status" => "uguu"}) mentions = [mentioned_user.ap_id] - {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "public") + {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "public", nil) assert length(to) == 3 assert length(cc) == 1 @@ -272,7 +272,7 @@ test "for unlisted posts, not a reply" do mentioned_user = insert(:user) mentions = [mentioned_user.ap_id] - {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "unlisted") + {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "unlisted", nil) assert length(to) == 2 assert length(cc) == 1 @@ -289,7 +289,7 @@ test "for unlisted posts, a reply" do {:ok, activity} = CommonAPI.post(third_user, %{"status" => "uguu"}) mentions = [mentioned_user.ap_id] - {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "unlisted") + {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "unlisted", nil) assert length(to) == 3 assert length(cc) == 1 @@ -305,7 +305,7 @@ test "for private posts, not a reply" do mentioned_user = insert(:user) mentions = [mentioned_user.ap_id] - {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "private") + {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "private", nil) assert length(to) == 2 assert length(cc) == 0 @@ -321,7 +321,7 @@ test "for private posts, a reply" do {:ok, activity} = CommonAPI.post(third_user, %{"status" => "uguu"}) mentions = [mentioned_user.ap_id] - {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "private") + {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "private", nil) assert length(to) == 3 assert length(cc) == 0 @@ -336,7 +336,7 @@ test "for direct posts, not a reply" do mentioned_user = insert(:user) mentions = [mentioned_user.ap_id] - {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "direct") + {to, cc} = Utils.get_to_and_cc(user, mentions, nil, "direct", nil) assert length(to) == 1 assert length(cc) == 0 @@ -351,7 +351,7 @@ test "for direct posts, a reply" do {:ok, activity} = CommonAPI.post(third_user, %{"status" => "uguu"}) mentions = [mentioned_user.ap_id] - {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "direct") + {to, cc} = Utils.get_to_and_cc(user, mentions, activity, "direct", nil) assert length(to) == 2 assert length(cc) == 0 diff --git a/test/web/pleroma_api/pleroma_api_controller_test.exs b/test/web/pleroma_api/pleroma_api_controller_test.exs new file mode 100644 index 000000000..43104e36e --- /dev/null +++ b/test/web/pleroma_api/pleroma_api_controller_test.exs @@ -0,0 +1,45 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do + use Pleroma.Web.ConnCase + + alias Pleroma.Conversation.Participation + alias Pleroma.Web.CommonAPI + + import Pleroma.Factory + + test "/api/v1/pleroma/conversations/:id/statuses", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + third_user = insert(:user) + + {:ok, _activity} = + CommonAPI.post(user, %{"status" => "Hi @#{third_user.nickname}!", "visibility" => "direct"}) + + {:ok, activity} = + CommonAPI.post(user, %{"status" => "Hi @#{other_user.nickname}!", "visibility" => "direct"}) + + [participation] = Participation.for_user(other_user) + + {:ok, activity_two} = + CommonAPI.post(other_user, %{ + "status" => "Hi!", + "in_reply_to_status_id" => activity.id, + "in_reply_to_conversation_id" => participation.id + }) + + result = + conn + |> assign(:user, other_user) + |> get("/api/v1/pleroma/conversations/#{participation.id}/statuses") + |> json_response(200) + + assert length(result) == 2 + + id_one = activity.id + id_two = activity_two.id + assert [%{"id" => ^id_one}, %{"id" => ^id_two}] = result + 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 075/202] 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 076/202] 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 077/202] 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 078/202] 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 079/202] 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 080/202] 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 081/202] 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 082/202] 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 083/202] 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 084/202] 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 085/202] 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 086/202] 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 087/202] 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 088/202] 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 089/202] 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 090/202] 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 091/202] Remove longfox emoji set --- CHANGELOG.md | 3 +++ config/emoji.txt | 28 ---------------------------- priv/static/emoji/f_00b.png | Bin 371 -> 0 bytes priv/static/emoji/f_00b11b.png | Bin 661 -> 0 bytes priv/static/emoji/f_00b33b.png | Bin 662 -> 0 bytes priv/static/emoji/f_00h.png | Bin 7522 -> 0 bytes priv/static/emoji/f_00t.png | Bin 541 -> 0 bytes priv/static/emoji/f_01b.png | Bin 4510 -> 0 bytes priv/static/emoji/f_03b.png | Bin 2872 -> 0 bytes priv/static/emoji/f_10b.png | Bin 2849 -> 0 bytes priv/static/emoji/f_11b.png | Bin 447 -> 0 bytes priv/static/emoji/f_11b00b.png | Bin 615 -> 0 bytes priv/static/emoji/f_11b22b.png | Bin 618 -> 0 bytes priv/static/emoji/f_11h.png | Bin 7314 -> 0 bytes priv/static/emoji/f_11t.png | Bin 559 -> 0 bytes priv/static/emoji/f_12b.png | Bin 4352 -> 0 bytes priv/static/emoji/f_21b.png | Bin 2900 -> 0 bytes priv/static/emoji/f_22b.png | Bin 386 -> 0 bytes priv/static/emoji/f_22b11b.png | Bin 666 -> 0 bytes priv/static/emoji/f_22b33b.png | Bin 663 -> 0 bytes priv/static/emoji/f_22h.png | Bin 7448 -> 0 bytes priv/static/emoji/f_22t.png | Bin 549 -> 0 bytes priv/static/emoji/f_23b.png | Bin 4334 -> 0 bytes priv/static/emoji/f_30b.png | Bin 4379 -> 0 bytes priv/static/emoji/f_32b.png | Bin 2921 -> 0 bytes priv/static/emoji/f_33b.png | Bin 459 -> 0 bytes priv/static/emoji/f_33b00b.png | Bin 611 -> 0 bytes priv/static/emoji/f_33b22b.png | Bin 623 -> 0 bytes priv/static/emoji/f_33h.png | Bin 7246 -> 0 bytes priv/static/emoji/f_33t.png | Bin 563 -> 0 bytes 30 files changed, 3 insertions(+), 28 deletions(-) delete mode 100644 priv/static/emoji/f_00b.png delete mode 100644 priv/static/emoji/f_00b11b.png delete mode 100644 priv/static/emoji/f_00b33b.png delete mode 100644 priv/static/emoji/f_00h.png delete mode 100644 priv/static/emoji/f_00t.png delete mode 100644 priv/static/emoji/f_01b.png delete mode 100644 priv/static/emoji/f_03b.png delete mode 100644 priv/static/emoji/f_10b.png delete mode 100644 priv/static/emoji/f_11b.png delete mode 100644 priv/static/emoji/f_11b00b.png delete mode 100644 priv/static/emoji/f_11b22b.png delete mode 100644 priv/static/emoji/f_11h.png delete mode 100644 priv/static/emoji/f_11t.png delete mode 100644 priv/static/emoji/f_12b.png delete mode 100644 priv/static/emoji/f_21b.png delete mode 100644 priv/static/emoji/f_22b.png delete mode 100644 priv/static/emoji/f_22b11b.png delete mode 100644 priv/static/emoji/f_22b33b.png delete mode 100644 priv/static/emoji/f_22h.png delete mode 100644 priv/static/emoji/f_22t.png delete mode 100644 priv/static/emoji/f_23b.png delete mode 100644 priv/static/emoji/f_30b.png delete mode 100644 priv/static/emoji/f_32b.png delete mode 100644 priv/static/emoji/f_33b.png delete mode 100644 priv/static/emoji/f_33b00b.png delete mode 100644 priv/static/emoji/f_33b22b.png delete mode 100644 priv/static/emoji/f_33h.png delete mode 100644 priv/static/emoji/f_33t.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 4fa9ffd9b..fc4d08aaf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - RichMedia: parsers and their order are configured in `rich_media` config. - RichMedia: add the rich media ttl based on image expiration time. +### Removed +- Emoji: Remove longfox emojis. + ## [1.0.1] - 2019-07-14 ### Security - OStatus: fix an object spoofing vulnerability. diff --git a/config/emoji.txt b/config/emoji.txt index 79246f239..200768ad1 100644 --- a/config/emoji.txt +++ b/config/emoji.txt @@ -1,30 +1,2 @@ firefox, /emoji/Firefox.gif, Gif,Fun blank, /emoji/blank.png, Fun -f_00b, /emoji/f_00b.png -f_00b11b, /emoji/f_00b11b.png -f_00b33b, /emoji/f_00b33b.png -f_00h, /emoji/f_00h.png -f_00t, /emoji/f_00t.png -f_01b, /emoji/f_01b.png -f_03b, /emoji/f_03b.png -f_10b, /emoji/f_10b.png -f_11b, /emoji/f_11b.png -f_11b00b, /emoji/f_11b00b.png -f_11b22b, /emoji/f_11b22b.png -f_11h, /emoji/f_11h.png -f_11t, /emoji/f_11t.png -f_12b, /emoji/f_12b.png -f_21b, /emoji/f_21b.png -f_22b, /emoji/f_22b.png -f_22b11b, /emoji/f_22b11b.png -f_22b33b, /emoji/f_22b33b.png -f_22h, /emoji/f_22h.png -f_22t, /emoji/f_22t.png -f_23b, /emoji/f_23b.png -f_30b, /emoji/f_30b.png -f_32b, /emoji/f_32b.png -f_33b, /emoji/f_33b.png -f_33b00b, /emoji/f_33b00b.png -f_33b22b, /emoji/f_33b22b.png -f_33h, /emoji/f_33h.png -f_33t, /emoji/f_33t.png diff --git a/priv/static/emoji/f_00b.png b/priv/static/emoji/f_00b.png deleted file mode 100644 index 3d00b89b02acbcf8cd3b4ff388e2f09f06c0aee1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 371 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H1|$#LC7xzrV6^gdaSW+oe0!bI&BamX_{aZe zX6tr}a(3}uiBn;ST_nxo^!x?ST;&xR;T&I=Y5KF@5L*AU=Eu*^3^kJaKZ=3oU;`6w zt-JN@_OS)(J9FoTt==ppyU6XFv-Q@zto0Qdv!5@2A)YF8{Drt>8YiOy14{#g00WZ) z0|x^mE>6Ry;sTMsi)`-2?%!(d_nO`LcJru6{1-oD!Mef_(Y8l8#ae0+Qj4GnKrsQ0BxFf%h37Z<-?u0A16 z0%&7iUS4i)Zd_bkLPEluhbQMeKD+2luf*|Y=HtyQr@90$%rZJNQStnAoeQ&!pRKZZvPkXO61C?m4Odqy zFDsK-P$bR6!;_w#o}ZszQBjeeo_cp*b52f9R%XV>_irB@={wym`gWb=lx$fcA))H( z>TJ8SnLw8@mIV0)GdMiEkp^U2d%8G=R4~3d7hKfBD8TSA^7h8u+uKV2Z+>FA`PXu_ zd+(S_YLj$D`dpGryVWPH+_lvB=A0d;+m$yu-R)Xhv@|bw<~P2=>o+AXT)oS2;W9VS zcr0Ko?~B*+5;?gCE_^ugqMzy5F?F@2t5&|04CXuV=FXp{UB3FKE`93KR8MeK7fkkZ za8X|!812_^Z|$k5gVTfq0*=PK`$wDhZOlBy8L)kA?#-Kbji0itxczRQ zjLfYfrZ4Y2`n0#rY3R~oMbTJPly#cZB6#lUqf2$wA9$%>7X~W&+3(A9;>FoTn(7Xv z)#}&gFch*wHJ!L`&SEn~no&*e=-n^DPb$~ueVl&%@vgml^-XkS4%;yBPFPsT{N%@E OkaAB~KbLh*2~7Y>h#0s4 diff --git a/priv/static/emoji/f_00b33b.png b/priv/static/emoji/f_00b33b.png deleted file mode 100644 index 8f4929297e401d8fc0b150c0080b989d974e00db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 662 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 diff --git a/priv/static/emoji/f_00t.png b/priv/static/emoji/f_00t.png deleted file mode 100644 index 31d98b4333b253da7c08b7925928d3750b816125..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 541 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 diff --git a/priv/static/emoji/f_03b.png b/priv/static/emoji/f_03b.png deleted file mode 100644 index 9e4ff1bf77074361c97d1ef8fcdc205575d03916..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2872 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 Wnd|l2N8F|W00003RzX=`6xmTY)Ni*_) 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 diff --git a/priv/static/emoji/f_11b00b.png b/priv/static/emoji/f_11b00b.png deleted file mode 100644 index c4c30e11f5c16d81c05217d2a109d27d89a73e4d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 615 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD~={fsrG?C&U%V&CSjA_4WDx|No~C zZ$G?y^X>2dC+C)&>6N%J%jm)^<7cZZR#z+Y@bDBD7stiLd3$?)eE;VC+gD%z{(pFK z_VH%sGZPihOjLTZNNrh}4A8Xn^z?*;1aEJzho|NqZ)VBN%#4bP3J3@Y3=GUjPfbrx z&C1O9@b2};_iyhX>b|?b<=+0*yZf3S9O*mR!FQ~Y;aDT%>2A^U({b4j%g!Y6&P4IPREY^`68&isQ?g~Hq@=jHxp{ec`S|#lnVA_G8HI#| z+S=M`YisN4>l+#x^78Tu3JP*^a;mGVpDj^aP$d2E*?YcQo;DgTe~DWM4f2cZE% diff --git a/priv/static/emoji/f_11b22b.png b/priv/static/emoji/f_11b22b.png deleted file mode 100644 index 47425e06e2713254552d9081069ed7553f6181e6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 618 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD~={fsrG?C&U%V&CJa7_V)V!|No~C zZ$G?y{q67n$7dIv>JmIZUFZCC-RCO}7ZgeJ@$pquRK&%_eSH7s{o7Yx|Neh?a?bH) z<}(u&&rDQ$wnPnRPJVuVLPEmBQ*)0uvoJC;78De;wYAmO*4Ee8H#9U97Z>N{<>ln$ zR99C^Nl9^YbMx}@GBYy^2?=#3iFYQ7_oYfqNR#MKlbDh%`);lA+m#ycR%*Oiq5gWg z`kUqIZ`WB~m}PvjgYQ@)!?8xj)7_%?4|U(&-*Rt%>)n0L503PGc=zVx`?neCsp;vd zS(zD8QBeT_0fB*mPZp^yE0gi|_I!A9c6xexZf>rxug{ZnOV0F4T$p9_Y?a08YGocC z9wpI7o^R1%uzC3;cb(>KnV&Jc({El|VyP>`{b5yL zO8V3$P^`g0LrJ3cx_w@nA_>(OH8!n#mAreMhzwj7#8|Q`IOUtq#--7&Y0GD>-SNwP z^@hjd@)60E_vc^p2#?$U?T!G7dIaZB{ef?OJN4|RypB6OiEH=9U17<#ORk-l%6wY3 aHu3)RS+gW`A6wr7srPjCb6Mw<&;$S)^#2(E diff --git a/priv/static/emoji/f_11h.png b/priv/static/emoji/f_11h.png deleted file mode 100644 index 28342363a20b6dbdeb34a2fc4d30a9def0c3201f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7314 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 diff --git a/priv/static/emoji/f_11t.png b/priv/static/emoji/f_11t.png deleted file mode 100644 index dca67dc70ac3d8259ab7a62acea3d9298751abb6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 559 zcmeAS@N?(olHy`uVBq!ia0vp^4Is?H3?#oinD`S&H3s;ExB}^fgamJIuWx_AKHX8uIb+Ra8`*4}EtCXeDDwkY6x^!?PP{Kz59$i(^Oy zgGn3nCI%jj7yPUO32_(CH#Bj(JZw-%5Ln=%AT^s8Xk(I5gQeJo9tQoF`i2VJ0S#UM zf|$>2Y)E6NaID|S_^kUKKf4LjgN8~@4guS^NTAvRrvotzix^&hXT1qM2Ur>mdKI;Vst0M7f_6#xJL diff --git a/priv/static/emoji/f_12b.png b/priv/static/emoji/f_12b.png deleted file mode 100644 index 9925adb7cf6dc3af9218a7e2f258840c4df7b423..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4352 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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 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 WbP@pQG7Dn>0000z?{ diff --git a/priv/static/emoji/f_22t.png b/priv/static/emoji/f_22t.png deleted file mode 100644 index addd9fec78839c418422e4a56a402ee003dcd8c5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 549 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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 092/202] 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 093/202] 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 094/202] 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 bbd9ed02576f1599e90f8575573fe6e935d32eae Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Mon, 5 Aug 2019 15:33:34 +0700 Subject: [PATCH 095/202] Update CHANGELOG --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd64b2259..e9d4e1710 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Added synchronization of following/followers counters for external users - Configuration: `enabled` option for `Pleroma.Emails.Mailer`, defaulting to `false`. - Configuration: Pleroma.Plugs.RateLimiter `bucket_name`, `params` options. +- Configuration: `user_bio_length` and `user_name_length` options. - Addressable lists - Twitter API: added rate limit for `/api/account/password_reset` endpoint. - ActivityPub: Add an internal service actor for fetching ActivityPub objects. From 3af6d14da769aa5adfdd6360b43c691fd8c8eed5 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 5 Aug 2019 15:09:19 +0200 Subject: [PATCH 096/202] Pleroma Conversations API: Add a way to set recipients. --- lib/pleroma/conversation/participation.ex | 20 ++++++++++ .../mastodon_api/views/conversation_view.ex | 13 +++++- .../web/pleroma_api/pleroma_api_controller.ex | 17 ++++++++ lib/pleroma/web/router.ex | 1 + .../mastodon_api/conversation_view_test.exs | 40 +++++++++++++++++++ .../pleroma_api_controller_test.exs | 31 ++++++++++++++ 6 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 test/web/mastodon_api/conversation_view_test.exs diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex index f1e1a6958..acdee5517 100644 --- a/lib/pleroma/conversation/participation.ex +++ b/lib/pleroma/conversation/participation.ex @@ -99,4 +99,24 @@ def get(nil), do: nil def get(id) do Repo.get(__MODULE__, id) end + + def set_recipients(participation, user_ids) do + Repo.transaction(fn -> + query = + from(r in RecipientShip, + where: r.participation_id == ^participation.id + ) + + Repo.delete_all(query) + + users = + from(u in User, + where: u.id in ^user_ids + ) + |> Repo.all() + + RecipientShip.create(users, participation) + :ok + end) + end end diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex index 38bdec737..5adaecdb0 100644 --- a/lib/pleroma/web/mastodon_api/views/conversation_view.ex +++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex @@ -12,7 +12,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do alias Pleroma.Web.MastodonAPI.StatusView def render("participation.json", %{participation: participation, user: user}) do - participation = Repo.preload(participation, conversation: :users) + participation = Repo.preload(participation, conversation: :users, recipients: []) last_activity_id = with nil <- participation.last_activity_id do @@ -37,11 +37,20 @@ def render("participation.json", %{participation: participation, user: user}) do as: :user }) + recipients = + AccountView.render("accounts.json", %{ + users: participation.recipients, + as: :user + }) + %{ id: participation.id |> to_string(), accounts: accounts, unread: !participation.read, - last_status: last_status + last_status: last_status, + pleroma: %{ + recipients: recipients + } } end end diff --git a/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex index b677892ed..759d8aef0 100644 --- a/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex @@ -10,6 +10,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do alias Pleroma.Conversation.Participation alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.MastodonAPI.StatusView + alias Pleroma.Web.MastodonAPI.ConversationView alias Pleroma.Repo def conversation_statuses( @@ -46,4 +47,20 @@ def conversation_statuses( |> render("index.json", %{activities: activities, for: user, as: :activity}) end end + + def update_conversation( + %{assigns: %{user: user}} = conn, + %{"id" => participation_id, "recipients" => recipients} + ) do + participation = + participation_id + |> Participation.get() + + with true <- user.id == participation.user_id, + {:ok, _} <- Participation.set_recipients(participation, recipients) do + conn + |> put_view(ConversationView) + |> render("participation.json", %{participation: participation, user: user}) + end + end end diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 40298538a..6cdef7e2f 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -263,6 +263,7 @@ defmodule Pleroma.Web.Router do scope [] do pipe_through(:oauth_write) get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses) + patch("/conversations/:id", PleromaAPIController, :update_conversation) end end diff --git a/test/web/mastodon_api/conversation_view_test.exs b/test/web/mastodon_api/conversation_view_test.exs new file mode 100644 index 000000000..2a4b41fa4 --- /dev/null +++ b/test/web/mastodon_api/conversation_view_test.exs @@ -0,0 +1,40 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Web.MastodonAPI.ConversationViewTest do + use Pleroma.DataCase + + alias Pleroma.Web.CommonAPI + alias Pleroma.Conversation.Participation + alias Pleroma.Web.MastodonAPI.ConversationView + + import Pleroma.Factory + + test "represents a Mastodon Conversation entity" do + user = insert(:user) + other_user = insert(:user) + + {:ok, activity} = + CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}", "visibility" => "direct"}) + + [participation] = Participation.for_user_with_last_activity_id(user) + + assert participation + + conversation = + ConversationView.render("participation.json", %{participation: participation, user: user}) + + assert conversation.id == participation.id |> to_string() + assert conversation.last_status.id == activity.id + + assert [account] = conversation.accounts + assert account.id == other_user.id + + assert recipients = conversation.pleroma.recipients + recipient_ids = recipients |> Enum.map(& &1.id) + + assert user.id in recipient_ids + assert other_user.id in recipient_ids + end +end diff --git a/test/web/pleroma_api/pleroma_api_controller_test.exs b/test/web/pleroma_api/pleroma_api_controller_test.exs index 43104e36e..7989defe0 100644 --- a/test/web/pleroma_api/pleroma_api_controller_test.exs +++ b/test/web/pleroma_api/pleroma_api_controller_test.exs @@ -7,6 +7,7 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do alias Pleroma.Conversation.Participation alias Pleroma.Web.CommonAPI + alias Pleroma.Repo import Pleroma.Factory @@ -42,4 +43,34 @@ test "/api/v1/pleroma/conversations/:id/statuses", %{conn: conn} do id_two = activity_two.id assert [%{"id" => ^id_one}, %{"id" => ^id_two}] = result end + + test "PATCH /api/v1/pleroma/conversations/:id", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, _activity} = CommonAPI.post(user, %{"status" => "Hi", "visibility" => "direct"}) + + [participation] = Participation.for_user(user) + + participation = Repo.preload(participation, :recipients) + + assert [user] == participation.recipients + assert other_user not in participation.recipients + + result = + conn + |> assign(:user, user) + |> patch("/api/v1/pleroma/conversations/#{participation.id}", %{ + "recipients" => [user.id, other_user.id] + }) + |> json_response(200) + + assert result["id"] == participation.id |> to_string + + assert recipients = result["pleroma"]["recipients"] + recipient_ids = Enum.map(recipients, & &1["id"]) + + assert user.id in recipient_ids + assert other_user.id in recipient_ids + end end From b64b6fee2a78fbfbc557b89550128494ca7d2894 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 5 Aug 2019 15:33:22 +0200 Subject: [PATCH 097/202] CommonAPI: Replies to conversations also get the correct context id. --- lib/pleroma/web/common_api/common_api.ex | 2 +- lib/pleroma/web/common_api/utils.ex | 8 ++++++-- test/web/common_api/common_api_test.exs | 15 +++++++++++++++ 3 files changed, 22 insertions(+), 3 deletions(-) diff --git a/lib/pleroma/web/common_api/common_api.ex b/lib/pleroma/web/common_api/common_api.ex index 86e95cd0f..72da46263 100644 --- a/lib/pleroma/web/common_api/common_api.ex +++ b/lib/pleroma/web/common_api/common_api.ex @@ -223,7 +223,7 @@ def post(user, %{"status" => status} = data) do {poll, poll_emoji} <- make_poll_data(data), {to, cc} <- get_to_and_cc(user, addressed_users, in_reply_to, visibility, in_reply_to_conversation), - context <- make_context(in_reply_to), + context <- make_context(in_reply_to, in_reply_to_conversation), cw <- data["spoiler_text"] || "", sensitive <- data["sensitive"] || Enum.member?(tags, {"#nsfw", "nsfw"}), full_payload <- String.trim(status <> cw), diff --git a/lib/pleroma/web/common_api/utils.ex b/lib/pleroma/web/common_api/utils.ex index e70ba7d43..425b6d656 100644 --- a/lib/pleroma/web/common_api/utils.ex +++ b/lib/pleroma/web/common_api/utils.ex @@ -244,8 +244,12 @@ defp maybe_add_nsfw_tag({text, mentions, tags}, %{"sensitive" => sensitive}) defp maybe_add_nsfw_tag(data, _), do: data - def make_context(%Activity{data: %{"context" => context}}), do: context - def make_context(_), do: Utils.generate_context_id() + def make_context(_, %Participation{} = participation) do + Repo.preload(participation, :conversation).conversation.ap_id + end + + def make_context(%Activity{data: %{"context" => context}}, _), do: context + def make_context(_, _), do: Utils.generate_context_id() def maybe_add_attachments(parsed, _attachments, true = _no_links), do: parsed diff --git a/test/web/common_api/common_api_test.exs b/test/web/common_api/common_api_test.exs index e2a5bf117..454523349 100644 --- a/test/web/common_api/common_api_test.exs +++ b/test/web/common_api/common_api_test.exs @@ -9,10 +9,25 @@ defmodule Pleroma.Web.CommonAPITest do alias Pleroma.Object alias Pleroma.User alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.CommonAPI import Pleroma.Factory + test "when replying to a conversation / participation, it will set the correct context id even if no explicit reply_to is given" do + user = insert(:user) + {:ok, activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"}) + + [participation] = Participation.for_user(user) + + {:ok, convo_reply} = + CommonAPI.post(user, %{"status" => ".", "in_reply_to_conversation_id" => participation.id}) + + assert Visibility.is_direct?(convo_reply) + + assert activity.data["context"] == convo_reply.data["context"] + end + test "when replying to a conversation / participation, it only mentions the recipients explicitly declared in the participation" do har = insert(:user) jafnhar = insert(:user) From ddabe7784b47939120a838b9c65381fd2262c161 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 5 Aug 2019 15:58:14 +0200 Subject: [PATCH 098/202] Pleroma Conversations: Document differences. --- docs/api/differences_in_mastoapi_responses.md | 8 +++++ docs/api/pleroma_api.md | 29 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/docs/api/differences_in_mastoapi_responses.md b/docs/api/differences_in_mastoapi_responses.md index 1907d70c8..79ca531b8 100644 --- a/docs/api/differences_in_mastoapi_responses.md +++ b/docs/api/differences_in_mastoapi_responses.md @@ -59,12 +59,19 @@ Has these additional fields under the `pleroma` object: - `show_role`: boolean, nullable, true when the user wants his role (e.g admin, moderator) to be shown - `no_rich_text` - boolean, nullable, true when html tags are stripped from all statuses requested from the API +## Conversations + +Has an additional field under the `pleroma` object: + +- `recipients`: The list of the recipients of this Conversation. These will be addressed when replying to this conversation. + ## Account Search Behavior has changed: - `/api/v1/accounts/search`: Does not require authentication + ## Notifications Has these additional fields under the `pleroma` object: @@ -79,6 +86,7 @@ Additional parameters can be added to the JSON body/Form data: - `content_type`: string, contain the MIME type of the status, it is transformed into HTML by the backend. You can get the list of the supported MIME types with the nodeinfo endpoint. - `to`: A list of nicknames (like `lain@soykaf.club` or `lain` on the local server) that will be used to determine who is going to be addressed by this post. Using this will disable the implicit addressing by mentioned names in the `status` body, only the people in the `to` list will be addressed. The normal rules for for post visibility are not affected by this and will still apply. - `visibility`: string, besides standard MastoAPI values (`direct`, `private`, `unlisted` or `public`) it can be used to address a List by setting it to `list:LIST_ID`. +- `in_reply_to_conversation_id`: Will reply to a given conversation, addressing only the people who are part of the recipient set of that conversation. Sets the visibility to `direct`. ## PATCH `/api/v1/update_credentials` diff --git a/docs/api/pleroma_api.md b/docs/api/pleroma_api.md index 5698e88ac..4323e59f0 100644 --- a/docs/api/pleroma_api.md +++ b/docs/api/pleroma_api.md @@ -319,3 +319,32 @@ See [Admin-API](Admin-API.md) "healthy": true # Instance state } ``` + +# Pleroma Conversations + +Pleroma Conversations have the same general structure that Mastodon Conversations have. The behavior differs in the following ways when using these endpoints: + +1. Pleroma Conversations never add or remove recipients, unless explicitly changed by the user. +2. Pleroma Conversations statuses can be requested by Conversation id. +3. Pleroma Conversations can be replied to. + +Conversations have the additional field "recipients" under the "pleroma" key. This holds a list of all the accounts that will receive a message in this conversation. + +The status posting endpoint takes an additional parameter, `in_reply_to_conversation_id`, which, when set, will set the visiblity to direct and address only the people who are the recipients of that Conversation. + + +## `GET /api/v1/pleroma/conversations/:id/statuses` +### Timeline for a given conversation +* Method `GET` +* Authentication: required +* Params: Like other timelines +* Response: JSON, statuses (200 - healthy, 503 unhealthy). + + +## `PATCH /api/v1/pleroma/conversations/:id` +### Update a conversation. Used to change the set of recipients. +* Method `PATCH` +* Authentication: required +* Params: + * `recipients`: A list of ids of users that should receive posts to this conversation. +* Response: JSON, statuses (200 - healthy, 503 unhealthy) From d6fe220e32d581220cc33f4f44d6517a401eabbf Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 5 Aug 2019 16:11:23 +0200 Subject: [PATCH 099/202] Linting. --- lib/pleroma/conversation/participation_recipient_ship.ex | 2 +- lib/pleroma/web/pleroma_api/pleroma_api_controller.ex | 6 +++--- test/web/mastodon_api/conversation_view_test.exs | 2 +- test/web/pleroma_api/pleroma_api_controller_test.exs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/pleroma/conversation/participation_recipient_ship.ex b/lib/pleroma/conversation/participation_recipient_ship.ex index 27c0c89cd..932cbd04c 100644 --- a/lib/pleroma/conversation/participation_recipient_ship.ex +++ b/lib/pleroma/conversation/participation_recipient_ship.ex @@ -6,8 +6,8 @@ defmodule Pleroma.Conversation.Participation.RecipientShip do use Ecto.Schema alias Pleroma.Conversation.Participation - alias Pleroma.User alias Pleroma.Repo + alias Pleroma.User import Ecto.Changeset diff --git a/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex index 759d8aef0..018564452 100644 --- a/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex @@ -8,10 +8,10 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do import Pleroma.Web.ControllerHelper, only: [add_link_headers: 7] alias Pleroma.Conversation.Participation - alias Pleroma.Web.ActivityPub.ActivityPub - alias Pleroma.Web.MastodonAPI.StatusView - alias Pleroma.Web.MastodonAPI.ConversationView alias Pleroma.Repo + alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.MastodonAPI.ConversationView + alias Pleroma.Web.MastodonAPI.StatusView def conversation_statuses( %{assigns: %{user: user}} = conn, diff --git a/test/web/mastodon_api/conversation_view_test.exs b/test/web/mastodon_api/conversation_view_test.exs index 2a4b41fa4..e32cde5a8 100644 --- a/test/web/mastodon_api/conversation_view_test.exs +++ b/test/web/mastodon_api/conversation_view_test.exs @@ -5,8 +5,8 @@ defmodule Pleroma.Web.MastodonAPI.ConversationViewTest do use Pleroma.DataCase - alias Pleroma.Web.CommonAPI alias Pleroma.Conversation.Participation + alias Pleroma.Web.CommonAPI alias Pleroma.Web.MastodonAPI.ConversationView import Pleroma.Factory diff --git a/test/web/pleroma_api/pleroma_api_controller_test.exs b/test/web/pleroma_api/pleroma_api_controller_test.exs index 7989defe0..7c75fb229 100644 --- a/test/web/pleroma_api/pleroma_api_controller_test.exs +++ b/test/web/pleroma_api/pleroma_api_controller_test.exs @@ -6,8 +6,8 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do use Pleroma.Web.ConnCase alias Pleroma.Conversation.Participation - alias Pleroma.Web.CommonAPI alias Pleroma.Repo + alias Pleroma.Web.CommonAPI import Pleroma.Factory From bdc9a7222cca9988a238cbe76d0e51125a016f8d Mon Sep 17 00:00:00 2001 From: Maksim Date: Mon, 5 Aug 2019 15:37:05 +0000 Subject: [PATCH 100/202] 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 a49c92f6ae2dc68a80345cff4793820a75835eb1 Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 6 Aug 2019 14:51:17 +0200 Subject: [PATCH 101/202] Participation: Setting recipients will always add the owner. --- docs/api/pleroma_api.md | 2 +- lib/pleroma/conversation/participation.ex | 6 ++++++ test/conversation/participation_test.exs | 19 +++++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/docs/api/pleroma_api.md b/docs/api/pleroma_api.md index 4323e59f0..590f2a3fb 100644 --- a/docs/api/pleroma_api.md +++ b/docs/api/pleroma_api.md @@ -346,5 +346,5 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa * Method `PATCH` * Authentication: required * Params: - * `recipients`: A list of ids of users that should receive posts to this conversation. + * `recipients`: A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though. * Response: JSON, statuses (200 - healthy, 503 unhealthy) diff --git a/lib/pleroma/conversation/participation.ex b/lib/pleroma/conversation/participation.ex index acdee5517..d17b6f7c5 100644 --- a/lib/pleroma/conversation/participation.ex +++ b/lib/pleroma/conversation/participation.ex @@ -101,6 +101,10 @@ def get(id) do end def set_recipients(participation, user_ids) do + user_ids = + [participation.user_id | user_ids] + |> Enum.uniq() + Repo.transaction(fn -> query = from(r in RecipientShip, @@ -118,5 +122,7 @@ def set_recipients(participation, user_ids) do RecipientShip.create(users, participation) :ok end) + + {:ok, Repo.preload(participation, :recipients, force: true)} end end diff --git a/test/conversation/participation_test.exs b/test/conversation/participation_test.exs index 4a3c397bd..7958e8e89 100644 --- a/test/conversation/participation_test.exs +++ b/test/conversation/participation_test.exs @@ -132,4 +132,23 @@ test "Doesn't die when the conversation gets empty" do [] = Participation.for_user_with_last_activity_id(user) end + + test "it sets recipients, always keeping the owner of the participation even when not explicitly set" do + user = insert(:user) + other_user = insert(:user) + + {:ok, _activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"}) + [participation] = Participation.for_user_with_last_activity_id(user) + + participation = Repo.preload(participation, :recipients) + + assert participation.recipients |> length() == 1 + assert user in participation.recipients + + {:ok, participation} = Participation.set_recipients(participation, [other_user.id]) + + assert participation.recipients |> length() == 2 + assert user in participation.recipients + assert other_user in participation.recipients + end end From e4a01d253ef7ab09d028198e5e39b9aba357486c Mon Sep 17 00:00:00 2001 From: lain Date: Tue, 6 Aug 2019 15:06:19 +0200 Subject: [PATCH 102/202] Conversation: Rename function to better express what it does. --- lib/pleroma/conversation.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pleroma/conversation.ex b/lib/pleroma/conversation.ex index fb0dfedca..be5821ad7 100644 --- a/lib/pleroma/conversation.ex +++ b/lib/pleroma/conversation.ex @@ -40,7 +40,7 @@ def get_for_ap_id(ap_id) do Repo.get_by(__MODULE__, ap_id: ap_id) end - def maybe_set_recipients(participation, activity) do + def maybe_create_recipientships(participation, activity) do participation = Repo.preload(participation, :recipients) if participation.recipients |> Enum.empty?() do @@ -70,7 +70,7 @@ def create_or_bump_for(activity, opts \\ []) do {:ok, participation} = Participation.create_for_user_and_conversation(user, conversation, opts) - maybe_set_recipients(participation, activity) + maybe_create_recipientships(participation, activity) participation end) From 139b196bc0328d812adb9434b2da97265d57257d Mon Sep 17 00:00:00 2001 From: Maksim Date: Tue, 6 Aug 2019 20:19:28 +0000 Subject: [PATCH 103/202] [#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 104/202] 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 105/202] 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 106/202] 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 107/202] 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 108/202] 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 109/202] 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 110/202] 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 111/202] 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, From 409bcad54b5de631536761952faed05ad5fe3b99 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Fri, 9 Aug 2019 16:49:09 +0300 Subject: [PATCH 112/202] Mastodon API: Set follower/following counters to 0 when hiding followers/following is enabled We are already doing that in AP representation, so I think we should do it here as well for consistency. --- CHANGELOG.md | 1 + .../web/mastodon_api/views/account_view.ex | 11 ++++++-- test/web/mastodon_api/account_view_test.exs | 27 +++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dccc36965..5d08fe757 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Federation/MediaProxy not working with instances that have wrong certificate order - Mastodon API: Handling of search timeouts (`/api/v1/search` and `/api/v2/search`) - Mastodon API: Embedded relationships not being properly rendered in the Account entity of Status entity +- Mastodon API: follower/following counters not being nullified, when `hide_follows`/`hide_followers` is set - Mastodon API: Add `account_id`, `type`, `offset`, and `limit` to search API (`/api/v1/search` and `/api/v2/search`) - Mastodon API, streaming: Fix filtering of notifications based on blocks/mutes/thread mutes - ActivityPub C2S: follower/following collection pages being inaccessible even when authentifucated if `hide_followers`/ `hide_follows` was set diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index de084fd6e..72c092f25 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -72,6 +72,13 @@ defp do_render("account.json", %{user: user} = opts) do image = User.avatar_url(user) |> MediaProxy.url() header = User.banner_url(user) |> MediaProxy.url() user_info = User.get_cached_user_info(user) + + following_count = + ((!user.info.hide_follows or opts[:for] == user) && user_info.following_count) || 0 + + followers_count = + ((!user.info.hide_followers or opts[:for] == user) && user_info.follower_count) || 0 + bot = (user.info.source_data["type"] || "Person") in ["Application", "Service"] emojis = @@ -102,8 +109,8 @@ defp do_render("account.json", %{user: user} = opts) do display_name: display_name, locked: user_info.locked, created_at: Utils.to_masto_date(user.inserted_at), - followers_count: user_info.follower_count, - following_count: user_info.following_count, + followers_count: followers_count, + following_count: following_count, statuses_count: user_info.note_count, note: bio || "", url: User.profile_url(user), diff --git a/test/web/mastodon_api/account_view_test.exs b/test/web/mastodon_api/account_view_test.exs index 905e9af98..a26f514a5 100644 --- a/test/web/mastodon_api/account_view_test.exs +++ b/test/web/mastodon_api/account_view_test.exs @@ -356,4 +356,31 @@ test "sanitizes display names" do result = AccountView.render("account.json", %{user: user}) refute result.display_name == " username " end + + describe "hiding follows/following" do + test "shows when follows/following are hidden and sets follower/following count to 0" do + user = insert(:user, info: %{hide_followers: true, hide_follows: true}) + other_user = insert(:user) + {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user) + {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) + + assert %{ + followers_count: 0, + following_count: 0, + pleroma: %{hide_follows: true, hide_followers: true} + } = AccountView.render("account.json", %{user: user}) + end + + test "shows actual follower/following count to the account owner" do + user = insert(:user, info: %{hide_followers: true, hide_follows: true}) + other_user = insert(:user) + {:ok, user, other_user, _activity} = CommonAPI.follow(user, other_user) + {:ok, _other_user, user, _activity} = CommonAPI.follow(other_user, user) + + assert %{ + followers_count: 1, + following_count: 1 + } = AccountView.render("account.json", %{user: user, for: user}) + end + end end From dfae61c25c7ee2bb8add38b2cbaa8391f03c9550 Mon Sep 17 00:00:00 2001 From: Maxim Filippov Date: Fri, 9 Aug 2019 23:05:28 +0300 Subject: [PATCH 113/202] Fix deactivated user deletion --- lib/mix/tasks/pleroma/user.ex | 2 +- lib/pleroma/user.ex | 34 +++++++++---------- lib/pleroma/web/activity_pub/activity_pub.ex | 10 +++--- .../web/activity_pub/transmogrifier.ex | 2 +- .../web/admin_api/admin_api_controller.ex | 4 +-- .../web/ostatus/handlers/delete_handler.ex | 2 +- test/user_test.exs | 8 +++++ 7 files changed, 36 insertions(+), 26 deletions(-) diff --git a/lib/mix/tasks/pleroma/user.ex b/lib/mix/tasks/pleroma/user.ex index a3f8bc945..f33d01429 100644 --- a/lib/mix/tasks/pleroma/user.ex +++ b/lib/mix/tasks/pleroma/user.ex @@ -176,7 +176,7 @@ def run(["rm", nickname]) do start_pleroma() with %User{local: true} = user <- User.get_cached_by_nickname(nickname) do - User.perform(:delete, user) + User.perform(:delete, user, nil) shell_info("User #{nickname} deleted.") else _ -> diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 7d18f099e..14057a0e4 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -1029,13 +1029,26 @@ def update_notification_settings(%User{} = user, settings \\ %{}) do |> update_and_set_cache() end + @spec perform(atom(), User.t()) :: {:ok, User.t()} + def perform(:fetch_initial_posts, %User{} = user) do + pages = Pleroma.Config.get!([:fetch_initial_posts, :pages]) + + Enum.each( + # Insert all the posts in reverse order, so they're in the right order on the timeline + Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)), + &Pleroma.Web.Federator.incoming_ap_doc/1 + ) + + {:ok, user} + end + @spec delete(User.t()) :: :ok - def delete(%User{} = user), - do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user]) + def delete(%User{} = user, actor \\ nil), + do: PleromaJobQueue.enqueue(:background, __MODULE__, [:delete, user, actor]) @spec perform(atom(), User.t()) :: {:ok, User.t()} - def perform(:delete, %User{} = user) do - {:ok, _user} = ActivityPub.delete(user) + def perform(:delete, %User{} = user, actor) do + {:ok, _user} = ActivityPub.delete(user, actor: actor) # Remove all relationships {:ok, followers} = User.get_followers(user) @@ -1057,19 +1070,6 @@ def perform(:delete, %User{} = user) do Repo.delete(user) end - @spec perform(atom(), User.t()) :: {:ok, User.t()} - def perform(:fetch_initial_posts, %User{} = user) do - pages = Pleroma.Config.get!([:fetch_initial_posts, :pages]) - - Enum.each( - # Insert all the posts in reverse order, so they're in the right order on the timeline - Enum.reverse(Utils.fetch_ordered_collection(user.info.source_data["outbox"], pages)), - &Pleroma.Web.Federator.incoming_ap_doc/1 - ) - - {:ok, user} - end - def perform(:deactivate_async, user, status), do: deactivate(user, status) @spec perform(atom(), User.t(), list()) :: list() | {:error, any()} diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 1a279a7df..8f669acb9 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -403,11 +403,13 @@ def unfollow(follower, followed, activity_id \\ nil, local \\ true) do end end - def delete(%User{ap_id: ap_id, follower_address: follower_address} = user) do + def delete(data, opts \\ %{actor: nil, local: true}) + + def delete(%User{ap_id: ap_id, follower_address: follower_address} = user, opts) do with data <- %{ "to" => [follower_address], "type" => "Delete", - "actor" => ap_id, + "actor" => opts[:actor] || ap_id, "object" => %{"type" => "Person", "id" => ap_id} }, {:ok, activity} <- insert(data, true, true), @@ -416,7 +418,7 @@ def delete(%User{ap_id: ap_id, follower_address: follower_address} = user) do end end - def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ true) do + def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, opts) do user = User.get_cached_by_ap_id(actor) to = (object.data["to"] || []) ++ (object.data["cc"] || []) @@ -428,7 +430,7 @@ def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ tru "to" => to, "deleted_activity_id" => activity && activity.id }, - {:ok, activity} <- insert(data, local, false), + {:ok, activity} <- insert(data, opts[:local], false), stream_out_participations(object, user), _ <- decrease_replies_count_if_reply(object), # Changing note count prior to enqueuing federation task in order to avoid diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 5403b71d8..b34ef73c0 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -649,7 +649,7 @@ def handle_incoming( {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor), {:ok, object} <- get_obj_helper(object_id), :ok <- Containment.contain_origin(actor.ap_id, object.data), - {:ok, activity} <- ActivityPub.delete(object, false) do + {:ok, activity} <- ActivityPub.delete(object, local: false) do {:ok, activity} else nil -> diff --git a/lib/pleroma/web/admin_api/admin_api_controller.ex b/lib/pleroma/web/admin_api/admin_api_controller.ex index 2d3d0adc4..63c9a7d7f 100644 --- a/lib/pleroma/web/admin_api/admin_api_controller.ex +++ b/lib/pleroma/web/admin_api/admin_api_controller.ex @@ -25,9 +25,9 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do action_fallback(:errors) - def user_delete(conn, %{"nickname" => nickname}) do + def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do User.get_cached_by_nickname(nickname) - |> User.delete() + |> User.delete(admin.ap_id) conn |> json(nickname) diff --git a/lib/pleroma/web/ostatus/handlers/delete_handler.ex b/lib/pleroma/web/ostatus/handlers/delete_handler.ex index b2f9f3946..ac2dc115c 100644 --- a/lib/pleroma/web/ostatus/handlers/delete_handler.ex +++ b/lib/pleroma/web/ostatus/handlers/delete_handler.ex @@ -11,7 +11,7 @@ defmodule Pleroma.Web.OStatus.DeleteHandler do def handle_delete(entry, _doc \\ nil) do with id <- XML.string_from_xpath("//id", entry), %Object{} = object <- Object.normalize(id), - {:ok, delete} <- ActivityPub.delete(object, false) do + {:ok, delete} <- ActivityPub.delete(object, local: false) do delete end end diff --git a/test/user_test.exs b/test/user_test.exs index 8440d456d..e2da8d84b 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -998,6 +998,14 @@ test ".delete_user_activities deletes all create activities", %{user: user} do refute Activity.get_by_id(activity.id) end + test "it deletes deactivated user" do + admin = insert(:user, %{info: %{is_admin: true}}) + {:ok, user} = insert(:user, info: %{deactivated: true}) |> User.set_cache() + + assert {:ok, _} = User.delete(user, admin.ap_id) + refute User.get_by_id(user.id) + end + test "it deletes a user, all follow relationships and all activities", %{user: user} do follower = insert(:user) {:ok, follower} = User.follow(follower, user) From bb9c53958038bb74ad76a9d887b15e6decb5249c Mon Sep 17 00:00:00 2001 From: Maksim Date: Sat, 10 Aug 2019 11:27:59 +0000 Subject: [PATCH 114/202] Uploader.S3 added support stream uploads --- lib/pleroma/uploaders/s3.ex | 12 ++--- mix.exs | 3 +- mix.lock | 1 + test/uploaders/s3_test.exs | 90 +++++++++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 6 deletions(-) create mode 100644 test/uploaders/s3_test.exs diff --git a/lib/pleroma/uploaders/s3.ex b/lib/pleroma/uploaders/s3.ex index 521daa93b..8c353bed3 100644 --- a/lib/pleroma/uploaders/s3.ex +++ b/lib/pleroma/uploaders/s3.ex @@ -6,10 +6,12 @@ defmodule Pleroma.Uploaders.S3 do @behaviour Pleroma.Uploaders.Uploader require Logger + alias Pleroma.Config + # The file name is re-encoded with S3's constraints here to comply with previous # links with less strict filenames def get_file(file) do - config = Pleroma.Config.get([__MODULE__]) + config = Config.get([__MODULE__]) bucket = Keyword.fetch!(config, :bucket) bucket_with_namespace = @@ -34,15 +36,15 @@ def get_file(file) do end def put_file(%Pleroma.Upload{} = upload) do - config = Pleroma.Config.get([__MODULE__]) + config = Config.get([__MODULE__]) bucket = Keyword.get(config, :bucket) - {:ok, file_data} = File.read(upload.tempfile) - s3_name = strict_encode(upload.path) op = - ExAws.S3.put_object(bucket, s3_name, file_data, [ + upload.tempfile + |> ExAws.S3.Upload.stream_file() + |> ExAws.S3.upload(bucket, s3_name, [ {:acl, :public_read}, {:content_type, upload.content_type} ]) diff --git a/mix.exs b/mix.exs index ac175dfed..334fabb33 100644 --- a/mix.exs +++ b/mix.exs @@ -114,8 +114,9 @@ defp deps do {:tesla, "~> 1.2"}, {:jason, "~> 1.0"}, {:mogrify, "~> 0.6.1"}, - {:ex_aws, "~> 2.0"}, + {:ex_aws, "~> 2.1"}, {:ex_aws_s3, "~> 2.0"}, + {:sweet_xml, "~> 0.6.6"}, {:earmark, "~> 1.3"}, {:bbcode, "~> 0.1.1"}, {:ex_machina, "~> 2.3", only: :test}, diff --git a/mix.lock b/mix.lock index 13728d11f..f8ee80c83 100644 --- a/mix.lock +++ b/mix.lock @@ -80,6 +80,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"}, + "sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm"}, "swoosh": {:hex, :swoosh, "0.23.2", "7dda95ff0bf54a2298328d6899c74dae1223777b43563ccebebb4b5d2b61df38", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"}, "syslog": {:git, "https://github.com/Vagabond/erlang-syslog.git", "4a6c6f2c996483e86c1320e9553f91d337bcb6aa", [tag: "1.0.5"]}, "telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"}, diff --git a/test/uploaders/s3_test.exs b/test/uploaders/s3_test.exs new file mode 100644 index 000000000..a0a1cfdf0 --- /dev/null +++ b/test/uploaders/s3_test.exs @@ -0,0 +1,90 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Uploaders.S3Test do + use Pleroma.DataCase + + alias Pleroma.Config + alias Pleroma.Uploaders.S3 + + import Mock + import ExUnit.CaptureLog + + setup do + config = Config.get([Pleroma.Uploaders.S3]) + + Config.put([Pleroma.Uploaders.S3], + bucket: "test_bucket", + public_endpoint: "https://s3.amazonaws.com" + ) + + on_exit(fn -> + Config.put([Pleroma.Uploaders.S3], config) + end) + + :ok + end + + describe "get_file/1" do + test "it returns path to local folder for files" do + assert S3.get_file("test_image.jpg") == { + :ok, + {:url, "https://s3.amazonaws.com/test_bucket/test_image.jpg"} + } + end + + test "it returns path without bucket when truncated_namespace set to ''" do + Config.put([Pleroma.Uploaders.S3], + bucket: "test_bucket", + public_endpoint: "https://s3.amazonaws.com", + truncated_namespace: "" + ) + + assert S3.get_file("test_image.jpg") == { + :ok, + {:url, "https://s3.amazonaws.com/test_image.jpg"} + } + end + + test "it returns path with bucket namespace when namespace is set" do + Config.put([Pleroma.Uploaders.S3], + bucket: "test_bucket", + public_endpoint: "https://s3.amazonaws.com", + bucket_namespace: "family" + ) + + assert S3.get_file("test_image.jpg") == { + :ok, + {:url, "https://s3.amazonaws.com/family:test_bucket/test_image.jpg"} + } + end + end + + describe "put_file/1" do + setup do + file_upload = %Pleroma.Upload{ + name: "image-tet.jpg", + content_type: "image/jpg", + path: "test_folder/image-tet.jpg", + tempfile: Path.absname("test/fixtures/image_tmp.jpg") + } + + [file_upload: file_upload] + end + + test "save file", %{file_upload: file_upload} do + with_mock ExAws, request: fn _ -> {:ok, :ok} end do + assert S3.put_file(file_upload) == {:ok, {:file, "test_folder/image-tet.jpg"}} + end + end + + test "returns error", %{file_upload: file_upload} do + with_mock ExAws, request: fn _ -> {:error, "S3 Upload failed"} end do + assert capture_log(fn -> + assert S3.put_file(file_upload) == {:error, "S3 Upload failed"} + end) =~ "Elixir.Pleroma.Uploaders.S3: {:error, \"S3 Upload failed\"}" + end + end + end +end From 0802a08871afee7f09362cbca8b802f0e27ff4b9 Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sat, 10 Aug 2019 16:27:46 +0300 Subject: [PATCH 115/202] Mastodon API: Fix thread mute detection It was calling CommonAPI.thread_muted? with post author's account instead of viewer's one. --- CHANGELOG.md | 1 + lib/pleroma/web/mastodon_api/views/status_view.ex | 2 +- test/web/mastodon_api/mastodon_api_controller_test.exs | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dccc36965..31caef499 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Federation/MediaProxy not working with instances that have wrong certificate order - Mastodon API: Handling of search timeouts (`/api/v1/search` and `/api/v2/search`) - Mastodon API: Embedded relationships not being properly rendered in the Account entity of Status entity +- Mastodon API: `muted` in the Status entity, using author's account to determine if the tread was muted - Mastodon API: Add `account_id`, `type`, `offset`, and `limit` to search API (`/api/v1/search` and `/api/v2/search`) - Mastodon API, streaming: Fix filtering of notifications based on blocks/mutes/thread mutes - ActivityPub C2S: follower/following collection pages being inaccessible even when authentifucated if `hide_followers`/ `hide_follows` was set diff --git a/lib/pleroma/web/mastodon_api/views/status_view.ex b/lib/pleroma/web/mastodon_api/views/status_view.ex index 80df9b2ac..02819e116 100644 --- a/lib/pleroma/web/mastodon_api/views/status_view.ex +++ b/lib/pleroma/web/mastodon_api/views/status_view.ex @@ -168,7 +168,7 @@ def render("status.json", %{activity: %{data: %{"object" => _object}} = activity thread_muted? = case activity.thread_muted? do thread_muted? when is_boolean(thread_muted?) -> thread_muted? - nil -> CommonAPI.thread_muted?(user, activity) + nil -> (opts[:for] && CommonAPI.thread_muted?(opts[:for], activity)) || false end attachment_data = object.data["attachment"] || [] diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index e49c4cc22..b023d1e4f 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -2901,8 +2901,10 @@ test "bookmarks" do describe "conversation muting" do setup do + post_user = insert(:user) user = insert(:user) - {:ok, activity} = CommonAPI.post(user, %{"status" => "HIE"}) + + {:ok, activity} = CommonAPI.post(post_user, %{"status" => "HIE"}) [user: user, activity: activity] end From 11d08c2de0226caed8119bb3a45a8e0ab8791fbe Mon Sep 17 00:00:00 2001 From: Maksim Date: Sat, 10 Aug 2019 18:46:26 +0000 Subject: [PATCH 116/202] tests for Pleroma.Uploaders --- docs/config.md | 1 + lib/pleroma/uploaders/local.ex | 4 +-- lib/pleroma/uploaders/mdii.ex | 2 ++ test/uploaders/local_test.exs | 32 ++++++++++++++++++++++ test/uploaders/mdii_test.exs | 50 ++++++++++++++++++++++++++++++++++ 5 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 test/uploaders/local_test.exs create mode 100644 test/uploaders/mdii_test.exs diff --git a/docs/config.md b/docs/config.md index 703ef67dd..55311b76d 100644 --- a/docs/config.md +++ b/docs/config.md @@ -18,6 +18,7 @@ Note: `strip_exif` has been replaced by `Pleroma.Upload.Filter.Mogrify`. ## Pleroma.Uploaders.S3 * `bucket`: S3 bucket name +* `bucket_namespace`: S3 bucket namespace * `public_endpoint`: S3 endpoint that the user finally accesses(ex. "https://s3.dualstack.ap-northeast-1.amazonaws.com") * `truncated_namespace`: If you use S3 compatible service such as Digital Ocean Spaces or CDN, set folder name or "" etc. For example, when using CDN to S3 virtual host format, set "". diff --git a/lib/pleroma/uploaders/local.ex b/lib/pleroma/uploaders/local.ex index fc533da23..36b3c35ec 100644 --- a/lib/pleroma/uploaders/local.ex +++ b/lib/pleroma/uploaders/local.ex @@ -11,7 +11,7 @@ def get_file(_) do def put_file(upload) do {local_path, file} = - case Enum.reverse(String.split(upload.path, "/", trim: true)) do + case Enum.reverse(Path.split(upload.path)) do [file] -> {upload_path(), file} @@ -23,7 +23,7 @@ def put_file(upload) do result_file = Path.join(local_path, file) - unless File.exists?(result_file) do + if not File.exists?(result_file) do File.cp!(upload.tempfile, result_file) end diff --git a/lib/pleroma/uploaders/mdii.ex b/lib/pleroma/uploaders/mdii.ex index 237544337..c36f3d61d 100644 --- a/lib/pleroma/uploaders/mdii.ex +++ b/lib/pleroma/uploaders/mdii.ex @@ -3,6 +3,8 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Pleroma.Uploaders.MDII do + @moduledoc "Represents uploader for https://github.com/hakaba-hitoyo/minimal-digital-image-infrastructure" + alias Pleroma.Config alias Pleroma.HTTP diff --git a/test/uploaders/local_test.exs b/test/uploaders/local_test.exs new file mode 100644 index 000000000..fc442d0f1 --- /dev/null +++ b/test/uploaders/local_test.exs @@ -0,0 +1,32 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Uploaders.LocalTest do + use Pleroma.DataCase + alias Pleroma.Uploaders.Local + + describe "get_file/1" do + test "it returns path to local folder for files" do + assert Local.get_file("") == {:ok, {:static_dir, "test/uploads"}} + end + end + + describe "put_file/1" do + test "put file to local folder" do + file_path = "local_upload/files/image.jpg" + + file = %Pleroma.Upload{ + name: "image.jpg", + content_type: "image/jpg", + path: file_path, + tempfile: Path.absname("test/fixtures/image_tmp.jpg") + } + + assert Local.put_file(file) == :ok + + assert Path.join([Local.upload_path(), file_path]) + |> File.exists?() + end + end +end diff --git a/test/uploaders/mdii_test.exs b/test/uploaders/mdii_test.exs new file mode 100644 index 000000000..d432d40f0 --- /dev/null +++ b/test/uploaders/mdii_test.exs @@ -0,0 +1,50 @@ +# Pleroma: A lightweight social networking server +# Copyright © 2017-2019 Pleroma Authors +# SPDX-License-Identifier: AGPL-3.0-only + +defmodule Pleroma.Uploaders.MDIITest do + use Pleroma.DataCase + alias Pleroma.Uploaders.MDII + import Tesla.Mock + + describe "get_file/1" do + test "it returns path to local folder for files" do + assert MDII.get_file("") == {:ok, {:static_dir, "test/uploads"}} + end + end + + describe "put_file/1" do + setup do + file_upload = %Pleroma.Upload{ + name: "mdii-image.jpg", + content_type: "image/jpg", + path: "test_folder/mdii-image.jpg", + tempfile: Path.absname("test/fixtures/image_tmp.jpg") + } + + [file_upload: file_upload] + end + + test "save file", %{file_upload: file_upload} do + mock(fn + %{method: :post, url: "https://mdii.sakura.ne.jp/mdii-post.cgi?jpg"} -> + %Tesla.Env{status: 200, body: "mdii-image"} + end) + + assert MDII.put_file(file_upload) == + {:ok, {:url, "https://mdii.sakura.ne.jp/mdii-image.jpg"}} + end + + test "save file to local if MDII isn`t available", %{file_upload: file_upload} do + mock(fn + %{method: :post, url: "https://mdii.sakura.ne.jp/mdii-post.cgi?jpg"} -> + %Tesla.Env{status: 500} + end) + + assert MDII.put_file(file_upload) == :ok + + assert Path.join([Pleroma.Uploaders.Local.upload_path(), file_upload.path]) + |> File.exists?() + end + end +end From af4cf35e2096a6d1660271f6935b6b9ce77c6757 Mon Sep 17 00:00:00 2001 From: Sergey Suprunenko Date: Sat, 10 Aug 2019 18:47:40 +0000 Subject: [PATCH 117/202] Strip internal fields including likes from incoming and outgoing activities --- CHANGELOG.md | 2 ++ lib/mix/tasks/pleroma/database.ex | 36 +++++++++++++++++++ .../web/activity_pub/transmogrifier.ex | 34 ++---------------- test/tasks/database_test.exs | 36 +++++++++++++++++++ test/web/activity_pub/transmogrifier_test.exs | 30 +++++++++++----- 5 files changed, 98 insertions(+), 40 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 31caef499..759779034 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,6 +73,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Admin API: Endpoint for fetching latest user's statuses - Pleroma API: Add `/api/v1/pleroma/accounts/confirmation_resend?email=` for resending account confirmation. - Relays: Added a task to list relay subscriptions. +- Mix Tasks: `mix pleroma.database fix_likes_collections` +- Federation: Remove `likes` from objects. ### Changed - Configuration: Filter.AnonymizeFilename added ability to retain file extension with custom text diff --git a/lib/mix/tasks/pleroma/database.ex b/lib/mix/tasks/pleroma/database.ex index 8547a329a..bcc2052d6 100644 --- a/lib/mix/tasks/pleroma/database.ex +++ b/lib/mix/tasks/pleroma/database.ex @@ -36,6 +36,10 @@ defmodule Mix.Tasks.Pleroma.Database do ## Remove duplicated items from following and update followers count for all users mix pleroma.database update_users_following_followers_counts + + ## Fix the pre-existing "likes" collections for all objects + + mix pleroma.database fix_likes_collections """ def run(["remove_embedded_objects" | args]) do {options, [], []} = @@ -125,4 +129,36 @@ def run(["prune_objects" | args]) do ) end end + + def run(["fix_likes_collections"]) do + import Ecto.Query + + start_pleroma() + + from(object in Object, + where: fragment("(?)->>'likes' is not null", object.data), + select: %{id: object.id, likes: fragment("(?)->>'likes'", object.data)} + ) + |> Pleroma.RepoStreamer.chunk_stream(100) + |> Stream.each(fn objects -> + ids = + objects + |> Enum.filter(fn object -> object.likes |> Jason.decode!() |> is_map() end) + |> Enum.map(& &1.id) + + Object + |> where([object], object.id in ^ids) + |> update([object], + set: [ + data: + fragment( + "jsonb_set(?, '{likes}', '[]'::jsonb, true)", + object.data + ) + ] + ) + |> Repo.update_all([], timeout: :infinity) + end) + |> Stream.run() + end end diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index 5403b71d8..b7bc48f0a 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -26,6 +26,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do """ def fix_object(object, options \\ []) do object + |> strip_internal_fields |> fix_actor |> fix_url |> fix_attachments @@ -34,7 +35,6 @@ def fix_object(object, options \\ []) do |> fix_emoji |> fix_tag |> fix_content_map - |> fix_likes |> fix_addressing |> fix_summary |> fix_type(options) @@ -151,20 +151,6 @@ def fix_actor(%{"attributedTo" => actor} = object) do |> Map.put("actor", Containment.get_actor(%{"actor" => actor})) end - # Check for standardisation - # This is what Peertube does - # curl -H 'Accept: application/activity+json' $likes | jq .totalItems - # Prismo returns only an integer (count) as "likes" - def fix_likes(%{"likes" => likes} = object) when not is_map(likes) do - object - |> Map.put("likes", []) - |> Map.put("like_count", 0) - end - - def fix_likes(object) do - object - end - def fix_in_reply_to(object, options \\ []) def fix_in_reply_to(%{"inReplyTo" => in_reply_to} = object, options) @@ -784,7 +770,6 @@ def prepare_object(object) do |> add_mention_tags |> add_emoji_tags |> add_attributed_to - |> add_likes |> prepare_attachments |> set_conversation |> set_reply_to_uri @@ -971,22 +956,6 @@ def add_attributed_to(object) do |> Map.put("attributedTo", attributed_to) end - def add_likes(%{"id" => id, "like_count" => likes} = object) do - likes = %{ - "id" => "#{id}/likes", - "first" => "#{id}/likes?page=1", - "type" => "OrderedCollection", - "totalItems" => likes - } - - object - |> Map.put("likes", likes) - end - - def add_likes(object) do - object - end - def prepare_attachments(object) do attachments = (object["attachment"] || []) @@ -1002,6 +971,7 @@ def prepare_attachments(object) do defp strip_internal_fields(object) do object |> Map.drop([ + "likes", "like_count", "announcements", "announcement_count", diff --git a/test/tasks/database_test.exs b/test/tasks/database_test.exs index 579130b05..a8f25f500 100644 --- a/test/tasks/database_test.exs +++ b/test/tasks/database_test.exs @@ -3,8 +3,11 @@ # SPDX-License-Identifier: AGPL-3.0-only defmodule Mix.Tasks.Pleroma.DatabaseTest do + alias Pleroma.Object alias Pleroma.Repo alias Pleroma.User + alias Pleroma.Web.CommonAPI + use Pleroma.DataCase import Pleroma.Factory @@ -46,4 +49,37 @@ test "following and followers count are updated" do assert user.info.follower_count == 0 end end + + describe "running fix_likes_collections" do + test "it turns OrderedCollection likes into empty arrays" do + [user, user2] = insert_pair(:user) + + {:ok, %{id: id, object: object}} = CommonAPI.post(user, %{"status" => "test"}) + {:ok, %{object: object2}} = CommonAPI.post(user, %{"status" => "test test"}) + + CommonAPI.favorite(id, user2) + + likes = %{ + "first" => + "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes?page=1", + "id" => "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes", + "totalItems" => 3, + "type" => "OrderedCollection" + } + + new_data = Map.put(object2.data, "likes", likes) + + object2 + |> Ecto.Changeset.change(%{data: new_data}) + |> Repo.update() + + assert length(Object.get_by_id(object.id).data["likes"]) == 1 + assert is_map(Object.get_by_id(object2.id).data["likes"]) + + assert :ok == Mix.Tasks.Pleroma.Database.run(["fix_likes_collections"]) + + assert length(Object.get_by_id(object.id).data["likes"]) == 1 + assert Enum.empty?(Object.get_by_id(object2.id).data["likes"]) + end + end end diff --git a/test/web/activity_pub/transmogrifier_test.exs b/test/web/activity_pub/transmogrifier_test.exs index e7498e005..060b91e29 100644 --- a/test/web/activity_pub/transmogrifier_test.exs +++ b/test/web/activity_pub/transmogrifier_test.exs @@ -450,6 +450,27 @@ test "it ensures that address fields become lists" do assert !is_nil(data["cc"]) end + test "it strips internal likes" do + data = + File.read!("test/fixtures/mastodon-post-activity.json") + |> Poison.decode!() + + likes = %{ + "first" => + "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes?page=1", + "id" => "http://mastodon.example.org/objects/dbdbc507-52c8-490d-9b7c-1e1d52e5c132/likes", + "totalItems" => 3, + "type" => "OrderedCollection" + } + + object = Map.put(data["object"], "likes", likes) + data = Map.put(data, "object", object) + + {:ok, %Activity{object: object}} = Transmogrifier.handle_incoming(data) + + refute Map.has_key?(object.data, "likes") + end + test "it works for incoming update activities" do data = File.read!("test/fixtures/mastodon-post-activity.json") |> Poison.decode!() @@ -1061,14 +1082,7 @@ test "it strips internal fields of article" do assert is_nil(modified["object"]["announcements"]) assert is_nil(modified["object"]["announcement_count"]) assert is_nil(modified["object"]["context_id"]) - end - - test "it adds like collection to object" do - activity = insert(:note_activity) - {:ok, modified} = Transmogrifier.prepare_outgoing(activity.data) - - assert modified["object"]["likes"]["type"] == "OrderedCollection" - assert modified["object"]["likes"]["totalItems"] == 0 + assert is_nil(modified["object"]["likes"]) end test "the directMessage flag is present" do From 9cfc289594c1d2a1b53c99e3e72bba4b6dc615ca Mon Sep 17 00:00:00 2001 From: Ariadne Conill Date: Sat, 10 Aug 2019 21:18:26 +0000 Subject: [PATCH 118/202] MRF: ensure that subdomain_match calls are case-insensitive --- CHANGELOG.md | 1 + lib/pleroma/web/activity_pub/mrf.ex | 2 +- test/web/activity_pub/mrf/mrf_test.exs | 24 +++++++++++++++++++----- 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfc73c8df..6f1a22359 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Invalid SemVer version generation, when the current branch does not have commits ahead of tag/checked out on a tag - Pleroma.Upload base_url was not automatically whitelisted by MediaProxy. Now your custom CDN or file hosting will be accessed directly as expected. - Report email not being sent to admins when the reporter is a remote user +- MRF: ensure that subdomain_match calls are case-insensitive ### Added - MRF: Support for priming the mediaproxy cache (`Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`) diff --git a/lib/pleroma/web/activity_pub/mrf.ex b/lib/pleroma/web/activity_pub/mrf.ex index dd204b21c..caa2a3231 100644 --- a/lib/pleroma/web/activity_pub/mrf.ex +++ b/lib/pleroma/web/activity_pub/mrf.ex @@ -28,7 +28,7 @@ defp get_policies(_), do: [] @spec subdomains_regex([String.t()]) :: [Regex.t()] def subdomains_regex(domains) when is_list(domains) do - for domain <- domains, do: ~r(^#{String.replace(domain, "*.", "(.*\\.)*")}$) + for domain <- domains, do: ~r(^#{String.replace(domain, "*.", "(.*\\.)*")}$)i end @spec subdomain_match?([Regex.t()], String.t()) :: boolean() diff --git a/test/web/activity_pub/mrf/mrf_test.exs b/test/web/activity_pub/mrf/mrf_test.exs index a9cdf5317..1a888e18f 100644 --- a/test/web/activity_pub/mrf/mrf_test.exs +++ b/test/web/activity_pub/mrf/mrf_test.exs @@ -4,8 +4,8 @@ defmodule Pleroma.Web.ActivityPub.MRFTest do test "subdomains_regex/1" do assert MRF.subdomains_regex(["unsafe.tld", "*.unsafe.tld"]) == [ - ~r/^unsafe.tld$/, - ~r/^(.*\.)*unsafe.tld$/ + ~r/^unsafe.tld$/i, + ~r/^(.*\.)*unsafe.tld$/i ] end @@ -13,7 +13,7 @@ test "subdomains_regex/1" do test "common domains" do regexes = MRF.subdomains_regex(["unsafe.tld", "unsafe2.tld"]) - assert regexes == [~r/^unsafe.tld$/, ~r/^unsafe2.tld$/] + assert regexes == [~r/^unsafe.tld$/i, ~r/^unsafe2.tld$/i] assert MRF.subdomain_match?(regexes, "unsafe.tld") assert MRF.subdomain_match?(regexes, "unsafe2.tld") @@ -24,7 +24,7 @@ test "common domains" do test "wildcard domains with one subdomain" do regexes = MRF.subdomains_regex(["*.unsafe.tld"]) - assert regexes == [~r/^(.*\.)*unsafe.tld$/] + assert regexes == [~r/^(.*\.)*unsafe.tld$/i] assert MRF.subdomain_match?(regexes, "unsafe.tld") assert MRF.subdomain_match?(regexes, "sub.unsafe.tld") @@ -35,12 +35,26 @@ test "wildcard domains with one subdomain" do test "wildcard domains with two subdomains" do regexes = MRF.subdomains_regex(["*.unsafe.tld"]) - assert regexes == [~r/^(.*\.)*unsafe.tld$/] + assert regexes == [~r/^(.*\.)*unsafe.tld$/i] assert MRF.subdomain_match?(regexes, "unsafe.tld") assert MRF.subdomain_match?(regexes, "sub.sub.unsafe.tld") refute MRF.subdomain_match?(regexes, "sub.anotherunsafe.tld") refute MRF.subdomain_match?(regexes, "sub.unsafe.tldanother") end + + test "matches are case-insensitive" do + regexes = MRF.subdomains_regex(["UnSafe.TLD", "UnSAFE2.Tld"]) + + assert regexes == [~r/^UnSafe.TLD$/i, ~r/^UnSAFE2.Tld$/i] + + assert MRF.subdomain_match?(regexes, "UNSAFE.TLD") + assert MRF.subdomain_match?(regexes, "UNSAFE2.TLD") + assert MRF.subdomain_match?(regexes, "unsafe.tld") + assert MRF.subdomain_match?(regexes, "unsafe2.tld") + + refute MRF.subdomain_match?(regexes, "EXAMPLE.COM") + refute MRF.subdomain_match?(regexes, "example.com") + end end end From 92479c6f4870f1ebe4f530db6e31ba960855e1fa Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sun, 11 Aug 2019 22:49:55 +0300 Subject: [PATCH 119/202] Do not fetch the reply object in `fix_type` unless the object has the `name` key and use a depth limit when fetching it --- lib/pleroma/web/activity_pub/transmogrifier.ex | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex index b7bc48f0a..0aee9369f 100644 --- a/lib/pleroma/web/activity_pub/transmogrifier.ex +++ b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -333,13 +333,15 @@ def fix_content_map(object), do: object def fix_type(object, options \\ []) - def fix_type(%{"inReplyTo" => reply_id} = object, options) when is_binary(reply_id) do + def fix_type(%{"inReplyTo" => reply_id, "name" => _} = object, options) + when is_binary(reply_id) do reply = - if Federator.allowed_incoming_reply_depth?(options[:depth]) do - Object.normalize(reply_id, true) + with true <- Federator.allowed_incoming_reply_depth?(options[:depth]), + {:ok, object} <- get_obj_helper(reply_id, options) do + object end - if reply && (reply.data["type"] == "Question" and object["name"]) do + if reply && reply.data["type"] == "Question" do Map.put(object, "type", "Answer") else object From d4d31ffdc4ea1b7a1bb154dbf6a61a90b99c646e Mon Sep 17 00:00:00 2001 From: rinpatch Date: Sun, 11 Aug 2019 23:19:20 +0300 Subject: [PATCH 120/202] Add a changelog entry for !1552 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f1a22359..f3338a5b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - 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_incoming_replies_max_depth` option being ignored in certain cases - Federation/MediaProxy not working with instances that have wrong certificate order - Mastodon API: Handling of search timeouts (`/api/v1/search` and `/api/v2/search`) - Mastodon API: Embedded relationships not being properly rendered in the Account entity of Status entity From 23c46f7e72701b773d87b825526450e5f4ec6322 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 12 Aug 2019 12:51:08 +0200 Subject: [PATCH 121/202] Conversations: Use 'recipients' for accounts in conversation view. According to gargron, this is the intended usage. --- .../web/mastodon_api/views/conversation_view.ex | 15 +++------------ test/web/mastodon_api/conversation_view_test.exs | 6 ------ .../pleroma_api/pleroma_api_controller_test.exs | 8 ++++---- 3 files changed, 7 insertions(+), 22 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex index 5adaecdb0..4a81f0248 100644 --- a/lib/pleroma/web/mastodon_api/views/conversation_view.ex +++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex @@ -12,7 +12,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do alias Pleroma.Web.MastodonAPI.StatusView def render("participation.json", %{participation: participation, user: user}) do - participation = Repo.preload(participation, conversation: :users, recipients: []) + participation = Repo.preload(participation, conversation: [], recipients: []) last_activity_id = with nil <- participation.last_activity_id do @@ -28,7 +28,7 @@ def render("participation.json", %{participation: participation, user: user}) do # Conversations return all users except the current user. users = - participation.conversation.users + participation.recipients |> Enum.reject(&(&1.id == user.id)) accounts = @@ -37,20 +37,11 @@ def render("participation.json", %{participation: participation, user: user}) do as: :user }) - recipients = - AccountView.render("accounts.json", %{ - users: participation.recipients, - as: :user - }) - %{ id: participation.id |> to_string(), accounts: accounts, unread: !participation.read, - last_status: last_status, - pleroma: %{ - recipients: recipients - } + last_status: last_status } end end diff --git a/test/web/mastodon_api/conversation_view_test.exs b/test/web/mastodon_api/conversation_view_test.exs index e32cde5a8..27f668d9f 100644 --- a/test/web/mastodon_api/conversation_view_test.exs +++ b/test/web/mastodon_api/conversation_view_test.exs @@ -30,11 +30,5 @@ test "represents a Mastodon Conversation entity" do assert [account] = conversation.accounts assert account.id == other_user.id - - assert recipients = conversation.pleroma.recipients - recipient_ids = recipients |> Enum.map(& &1.id) - - assert user.id in recipient_ids - assert other_user.id in recipient_ids end end diff --git a/test/web/pleroma_api/pleroma_api_controller_test.exs b/test/web/pleroma_api/pleroma_api_controller_test.exs index 7c75fb229..56bc1572c 100644 --- a/test/web/pleroma_api/pleroma_api_controller_test.exs +++ b/test/web/pleroma_api/pleroma_api_controller_test.exs @@ -67,10 +67,10 @@ test "PATCH /api/v1/pleroma/conversations/:id", %{conn: conn} do assert result["id"] == participation.id |> to_string - assert recipients = result["pleroma"]["recipients"] - recipient_ids = Enum.map(recipients, & &1["id"]) + [participation] = Participation.for_user(user) + participation = Repo.preload(participation, :recipients) - assert user.id in recipient_ids - assert other_user.id in recipient_ids + assert user in participation.recipients + assert other_user in participation.recipients end end From 60231ec7bd0af993dc19f69a57b261b3b4167636 Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 12 Aug 2019 13:58:04 +0200 Subject: [PATCH 122/202] Conversation: Add endpoint to get a conversation by id. --- docs/api/pleroma_api.md | 6 ++++++ .../web/pleroma_api/pleroma_api_controller.ex | 9 +++++++++ lib/pleroma/web/router.ex | 1 + .../pleroma_api_controller_test.exs | 18 ++++++++++++++++++ 4 files changed, 34 insertions(+) diff --git a/docs/api/pleroma_api.md b/docs/api/pleroma_api.md index 590f2a3fb..b134b31a8 100644 --- a/docs/api/pleroma_api.md +++ b/docs/api/pleroma_api.md @@ -340,6 +340,12 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa * Params: Like other timelines * Response: JSON, statuses (200 - healthy, 503 unhealthy). +## `GET /api/v1/pleroma/conversations/:id` +### The conversation with the given ID. +* Method `GET` +* Authentication: required +* Params: None +* Response: JSON, statuses (200 - healthy, 503 unhealthy). ## `PATCH /api/v1/pleroma/conversations/:id` ### Update a conversation. Used to change the set of recipients. diff --git a/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex index 018564452..3175a99b1 100644 --- a/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex @@ -13,6 +13,15 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do alias Pleroma.Web.MastodonAPI.ConversationView alias Pleroma.Web.MastodonAPI.StatusView + def conversation(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do + with %Participation{} = participation <- Participation.get(participation_id), + true <- user.id == participation.user_id do + conn + |> put_view(ConversationView) + |> render("participation.json", %{participation: participation, user: user}) + end + end + def conversation_statuses( %{assigns: %{user: user}} = conn, %{"id" => participation_id} = params diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index c835f06b4..f0b6a02e9 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -265,6 +265,7 @@ defmodule Pleroma.Web.Router do scope [] do pipe_through(:oauth_write) get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses) + get("/conversations/:id", PleromaAPIController, :conversation) patch("/conversations/:id", PleromaAPIController, :update_conversation) end end diff --git a/test/web/pleroma_api/pleroma_api_controller_test.exs b/test/web/pleroma_api/pleroma_api_controller_test.exs index 56bc1572c..ed6b79727 100644 --- a/test/web/pleroma_api/pleroma_api_controller_test.exs +++ b/test/web/pleroma_api/pleroma_api_controller_test.exs @@ -11,6 +11,24 @@ defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do import Pleroma.Factory + test "/api/v1/pleroma/conversations/:id", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, _activity} = + CommonAPI.post(user, %{"status" => "Hi @#{other_user.nickname}!", "visibility" => "direct"}) + + [participation] = Participation.for_user(other_user) + + result = + conn + |> assign(:user, other_user) + |> get("/api/v1/pleroma/conversations/#{participation.id}") + |> json_response(200) + + assert result["id"] == participation.id |> to_string() + end + test "/api/v1/pleroma/conversations/:id/statuses", %{conn: conn} do user = insert(:user) other_user = insert(:user) From 511ccea5aa36b4b0098e49b409b335b0ce8f042e Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 12 Aug 2019 14:23:06 +0200 Subject: [PATCH 123/202] ConversationView: Align parameter names with other views. --- lib/pleroma/web/mastodon_api/mastodon_api_controller.ex | 4 ++-- lib/pleroma/web/mastodon_api/views/conversation_view.ex | 2 +- lib/pleroma/web/pleroma_api/pleroma_api_controller.ex | 4 ++-- lib/pleroma/web/streamer.ex | 2 +- test/web/mastodon_api/conversation_view_test.exs | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 0deeab2be..eb2351eb7 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -1743,7 +1743,7 @@ def conversations(%{assigns: %{user: user}} = conn, params) do conversations = Enum.map(participations, fn participation -> - ConversationView.render("participation.json", %{participation: participation, user: user}) + ConversationView.render("participation.json", %{participation: participation, for: user}) end) conn @@ -1756,7 +1756,7 @@ def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_ Repo.get_by(Participation, id: participation_id, user_id: user.id), {:ok, participation} <- Participation.mark_as_read(participation) do participation_view = - ConversationView.render("participation.json", %{participation: participation, user: user}) + ConversationView.render("participation.json", %{participation: participation, for: user}) conn |> json(participation_view) diff --git a/lib/pleroma/web/mastodon_api/views/conversation_view.ex b/lib/pleroma/web/mastodon_api/views/conversation_view.ex index 4a81f0248..40acc07b3 100644 --- a/lib/pleroma/web/mastodon_api/views/conversation_view.ex +++ b/lib/pleroma/web/mastodon_api/views/conversation_view.ex @@ -11,7 +11,7 @@ defmodule Pleroma.Web.MastodonAPI.ConversationView do alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.StatusView - def render("participation.json", %{participation: participation, user: user}) do + def render("participation.json", %{participation: participation, for: user}) do participation = Repo.preload(participation, conversation: [], recipients: []) last_activity_id = diff --git a/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex index 3175a99b1..b5c3d2728 100644 --- a/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex +++ b/lib/pleroma/web/pleroma_api/pleroma_api_controller.ex @@ -18,7 +18,7 @@ def conversation(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) true <- user.id == participation.user_id do conn |> put_view(ConversationView) - |> render("participation.json", %{participation: participation, user: user}) + |> render("participation.json", %{participation: participation, for: user}) end end @@ -69,7 +69,7 @@ def update_conversation( {:ok, _} <- Participation.set_recipients(participation, recipients) do conn |> put_view(ConversationView) - |> render("participation.json", %{participation: participation, user: user}) + |> render("participation.json", %{participation: participation, for: user}) end end end diff --git a/lib/pleroma/web/streamer.ex b/lib/pleroma/web/streamer.ex index 9ee331030..a0bb10895 100644 --- a/lib/pleroma/web/streamer.ex +++ b/lib/pleroma/web/streamer.ex @@ -209,7 +209,7 @@ def represent_conversation(%Participation{} = participation) do payload: Pleroma.Web.MastodonAPI.ConversationView.render("participation.json", %{ participation: participation, - user: participation.user + for: participation.user }) |> Jason.encode!() } diff --git a/test/web/mastodon_api/conversation_view_test.exs b/test/web/mastodon_api/conversation_view_test.exs index 27f668d9f..a2a880705 100644 --- a/test/web/mastodon_api/conversation_view_test.exs +++ b/test/web/mastodon_api/conversation_view_test.exs @@ -23,7 +23,7 @@ test "represents a Mastodon Conversation entity" do assert participation conversation = - ConversationView.render("participation.json", %{participation: participation, user: user}) + ConversationView.render("participation.json", %{participation: participation, for: user}) assert conversation.id == participation.id |> to_string() assert conversation.last_status.id == activity.id From 2674db14a2ee29e98265c0c0b1db412835b6bbed Mon Sep 17 00:00:00 2001 From: lain Date: Mon, 12 Aug 2019 14:26:18 +0200 Subject: [PATCH 124/202] Modify Changelog. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 069974e44..f8c90a73b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Report email not being sent to admins when the reporter is a remote user ### Added +- Conversations: Add Pleroma-specific conversation endpoints and status posting extensions. Run the `bump_all_conversations` task again to create the necessary data. - MRF: Support for priming the mediaproxy cache (`Pleroma.Web.ActivityPub.MRF.MediaProxyWarmingPolicy`) - MRF: Support for excluding specific domains from Transparency. - MRF: Support for filtering posts based on who they mention (`Pleroma.Web.ActivityPub.MRF.MentionPolicy`) From 24a731a9a67a719749c99ca925f4adb2973a3f2d Mon Sep 17 00:00:00 2001 From: Mark Felder Date: Mon, 12 Aug 2019 15:00:03 -0500 Subject: [PATCH 125/202] Update AdminFE Now permits server configuration. Consider this ALPHA. --- priv/static/adminfe/chunk-0e18.e12401fb.css | Bin 0 -> 723 bytes ...9.c27dac5e.css => chunk-1fbf.d7a1893c.css} | Bin 3624 -> 3624 bytes ...8.0d22684d.css => chunk-2325.0d22684d.css} | Bin priv/static/adminfe/chunk-5e57.ac97b15a.css | Bin 0 -> 2321 bytes ...f.1a04e979.css => chunk-e547.e4b6230b.css} | Bin 3304 -> 3304 bytes .../adminfe/chunk-elementUI.e5cd8da6.css | Bin 0 -> 224642 bytes .../adminfe/chunk-elementUI.f74c256b.css | Bin 202027 -> 0 bytes priv/static/adminfe/index.html | 2 +- .../static/fonts/element-icons.2fad952.woff | Bin 6164 -> 0 bytes .../static/fonts/element-icons.535877f.woff | Bin 0 -> 28200 bytes .../static/fonts/element-icons.6f0a763.ttf | Bin 11040 -> 0 bytes .../static/fonts/element-icons.732389d.ttf | Bin 0 -> 55956 bytes priv/static/adminfe/static/js/app.4137ad8f.js | Bin 115467 -> 0 bytes priv/static/adminfe/static/js/app.8e186193.js | Bin 0 -> 137815 bytes .../adminfe/static/js/chunk-0e18.208cd826.js | Bin 0 -> 4774 bytes .../adminfe/static/js/chunk-1fbf.616fb309.js | Bin 0 -> 17717 bytes ...018.e1a7a454.js => chunk-2325.154a537b.js} | Bin 8220 -> 8220 bytes .../adminfe/static/js/chunk-56c9.28e35fc3.js | Bin 14105 -> 0 bytes .../adminfe/static/js/chunk-5e57.7313703a.js | Bin 0 -> 217441 bytes .../adminfe/static/js/chunk-5eaf.5b76e416.js | Bin 23071 -> 0 bytes .../adminfe/static/js/chunk-7fe2.458f9da5.js | Bin 0 -> 408401 bytes .../adminfe/static/js/chunk-e547.d57d1b91.js | Bin 0 -> 23125 bytes .../static/js/chunk-elementUI.1911151b.js | Bin 0 -> 638883 bytes .../static/js/chunk-elementUI.1fa5434b.js | Bin 562077 -> 0 bytes ...ibs.d5609760.js => chunk-libs.fb0b7f4a.js} | Bin 204098 -> 204635 bytes .../adminfe/static/js/runtime.d8d12c12.js | Bin 3434 -> 0 bytes .../adminfe/static/js/runtime.f40c8ec4.js | Bin 0 -> 3608 bytes 27 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 priv/static/adminfe/chunk-0e18.e12401fb.css rename priv/static/adminfe/{chunk-56c9.c27dac5e.css => chunk-1fbf.d7a1893c.css} (96%) rename priv/static/adminfe/{chunk-f018.0d22684d.css => chunk-2325.0d22684d.css} (100%) create mode 100644 priv/static/adminfe/chunk-5e57.ac97b15a.css rename priv/static/adminfe/{chunk-5eaf.1a04e979.css => chunk-e547.e4b6230b.css} (60%) create mode 100644 priv/static/adminfe/chunk-elementUI.e5cd8da6.css delete mode 100644 priv/static/adminfe/chunk-elementUI.f74c256b.css delete mode 100644 priv/static/adminfe/static/fonts/element-icons.2fad952.woff create mode 100644 priv/static/adminfe/static/fonts/element-icons.535877f.woff delete mode 100644 priv/static/adminfe/static/fonts/element-icons.6f0a763.ttf create mode 100644 priv/static/adminfe/static/fonts/element-icons.732389d.ttf delete mode 100644 priv/static/adminfe/static/js/app.4137ad8f.js create mode 100644 priv/static/adminfe/static/js/app.8e186193.js create mode 100644 priv/static/adminfe/static/js/chunk-0e18.208cd826.js create mode 100644 priv/static/adminfe/static/js/chunk-1fbf.616fb309.js rename priv/static/adminfe/static/js/{chunk-f018.e1a7a454.js => chunk-2325.154a537b.js} (99%) delete mode 100644 priv/static/adminfe/static/js/chunk-56c9.28e35fc3.js create mode 100644 priv/static/adminfe/static/js/chunk-5e57.7313703a.js delete mode 100644 priv/static/adminfe/static/js/chunk-5eaf.5b76e416.js create mode 100644 priv/static/adminfe/static/js/chunk-7fe2.458f9da5.js create mode 100644 priv/static/adminfe/static/js/chunk-e547.d57d1b91.js create mode 100644 priv/static/adminfe/static/js/chunk-elementUI.1911151b.js delete mode 100644 priv/static/adminfe/static/js/chunk-elementUI.1fa5434b.js rename priv/static/adminfe/static/js/{chunk-libs.d5609760.js => chunk-libs.fb0b7f4a.js} (71%) delete mode 100644 priv/static/adminfe/static/js/runtime.d8d12c12.js create mode 100644 priv/static/adminfe/static/js/runtime.f40c8ec4.js diff --git a/priv/static/adminfe/chunk-0e18.e12401fb.css b/priv/static/adminfe/chunk-0e18.e12401fb.css new file mode 100644 index 0000000000000000000000000000000000000000..ba85e77d555e97cbaf09bdaef884580de5d557c3 GIT binary patch literal 723 zcmZ{iL2iRE5Jj)Trn?R@RBa@4l&XsX4`9{UmOV*`qTIchKokWkcB40s?{D{nio&-- zMmWKtXby^$__@NF>R-)JyAjan&dP=?Q>b8w&>DJ~&Io9xA+Dg((Hp$TCsXy9Et1Lp zm?dd7VCb}!W$DLER34SmwgW>g%i`0Iw|0V+;(iK|#5oz57hviq?DZ$HoyvND0=wXjexrTmwm?m-3v{iq`ArI|Qal$V z2ei>+m~Q2kduL2`+(~V8WQcq*1bp!%t+TY&Dn)fa)Q5Px<$Azwr>r|sK8Q>Y-6rI9 zDMutMGV(D}+*0dx2Ho{6%el$eyEFKpPslreXBv5Ve)Cdgv?b_i7JME2xSj=`oPWX$ B0WSam literal 0 HcmV?d00001 diff --git a/priv/static/adminfe/chunk-56c9.c27dac5e.css b/priv/static/adminfe/chunk-1fbf.d7a1893c.css similarity index 96% rename from priv/static/adminfe/chunk-56c9.c27dac5e.css rename to priv/static/adminfe/chunk-1fbf.d7a1893c.css index 2b4283ec5479d85daee882406947355c6599c6c9..4672a9f758af89f89f69ce05a0216d8cece66601 100644 GIT binary patch delta 34 icmZ1>vqEMAFE@vQxtXPDQnJZpQSN3qXY&H?4kiGlSqX;# delta 34 icmZ1>vqEMAFE>Y0l8LdUK}zCeQSN3qXY&H?4kiGxbP6#5 diff --git a/priv/static/adminfe/chunk-f018.0d22684d.css b/priv/static/adminfe/chunk-2325.0d22684d.css similarity index 100% rename from priv/static/adminfe/chunk-f018.0d22684d.css rename to priv/static/adminfe/chunk-2325.0d22684d.css diff --git a/priv/static/adminfe/chunk-5e57.ac97b15a.css b/priv/static/adminfe/chunk-5e57.ac97b15a.css new file mode 100644 index 0000000000000000000000000000000000000000..0c9284744e242456ea5327b6682c1ec4f8abe280 GIT binary patch literal 2321 zcmd6pTWjM+6oCJVAS}e-5t8lLS?Q&;u$z~b!akK{F(c_%9x|E{GviAt{NH=#B1?*s z-7WN?J7Km=xJTRG>H$aEVJtbz zsv*ok(=}ApOc2HDy~&nI_^yFNX}S}u%sxw#^9d7k*S=_rtvt#SAnL1dAC%(~&~n%rWN69yh?!^)J=@jehaL(7i7c2$FDYPzKMxR-Q1dKB zK`l9}qB{TxCNNcG(PS3&C2puLr5S+<@dTk1eO-loNK=8$=)0f|j4WMo{u{(Uus#XF zx1)#Ve28k7()09Vnk=yUW^*a{OQcH$AO^`^ zAHmJK?_3Jc5if}=({{^*UA@Dham!_VXtrBU3*ecM<~|mZ7}*lvE3;mWxZ2vg5$yCe zvsIjwbvPx~PUbchI&1Jo(Hc6-!zY4`Q}M9>5z5Zs6bR@Acr%F|g}uGDr^RfUIk!X6}~*E$|8)4CI!>;sLY0ZKl`O z(b&|w-Jh2@YzIAzMPwVmpo^~+HIy95X7TZ1xhmmX7ou}s9fUWu_vw3j?n2pL0FmpX Fe*ifSS-=1Q literal 0 HcmV?d00001 diff --git a/priv/static/adminfe/chunk-5eaf.1a04e979.css b/priv/static/adminfe/chunk-e547.e4b6230b.css similarity index 60% rename from priv/static/adminfe/chunk-5eaf.1a04e979.css rename to priv/static/adminfe/chunk-e547.e4b6230b.css index a09287f584eb4329d6eefeee36f3424ec8977ee1..f740543a0ae4961dea98604533843ffc08f9f480 100644 GIT binary patch delta 321 zcmaDM`9g9-3JXWFsbO-8MQZBg92Qpu$Cle}@^_Y92!WlfP6!S&nkS zaL%wHIPvjDvT*1RLc`pyY3|zRnATcE+ SGcR2?CpE3a%A%m6wiW>NkY=#} delta 321 zcmaDM`9g9-3JZsET3SjkS zaL%wHIPvjDvT*1RLc`pyY3|zRnATcE+ SGcR2?CpE3a%A%m6wiW=aoMt5e diff --git a/priv/static/adminfe/chunk-elementUI.e5cd8da6.css b/priv/static/adminfe/chunk-elementUI.e5cd8da6.css new file mode 100644 index 0000000000000000000000000000000000000000..3fef5e5fdb26a8762d4ca7ef8be2410ec15c1654 GIT binary patch literal 224642 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 rY}3kDkPzH8JxDaf5|H9;_9$s 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