diff options
-rw-r--r-- | .gitlab-ci.yml | 9 | ||||
-rw-r--r-- | README.md | 2 | ||||
-rw-r--r-- | config/config.exs | 35 | ||||
-rw-r--r-- | lib/pleroma/notification.ex | 5 | ||||
-rw-r--r-- | lib/pleroma/object.ex | 21 | ||||
-rw-r--r-- | lib/pleroma/object_tombstone.ex | 4 | ||||
-rw-r--r-- | lib/pleroma/user.ex | 1 | ||||
-rw-r--r-- | lib/pleroma/web/activity_pub/activity_pub.ex | 7 | ||||
-rw-r--r-- | lib/pleroma/web/nodeinfo/nodeinfo_controller.ex | 3 | ||||
-rw-r--r-- | lib/pleroma/web/twitter_api/twitter_api_controller.ex | 11 | ||||
-rw-r--r-- | mix.exs | 4 | ||||
-rw-r--r-- | test/activity_test.exs | 8 | ||||
-rw-r--r-- | test/object_test.exs | 4 | ||||
-rw-r--r-- | test/user_test.exs | 14 | ||||
-rw-r--r-- | test/web/activity_pub/activity_pub_test.exs | 12 | ||||
-rw-r--r-- | test/web/mastodon_api/mastodon_api_controller_test.exs | 22 | ||||
-rw-r--r-- | test/web/node_info_test.exs | 11 | ||||
-rw-r--r-- | test/web/ostatus/incoming_documents/delete_handling_test.exs | 2 | ||||
-rw-r--r-- | test/web/ostatus/ostatus_controller_test.exs | 18 | ||||
-rw-r--r-- | test/web/twitter_api/twitter_api_controller_test.exs | 88 |
20 files changed, 257 insertions, 24 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 54215c318..b59445895 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -8,6 +8,7 @@ variables: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres DB_HOST: postgres + MIX_ENV: test cache: key: ${CI_COMMIT_REF_SLUG} @@ -23,15 +24,15 @@ before_script: - mix local.rebar --force - mix deps.get - mix compile --force - - MIX_ENV=test mix ecto.create - - MIX_ENV=test mix ecto.migrate + - mix ecto.create + - mix ecto.migrate lint: stage: lint script: - - MIX_ENV=test mix format --check-formatted + - mix format --check-formatted unit-testing: stage: test script: - - MIX_ENV=test mix test --trace + - mix test --trace @@ -30,7 +30,7 @@ While we don't provide docker files, other people have written very good ones. T ### Dependencies * Postgresql version 9.6 or newer -* Elixir version 1.5 or newer. If your distribution only has an old version available, check [Elixir's install page](https://elixir-lang.org/install.html) or use a tool like [asdf](https://github.com/asdf-vm/asdf). +* Elixir version 1.7 or newer. If your distribution only has an old version available, check [Elixir's install page](https://elixir-lang.org/install.html) or use a tool like [asdf](https://github.com/asdf-vm/asdf). * Build-essential tools ### Configuration diff --git a/config/config.exs b/config/config.exs index 4b8762761..1983b31ab 100644 --- a/config/config.exs +++ b/config/config.exs @@ -137,8 +137,8 @@ config :pleroma, :fe, logo_mask: true, logo_margin: "0.1em", background: "/static/aurora_borealis.jpg", - redirect_root_no_login: "/~/main/all", - redirect_root_login: "/~/main/friends", + redirect_root_no_login: "/main/all", + redirect_root_login: "/main/friends", show_instance_panel: true, scope_options_enabled: false, formatting_options_enabled: false, @@ -220,6 +220,37 @@ config :cors_plug, credentials: true, headers: ["Authorization", "Content-Type", "Idempotency-Key"] +config :pleroma, Pleroma.User, + restricted_nicknames: [ + "about", + "~", + "main", + "users", + "settings", + "objects", + "activities", + "web", + "registration", + "friend-requests", + "pleroma", + "api", + "tag", + "notice", + "status", + "user-search", + "ostatus_subscribe", + "oauth", + "push", + "relay", + "inbox", + ".well-known", + "nodeinfo", + "auth", + "proxy", + "dev", + "internal" + ] + # 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/notification.ex b/lib/pleroma/notification.ex index 301cfd134..b5aadfd17 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -80,9 +80,8 @@ defmodule Pleroma.Notification do end def clear(user) do - query = from(n in Notification, where: n.user_id == ^user.id) - - Repo.delete_all(query) + from(n in Notification, where: n.user_id == ^user.id) + |> Repo.delete_all() end def dismiss(%{id: user_id} = _user, id) do diff --git a/lib/pleroma/object.ex b/lib/pleroma/object.ex index cc4a2181a..e2b648727 100644 --- a/lib/pleroma/object.ex +++ b/lib/pleroma/object.ex @@ -4,7 +4,7 @@ defmodule Pleroma.Object do use Ecto.Schema - alias Pleroma.{Repo, Object, User, Activity} + alias Pleroma.{Repo, Object, User, Activity, ObjectTombstone} import Ecto.{Query, Changeset} schema "objects" do @@ -66,8 +66,25 @@ defmodule Pleroma.Object do Object.change(%Object{}, %{data: %{"id" => context}}) end + def make_tombstone(%Object{data: %{"id" => id, "type" => type}}, deleted \\ DateTime.utc_now()) do + %ObjectTombstone{ + id: id, + formerType: type, + deleted: deleted + } + |> Map.from_struct() + end + + def swap_object_with_tombstone(object) do + tombstone = make_tombstone(object) + + object + |> Object.change(%{data: tombstone}) + |> Repo.update() + end + def delete(%Object{data: %{"id" => id}} = object) do - with Repo.delete(object), + with {:ok, _obj} = swap_object_with_tombstone(object), Repo.delete_all(Activity.all_non_create_by_object_ap_id_q(id)), {:ok, true} <- Cachex.del(:object_cache, "object:#{id}") do {:ok, object} diff --git a/lib/pleroma/object_tombstone.ex b/lib/pleroma/object_tombstone.ex new file mode 100644 index 000000000..64d836d3e --- /dev/null +++ b/lib/pleroma/object_tombstone.ex @@ -0,0 +1,4 @@ +defmodule Pleroma.ObjectTombstone do + @enforce_keys [:id, :formerType, :deleted] + defstruct [:id, :formerType, :deleted, type: "Tombstone"] +end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 1f930479d..33f5e43fc 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -197,6 +197,7 @@ defmodule Pleroma.User do |> validate_confirmation(:password) |> unique_constraint(:email) |> unique_constraint(:nickname) + |> validate_exclusion(:nickname, Pleroma.Config.get([Pleroma.User, :restricted_nicknames])) |> validate_format(:nickname, local_nickname_regex()) |> validate_format(:email, @email_regex) |> validate_length(:bio, max: 1000) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 188060780..2d4cc9f68 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -503,6 +503,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do defp restrict_replies(query, _), do: query + defp restrict_reblogs(query, %{"exclude_reblogs" => val}) when val == "true" or val == "1" do + from(activity in query, where: fragment("?->>'type' != 'Announce'", activity.data)) + end + + defp restrict_reblogs(query, _), do: query + # Only search through last 100_000 activities by default defp restrict_recent(query, %{"whole_db" => true}), do: query @@ -561,6 +567,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do |> restrict_media(opts) |> restrict_visibility(opts) |> restrict_replies(opts) + |> restrict_reblogs(opts) end def fetch_activities(recipients, opts \\ %{}) do diff --git a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex index 1265d81c5..a992f75f6 100644 --- a/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex +++ b/lib/pleroma/web/nodeinfo/nodeinfo_controller.ex @@ -138,7 +138,8 @@ defmodule Pleroma.Web.Nodeinfo.NodeinfoController do }, accountActivationRequired: Keyword.get(instance, :account_activation_required, false), invitesEnabled: Keyword.get(instance, :invites_enabled, false), - features: features + features: features, + restrictedNicknames: Pleroma.Config.get([Pleroma.User, :restricted_nicknames]) } } diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex index c25cb0876..92b7386da 100644 --- a/lib/pleroma/web/twitter_api/twitter_api_controller.ex +++ b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -130,6 +130,15 @@ defmodule Pleroma.Web.TwitterAPI.Controller do def user_timeline(%{assigns: %{user: user}} = conn, params) do case TwitterAPI.get_user(user, params) do {:ok, target_user} -> + # Twitter and ActivityPub use a different name and sense for this parameter. + {include_rts, params} = Map.pop(params, "include_rts") + + params = + case include_rts do + x when x == "false" or x == "0" -> Map.put(params, "exclude_reblogs", "true") + _ -> params + end + activities = ActivityPub.fetch_user_activities(target_user, user, params) conn @@ -653,7 +662,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do json_reply(conn, 403, json) end - def only_if_public_instance(conn = %{conn: %{assigns: %{user: _user}}}, _), do: conn + def only_if_public_instance(%{assigns: %{user: %User{}}} = conn, _), do: conn def only_if_public_instance(conn, _) do if Keyword.get(Application.get_env(:pleroma, :instance), :public) do @@ -5,7 +5,7 @@ defmodule Pleroma.Mixfile do [ app: :pleroma, version: version("0.9.0"), - elixir: "~> 1.4", + elixir: "~> 1.7", elixirc_paths: elixirc_paths(Mix.env()), compilers: [:phoenix, :gettext] ++ Mix.compilers(), elixirc_options: [warnings_as_errors: true], @@ -71,7 +71,7 @@ defmodule Pleroma.Mixfile do {:crypt, git: "https://github.com/msantos/crypt", ref: "1f2b58927ab57e72910191a7ebaeff984382a1d3"}, {:cors_plug, "~> 1.5"}, - {:ex_doc, "> 0.18.3 and < 0.20.0", only: :dev, runtime: false}, + {:ex_doc, "~> 0.19", only: :dev, runtime: false}, {:web_push_encryption, "~> 0.2.1"}, {:swoosh, "~> 0.20"}, {:gen_smtp, "~> 0.13"}, diff --git a/test/activity_test.exs b/test/activity_test.exs index cf27fbbc5..36c718869 100644 --- a/test/activity_test.exs +++ b/test/activity_test.exs @@ -4,18 +4,19 @@ defmodule Pleroma.ActivityTest do use Pleroma.DataCase + alias Pleroma.Activity import Pleroma.Factory test "returns an activity by it's AP id" do activity = insert(:note_activity) - found_activity = Pleroma.Activity.get_by_ap_id(activity.data["id"]) + found_activity = Activity.get_by_ap_id(activity.data["id"]) assert activity == found_activity end test "returns activities by it's objects AP ids" do activity = insert(:note_activity) - [found_activity] = Pleroma.Activity.all_by_object_ap_id(activity.data["object"]["id"]) + [found_activity] = Activity.all_by_object_ap_id(activity.data["object"]["id"]) assert activity == found_activity end @@ -23,8 +24,7 @@ defmodule Pleroma.ActivityTest do test "returns the activity that created an object" do activity = insert(:note_activity) - found_activity = - Pleroma.Activity.get_create_activity_by_object_ap_id(activity.data["object"]["id"]) + found_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"]["id"]) assert activity == found_activity end diff --git a/test/object_test.exs b/test/object_test.exs index 0effb9505..72194975d 100644 --- a/test/object_test.exs +++ b/test/object_test.exs @@ -36,6 +36,8 @@ defmodule Pleroma.ObjectTest do found_object = Object.get_by_ap_id(object.data["id"]) refute object == found_object + + assert found_object.data["type"] == "Tombstone" end test "ensures cache is cleared for the object" do @@ -51,6 +53,8 @@ defmodule Pleroma.ObjectTest do cached_object = Object.get_cached_by_ap_id(object.data["id"]) refute object == cached_object + + assert cached_object.data["type"] == "Tombstone" end end end diff --git a/test/user_test.exs b/test/user_test.exs index aab6473cf..8c7e1594b 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -153,6 +153,20 @@ defmodule Pleroma.UserTest do end) end + test "it restricts certain nicknames" do + [restricted_name | _] = Pleroma.Config.get([Pleroma.User, :restricted_nicknames]) + + assert is_bitstring(restricted_name) + + params = + @full_user_data + |> Map.put(:nickname, restricted_name) + + changeset = User.register_changeset(%User{}, params) + + refute changeset.valid? + end + test "it sets the password_hash, ap_id and following fields" do changeset = User.register_changeset(%User{}, @full_user_data) diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index 4f6b7f058..7bccd7500 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -180,6 +180,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert Enum.member?(activities, activity_one) end + test "excludes reblogs on request" do + user = insert(:user) + {:ok, expected_activity} = ActivityBuilder.insert(%{"type" => "Create"}, %{:user => user}) + {:ok, _} = ActivityBuilder.insert(%{"type" => "Announce"}, %{:user => user}) + + [activity] = ActivityPub.fetch_user_activities(user, nil, %{"exclude_reblogs" => "true"}) + + assert activity == expected_activity + end + describe "public fetch activities" do test "doesn't retrieve unlisted activities" do user = insert(:user) @@ -482,7 +492,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do assert Repo.get(Activity, delete.id) != nil - assert Repo.get(Object, object.id) == nil + assert Repo.get(Object, object.id).data["type"] == "Tombstone" 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 433c135f7..0136acf8c 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -296,7 +296,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert %{} = json_response(conn, 200) - assert Repo.get(Activity, activity.id) == nil + refute Repo.get(Activity, activity.id) end test "when you didn't create it", %{conn: conn} do @@ -840,6 +840,26 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do assert [%{"id" => id}] = json_response(conn, 200) assert id == to_string(image_post.id) end + + test "gets a user's statuses without reblogs", %{conn: conn} do + user = insert(:user) + {:ok, post} = CommonAPI.post(user, %{"status" => "HI!!!"}) + {:ok, _, _} = CommonAPI.repeat(post.id, user) + + conn = + conn + |> get("/api/v1/accounts/#{user.id}/statuses", %{"exclude_reblogs" => "true"}) + + assert [%{"id" => id}] = json_response(conn, 200) + assert id == to_string(post.id) + + conn = + conn + |> get("/api/v1/accounts/#{user.id}/statuses", %{"exclude_reblogs" => "1"}) + + assert [%{"id" => id}] = json_response(conn, 200) + assert id == to_string(post.id) + end end describe "user relationships" do diff --git a/test/web/node_info_test.exs b/test/web/node_info_test.exs index 6769a4490..5981c70a7 100644 --- a/test/web/node_info_test.exs +++ b/test/web/node_info_test.exs @@ -19,6 +19,17 @@ defmodule Pleroma.Web.NodeInfoTest do assert user.ap_id in result["metadata"]["staffAccounts"] end + test "nodeinfo shows restricted nicknames", %{conn: conn} do + conn = + conn + |> get("/nodeinfo/2.0.json") + + assert result = json_response(conn, 200) + + assert Pleroma.Config.get([Pleroma.User, :restricted_nicknames]) == + result["metadata"]["restrictedNicknames"] + end + test "returns 404 when federation is disabled", %{conn: conn} do instance = Application.get_env(:pleroma, :instance) diff --git a/test/web/ostatus/incoming_documents/delete_handling_test.exs b/test/web/ostatus/incoming_documents/delete_handling_test.exs index 1e041e5b0..c8fbff6cc 100644 --- a/test/web/ostatus/incoming_documents/delete_handling_test.exs +++ b/test/web/ostatus/incoming_documents/delete_handling_test.exs @@ -25,7 +25,7 @@ defmodule Pleroma.Web.OStatus.DeleteHandlingTest do refute Repo.get(Activity, note.id) refute Repo.get(Activity, like.id) - refute Object.get_by_ap_id(note.data["object"]["id"]) + assert Object.get_by_ap_id(note.data["object"]["id"]).data["type"] == "Tombstone" assert Repo.get(Activity, second_note.id) assert Object.get_by_ap_id(second_note.data["object"]["id"]) diff --git a/test/web/ostatus/ostatus_controller_test.exs b/test/web/ostatus/ostatus_controller_test.exs index 6b535a1a9..995cc00d6 100644 --- a/test/web/ostatus/ostatus_controller_test.exs +++ b/test/web/ostatus/ostatus_controller_test.exs @@ -5,7 +5,7 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do use Pleroma.Web.ConnCase import Pleroma.Factory - alias Pleroma.{User, Repo} + alias Pleroma.{User, Repo, Object} alias Pleroma.Web.CommonAPI alias Pleroma.Web.OStatus.ActivityRepresenter @@ -114,6 +114,22 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do |> response(404) end + test "404s on deleted objects", %{conn: conn} do + note_activity = insert(:note_activity) + [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["object"]["id"])) + object = Object.get_by_ap_id(note_activity.data["object"]["id"]) + + conn + |> get("/objects/#{uuid}") + |> response(200) + + Object.delete(object) + + conn + |> get("/objects/#{uuid}") + |> response(404) + end + test "gets an activity", %{conn: conn} do note_activity = insert(:note_activity) [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"])) diff --git a/test/web/twitter_api/twitter_api_controller_test.exs b/test/web/twitter_api/twitter_api_controller_test.exs index 0e656f9ca..a4526eeda 100644 --- a/test/web/twitter_api/twitter_api_controller_test.exs +++ b/test/web/twitter_api/twitter_api_controller_test.exs @@ -112,6 +112,8 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do end describe "GET /statuses/public_timeline.json" do + setup [:valid_user] + test "returns statuses", %{conn: conn} do user = insert(:user) activities = ActivityBuilder.insert_list(30, %{}, %{user: user}) @@ -145,14 +147,44 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do Application.put_env(:pleroma, :instance, instance) end + test "returns 200 to authenticated request when the instance is not public", + %{conn: conn, user: user} do + instance = + Application.get_env(:pleroma, :instance) + |> Keyword.put(:public, false) + + Application.put_env(:pleroma, :instance, instance) + + conn + |> with_credentials(user.nickname, "test") + |> get("/api/statuses/public_timeline.json") + |> json_response(200) + + instance = + Application.get_env(:pleroma, :instance) + |> Keyword.put(:public, true) + + Application.put_env(:pleroma, :instance, instance) + end + test "returns 200 to unauthenticated request when the instance is public", %{conn: conn} do conn |> get("/api/statuses/public_timeline.json") |> json_response(200) end + + test "returns 200 to authenticated request when the instance is public", + %{conn: conn, user: user} do + conn + |> with_credentials(user.nickname, "test") + |> get("/api/statuses/public_timeline.json") + |> json_response(200) + end end describe "GET /statuses/public_and_external_timeline.json" do + setup [:valid_user] + test "returns 403 to unauthenticated request when the instance is not public", %{conn: conn} do instance = Application.get_env(:pleroma, :instance) @@ -171,11 +203,39 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do Application.put_env(:pleroma, :instance, instance) end + test "returns 200 to authenticated request when the instance is not public", + %{conn: conn, user: user} do + instance = + Application.get_env(:pleroma, :instance) + |> Keyword.put(:public, false) + + Application.put_env(:pleroma, :instance, instance) + + conn + |> with_credentials(user.nickname, "test") + |> get("/api/statuses/public_and_external_timeline.json") + |> json_response(200) + + instance = + Application.get_env(:pleroma, :instance) + |> Keyword.put(:public, true) + + Application.put_env(:pleroma, :instance, instance) + end + test "returns 200 to unauthenticated request when the instance is public", %{conn: conn} do conn |> get("/api/statuses/public_and_external_timeline.json") |> json_response(200) end + + test "returns 200 to authenticated request when the instance is public", + %{conn: conn, user: user} do + conn + |> with_credentials(user.nickname, "test") + |> get("/api/statuses/public_and_external_timeline.json") + |> json_response(200) + end end describe "GET /statuses/show/:id.json" do @@ -519,6 +579,34 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do assert length(response) == 1 assert Enum.at(response, 0) == ActivityRepresenter.to_map(activity, %{user: user}) end + + test "with credentials with user_id, excluding RTs", %{conn: conn, user: current_user} do + user = insert(:user) + {:ok, activity} = ActivityBuilder.insert(%{"id" => 1, "type" => "Create"}, %{user: user}) + {:ok, _} = ActivityBuilder.insert(%{"id" => 2, "type" => "Announce"}, %{user: user}) + + conn = + conn + |> with_credentials(current_user.nickname, "test") + |> get("/api/statuses/user_timeline.json", %{ + "user_id" => user.id, + "include_rts" => "false" + }) + + response = json_response(conn, 200) + + assert length(response) == 1 + assert Enum.at(response, 0) == ActivityRepresenter.to_map(activity, %{user: user}) + + conn = + conn + |> get("/api/statuses/user_timeline.json", %{"user_id" => user.id, "include_rts" => "0"}) + + response = json_response(conn, 200) + + assert length(response) == 1 + assert Enum.at(response, 0) == ActivityRepresenter.to_map(activity, %{user: user}) + end end describe "POST /friendships/create.json" do |