diff --git a/lib/pleroma/notification.ex b/lib/pleroma/notification.ex index 35f817d1d..00a382f31 100644 --- a/lib/pleroma/notification.ex +++ b/lib/pleroma/notification.ex @@ -46,9 +46,11 @@ def create_notifications(_), do: {:ok, []} # TODO move to sql, too. def create_notification(%Activity{} = activity, %User{} = user) do - notification = %Notification{user_id: user.id, activity_id: activity.id} - {:ok, notification} = Repo.insert(notification) - notification + unless User.blocks?(user, %{ap_id: activity.data["actor"]}) do + notification = %Notification{user_id: user.id, activity_id: activity.id} + {:ok, notification} = Repo.insert(notification) + notification + end end end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex index 5f1750035..771c54e81 100644 --- a/lib/pleroma/user.ex +++ b/lib/pleroma/user.ex @@ -293,4 +293,28 @@ def search(query, resolve) do limit: 20 Repo.all(q) end + + def block(user, %{ap_id: ap_id}) do + blocks = user.info["blocks"] || [] + new_blocks = Enum.uniq([ap_id | blocks]) + new_info = Map.put(user.info, "blocks", new_blocks) + + cs = User.info_changeset(user, %{info: new_info}) + Repo.update(cs) + end + + def unblock(user, %{ap_id: ap_id}) do + blocks = user.info["blocks"] || [] + new_blocks = List.delete(blocks, ap_id) + new_info = Map.put(user.info, "blocks", new_blocks) + + cs = User.info_changeset(user, %{info: new_info}) + Repo.update(cs) + end + + def blocks?(user, %{ap_id: ap_id}) do + blocks = user.info["blocks"] || [] + Enum.member?(blocks, ap_id) + end + end diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex index 71e52cb46..a62be2511 100644 --- a/lib/pleroma/web/activity_pub/activity_pub.ex +++ b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -93,10 +93,11 @@ def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ tru end end - def fetch_activities_for_context(context) do + def fetch_activities_for_context(context, opts \\ %{}) do query = from activity in Activity, where: fragment("?->>'type' = ? and ?->>'context' = ?", activity.data, "Create", activity.data, ^context), order_by: [desc: :id] + query = restrict_blocked(query, opts) Repo.all(query) end @@ -163,6 +164,13 @@ defp restrict_recent(query, _) do where: activity.id > ^since end + defp restrict_blocked(query, %{"blocking_user" => %User{info: info}}) do + blocks = info["blocks"] || [] + from activity in query, + where: fragment("not (?->>'actor' = ANY(?))", activity.data, ^blocks) + end + defp restrict_blocked(query, _), do: query + def fetch_activities(recipients, opts \\ %{}) do base_query = from activity in Activity, limit: 20, @@ -178,6 +186,7 @@ def fetch_activities(recipients, opts \\ %{}) do |> restrict_type(opts) |> restrict_favorited_by(opts) |> restrict_recent(opts) + |> restrict_blocked(opts) |> Repo.all |> Enum.reverse end diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex index 19e0be3a1..b1a54a4f1 100644 --- a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex +++ b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -79,6 +79,7 @@ defp add_link_headers(conn, method, activities) do def home_timeline(%{assigns: %{user: user}} = conn, params) do params = params |> Map.put("type", ["Create", "Announce"]) + |> Map.put("blocking_user", user) activities = ActivityPub.fetch_activities([user.ap_id | user.following], params) |> Enum.reverse @@ -92,6 +93,7 @@ def public_timeline(%{assigns: %{user: user}} = conn, params) do params = params |> Map.put("type", ["Create", "Announce"]) |> Map.put("local_only", !!params["local"]) + |> Map.put("blocking_user", user) activities = ActivityPub.fetch_public_activities(params) |> Enum.reverse @@ -123,7 +125,7 @@ def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do with %Activity{} = activity <- Repo.get(Activity, id), - activities <- ActivityPub.fetch_activities_for_context(activity.data["object"]["context"]), + activities <- ActivityPub.fetch_activities_for_context(activity.data["object"]["context"], %{"blocking_user" => user}), activities <- activities |> Enum.filter(fn (%{id: aid}) -> to_string(aid) != to_string(id) end), grouped_activities <- Enum.group_by(activities, fn (%{id: id}) -> id < activity.id end) do result = %{ @@ -246,6 +248,7 @@ def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do params = params |> Map.put("type", "Create") |> Map.put("local_only", !!params["local"]) + |> Map.put("blocking_user", user) activities = ActivityPub.fetch_public_activities(params) |> Enum.reverse @@ -308,6 +311,39 @@ def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do end end + def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do + with %User{} = blocked <- Repo.get(User, id), + {:ok, blocker} <- User.block(blocker, blocked) do + render conn, AccountView, "relationship.json", %{user: blocker, target: blocked} + else + {:error, message} = err -> + conn + |> put_resp_content_type("application/json") + |> send_resp(403, Poison.encode!(%{"error" => message})) + end + end + + def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do + with %User{} = blocked <- Repo.get(User, id), + {:ok, blocker} <- User.unblock(blocker, blocked) do + render conn, AccountView, "relationship.json", %{user: blocker, target: blocked} + else + {:error, message} = err -> + conn + |> put_resp_content_type("application/json") + |> send_resp(403, Poison.encode!(%{"error" => message})) + end + end + + # TODO: Use proper query + def blocks(%{assigns: %{user: user}} = conn, _) do + with blocked_users <- user.info["blocks"] || [], + accounts <- Enum.map(blocked_users, fn (ap_id) -> User.get_cached_by_ap_id(ap_id) end) do + res = AccountView.render("accounts.json", users: accounts, for: user, as: :user) + json(conn, res) + end + end + def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do accounts = User.search(query, params["resolve"] == "true") @@ -338,6 +374,7 @@ def favourites(%{assigns: %{user: user}} = conn, params) do params = conn |> Map.put("type", "Create") |> Map.put("favorited_by", user.ap_id) + |> Map.put("blocking_user", user) activities = ActivityPub.fetch_activities([], params) |> Enum.reverse diff --git a/lib/pleroma/web/mastodon_api/views/account_view.ex b/lib/pleroma/web/mastodon_api/views/account_view.ex index ff02587d6..cf97ab746 100644 --- a/lib/pleroma/web/mastodon_api/views/account_view.ex +++ b/lib/pleroma/web/mastodon_api/views/account_view.ex @@ -55,7 +55,7 @@ def render("relationship.json", %{user: user, target: target}) do id: target.id, following: User.following?(user, target), followed_by: User.following?(target, user), - blocking: false, + blocking: User.blocks?(user, target), muting: false, requested: false, domain_blocking: false diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex index 1fb5eadf6..f96ec7213 100644 --- a/lib/pleroma/web/router.ex +++ b/lib/pleroma/web/router.ex @@ -58,14 +58,15 @@ def user_fetcher(username) do get "/accounts/search", MastodonAPIController, :account_search post "/accounts/:id/follow", MastodonAPIController, :follow post "/accounts/:id/unfollow", MastodonAPIController, :unfollow - post "/accounts/:id/block", MastodonAPIController, :relationship_noop - post "/accounts/:id/unblock", MastodonAPIController, :relationship_noop + post "/accounts/:id/block", MastodonAPIController, :block + post "/accounts/:id/unblock", MastodonAPIController, :unblock post "/accounts/:id/mute", MastodonAPIController, :relationship_noop post "/accounts/:id/unmute", MastodonAPIController, :relationship_noop post "/follows", MastodonAPIController, :follow - get "/blocks", MastodonAPIController, :empty_array + get "/blocks", MastodonAPIController, :blocks + get "/domain_blocks", MastodonAPIController, :empty_array get "/follow_requests", MastodonAPIController, :empty_array get "/mutes", MastodonAPIController, :empty_array diff --git a/lib/pleroma/web/twitter_api/twitter_api.ex b/lib/pleroma/web/twitter_api/twitter_api.ex index d5c5cf5cf..912d5e278 100644 --- a/lib/pleroma/web/twitter_api/twitter_api.ex +++ b/lib/pleroma/web/twitter_api/twitter_api.ex @@ -14,17 +14,20 @@ def create_status(%User{} = user, %{"status" => status} = data) do end def fetch_friend_statuses(user, opts \\ %{}) do + opts = Map.put(opts, "blocking_user", user) ActivityPub.fetch_activities([user.ap_id | user.following], opts) |> activities_to_statuses(%{for: user}) end def fetch_public_statuses(user, opts \\ %{}) do opts = Map.put(opts, "local_only", true) + opts = Map.put(opts, "blocking_user", user) ActivityPub.fetch_public_activities(opts) |> activities_to_statuses(%{for: user}) end def fetch_public_and_external_statuses(user, opts \\ %{}) do + opts = Map.put(opts, "blocking_user", user) ActivityPub.fetch_public_activities(opts) |> activities_to_statuses(%{for: user}) end @@ -41,7 +44,7 @@ def fetch_mentions(user, opts \\ %{}) do def fetch_conversation(user, id) do with context when is_binary(context) <- conversation_id_to_context(id), - activities <- ActivityPub.fetch_activities_for_context(context), + activities <- ActivityPub.fetch_activities_for_context(context, %{"blocking_user" => user}), statuses <- activities |> activities_to_statuses(%{for: user}) do statuses diff --git a/test/notification_test.exs b/test/notification_test.exs index f50b3cb24..77fdb532f 100644 --- a/test/notification_test.exs +++ b/test/notification_test.exs @@ -20,4 +20,15 @@ test "notifies someone when they are directly addressed" do assert other_notification.activity_id == activity.id end end + + describe "create_notification" do + test "it doesn't create a notification for user if the user blocks the activity author" do + activity = insert(:note_activity) + author = User.get_by_ap_id(activity.data["actor"]) + user = insert(:user) + {:ok, user} = User.block(user, author) + + assert nil == Notification.create_notification(activity, user) + end + end end diff --git a/test/user_test.exs b/test/user_test.exs index ae9a48e74..151b9afc0 100644 --- a/test/user_test.exs +++ b/test/user_test.exs @@ -9,11 +9,6 @@ defmodule Pleroma.UserTest do import Ecto.Query test "ap_id returns the activity pub id for the user" do - host = - Application.get_env(:pleroma, Pleroma.Web.Endpoint) - |> Keyword.fetch!(:url) - |> Keyword.fetch!(:host) - user = UserBuilder.build expected_ap_id = "#{Pleroma.Web.base_url}/users/#{user.nickname}" @@ -213,7 +208,9 @@ test "gets all followers for a given user" do {:ok, res} = User.get_followers(user) - assert res == [follower_one, follower_two] + assert Enum.member?(res, follower_one) + assert Enum.member?(res, follower_two) + refute Enum.member?(res, not_follower) end test "gets all friends (followed users) for a given user" do @@ -229,7 +226,9 @@ test "gets all friends (followed users) for a given user" do followed_one = User.get_by_ap_id(followed_one.ap_id) followed_two = User.get_by_ap_id(followed_two.ap_id) - assert res == [followed_one, followed_two] + assert Enum.member?(res, followed_one) + assert Enum.member?(res, followed_two) + refute Enum.member?(res, not_followed) end end @@ -274,5 +273,28 @@ test "it sets the info->follower_count property" do assert user.info["follower_count"] == 1 end end + + describe "blocks" do + test "it blocks people" do + user = insert(:user) + blocked_user = insert(:user) + + refute User.blocks?(user, blocked_user) + + {:ok, user} = User.block(user, blocked_user) + + assert User.blocks?(user, blocked_user) + end + + test "it unblocks users" do + user = insert(:user) + blocked_user = insert(:user) + + {:ok, user} = User.block(user, blocked_user) + {:ok, user} = User.unblock(user, blocked_user) + + refute User.blocks?(user, blocked_user) + end + end end diff --git a/test/web/activity_pub/activity_pub_test.exs b/test/web/activity_pub/activity_pub_test.exs index a088e97be..a02740d5d 100644 --- a/test/web/activity_pub/activity_pub_test.exs +++ b/test/web/activity_pub/activity_pub_test.exs @@ -73,13 +73,40 @@ test "retrieves activities that have a given context" do {:ok, activity_two} = ActivityBuilder.insert(%{"type" => "Create", "context" => "2hu"}) {:ok, _activity_three} = ActivityBuilder.insert(%{"type" => "Create", "context" => "3hu"}) {:ok, _activity_four} = ActivityBuilder.insert(%{"type" => "Announce", "context" => "2hu"}) + activity_five = insert(:note_activity) + user = insert(:user) - activities = ActivityPub.fetch_activities_for_context("2hu") + {:ok, user} = User.block(user, %{ap_id: activity_five.data["actor"]}) + activities = ActivityPub.fetch_activities_for_context("2hu", %{"blocking_user" => user}) assert activities == [activity_two, activity] end end + test "doesn't return blocked activities" do + activity_one = insert(:note_activity) + activity_two = insert(:note_activity) + user = insert(:user) + {:ok, user} = User.block(user, %{ap_id: activity_one.data["actor"]}) + + activities = ActivityPub.fetch_activities([], %{"blocking_user" => user}) + + assert Enum.member?(activities, activity_two) + refute Enum.member?(activities, activity_one) + + {:ok, user} = User.unblock(user, %{ap_id: activity_one.data["actor"]}) + + activities = ActivityPub.fetch_activities([], %{"blocking_user" => user}) + + assert Enum.member?(activities, activity_two) + assert Enum.member?(activities, activity_one) + + activities = ActivityPub.fetch_activities([], %{"blocking_user" => nil}) + + assert Enum.member?(activities, activity_two) + assert Enum.member?(activities, activity_one) + end + describe "public fetch activities" do test "retrieves public activities" do %{public: public} = ActivityBuilder.public_and_non_public diff --git a/test/web/mastodon_api/account_view_test.exs b/test/web/mastodon_api/account_view_test.exs index aa74ed966..c62cb4f36 100644 --- a/test/web/mastodon_api/account_view_test.exs +++ b/test/web/mastodon_api/account_view_test.exs @@ -51,12 +51,13 @@ test "represent a relationship" do other_user = insert(:user) {:ok, user} = User.follow(user, other_user) + {:ok, user} = User.block(user, other_user) expected = %{ id: other_user.id, following: true, followed_by: false, - blocking: false, + blocking: true, muting: false, requested: false, domain_blocking: false diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs index cf09bc4b8..d118026eb 100644 --- a/test/web/mastodon_api/mastodon_api_controller_test.exs +++ b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -291,11 +291,43 @@ test "following / unfollowing a user", %{conn: conn} do assert id == other_user.id end - test "unimplemented block/mute endpoints" do + test "blocking / unblocking a user", %{conn: conn} do user = insert(:user) other_user = insert(:user) - ["block", "unblock", "mute", "unmute"] + conn = conn + |> assign(:user, user) + |> post("/api/v1/accounts/#{other_user.id}/block") + + assert %{"id" => id, "blocking" => true} = json_response(conn, 200) + + user = Repo.get(User, user.id) + conn = build_conn() + |> assign(:user, user) + |> post("/api/v1/accounts/#{other_user.id}/unblock") + + assert %{"id" => id, "blocking" => false} = json_response(conn, 200) + end + + test "getting a list of blocks", %{conn: conn} do + user = insert(:user) + other_user = insert(:user) + + {:ok, user} = User.block(user, other_user) + + conn = conn + |> assign(:user, user) + |> get("/api/v1/blocks") + + other_user_id = other_user.id + assert [%{"id" => ^other_user_id}] = json_response(conn, 200) + end + + test "unimplemented mute endpoints" do + user = insert(:user) + other_user = insert(:user) + + ["mute", "unmute"] |> Enum.each(fn(endpoint) -> conn = build_conn() |> assign(:user, user)