logo

pleroma

My custom branche(s) on git.pleroma.social/pleroma/pleroma
commit: a6e0c31518ff60bd5e147941446779d719afaeb2
parent: 01240fd8b6654f74aa7273fa624e927870629fa0
Author: lambda <pleromagit@rogerbraun.net>
Date:   Tue, 12 Jun 2018 07:18:19 +0000

Merge branch 'feature/locked-accounts-part-1' into 'develop'

locked accounts

See merge request pleroma/pleroma!180

Diffstat:

Alib/mix/tasks/set_locked.ex30++++++++++++++++++++++++++++++
Mlib/pleroma/user.ex46++++++++++++++++++++++++++++++++++++++++++++++
Mlib/pleroma/web/activity_pub/activity_pub.ex1+
Mlib/pleroma/web/activity_pub/transmogrifier.ex66+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mlib/pleroma/web/activity_pub/utils.ex25+++++++++++++++++++++++--
Mlib/pleroma/web/mastodon_api/mastodon_api_controller.ex62++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/pleroma/web/router.ex9++++++++-
Mlib/pleroma/web/twitter_api/twitter_api_controller.ex63+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mlib/pleroma/web/twitter_api/views/user_view.ex3++-
Mtest/web/mastodon_api/mastodon_api_controller_test.exs68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/web/twitter_api/twitter_api_controller_test.exs100+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mtest/web/twitter_api/views/user_view_test.exs12++++++++----
12 files changed, 474 insertions(+), 11 deletions(-)

diff --git a/lib/mix/tasks/set_locked.ex b/lib/mix/tasks/set_locked.ex @@ -0,0 +1,30 @@ +defmodule Mix.Tasks.SetLocked do + use Mix.Task + import Mix.Ecto + alias Pleroma.{Repo, User} + + @shortdoc "Set locked status" + def run([nickname | rest]) do + ensure_started(Repo, []) + + locked = + case rest do + [locked] -> locked == "true" + _ -> true + end + + with %User{local: true} = user <- User.get_by_nickname(nickname) do + info = + user.info + |> Map.put("locked", !!locked) + + cng = User.info_changeset(user, %{info: info}) + user = Repo.update!(cng) + + IO.puts("locked status of #{nickname}: #{user.info["locked"]}") + else + _ -> + IO.puts("No local user #{nickname}") + end + end +end diff --git a/lib/pleroma/user.ex b/lib/pleroma/user.ex @@ -197,6 +197,14 @@ defmodule Pleroma.User do end end + def maybe_follow(%User{} = follower, %User{info: info} = followed) do + if not following?(follower, followed) do + follow(follower, followed) + else + {:ok, follower} + end + end + def follow(%User{} = follower, %User{info: info} = followed) do ap_followers = followed.follower_address @@ -252,6 +260,10 @@ defmodule Pleroma.User do Enum.member?(follower.following, followed.follower_address) end + def locked?(%User{} = user) do + user.info["locked"] || false + end + def get_by_ap_id(ap_id) do Repo.get_by(User, ap_id: ap_id) end @@ -349,6 +361,40 @@ defmodule Pleroma.User do {:ok, Repo.all(q)} end + def get_follow_requests_query(%User{} = user) do + from( + a in Activity, + where: + fragment( + "? ->> 'type' = 'Follow'", + a.data + ), + where: + fragment( + "? ->> 'state' = 'pending'", + a.data + ), + where: + fragment( + "? @> ?", + a.data, + ^%{"object" => user.ap_id} + ) + ) + end + + def get_follow_requests(%User{} = user) do + q = get_follow_requests_query(user) + reqs = Repo.all(q) + + users = + Enum.map(reqs, fn req -> req.actor end) + |> Enum.uniq() + |> Enum.map(fn ap_id -> get_by_ap_id(ap_id) end) + + {:ok, users} + end + def increase_note_count(%User{} = user) do note_count = (user.info["note_count"] || 0) + 1 new_info = Map.put(user.info, "note_count", note_count) diff --git a/lib/pleroma/web/activity_pub/activity_pub.ex b/lib/pleroma/web/activity_pub/activity_pub.ex @@ -214,6 +214,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do def unfollow(follower, followed, activity_id \\ nil, local \\ true) do with %Activity{} = follow_activity <- fetch_latest_follow(follower, followed), + {:ok, follow_activity} <- update_follow_state(follow_activity, "cancelled"), unfollow_data <- make_unfollow_data(follower, followed, follow_activity, activity_id), {:ok, activity} <- insert(unfollow_data, local), :ok <- maybe_federate(activity) do diff --git a/lib/pleroma/web/activity_pub/transmogrifier.ex b/lib/pleroma/web/activity_pub/transmogrifier.ex @@ -137,9 +137,17 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do with %User{local: true} = followed <- User.get_cached_by_ap_id(followed), %User{} = follower <- User.get_or_fetch_by_ap_id(follower), {:ok, activity} <- ActivityPub.follow(follower, followed, id, false) do - ActivityPub.accept(%{to: [follower.ap_id], actor: followed.ap_id, object: data, local: true}) + if not User.locked?(followed) do + ActivityPub.accept(%{ + to: [follower.ap_id], + actor: followed.ap_id, + object: data, + local: true + }) + + User.follow(follower, followed) + end - User.follow(follower, followed) {:ok, activity} else _e -> :error @@ -252,7 +260,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do {:ok, new_user_data} = ActivityPub.user_data_from_user_object(object) banner = new_user_data[:info]["banner"] - locked = new_user_data[:info]["locked"] + locked = new_user_data[:info]["locked"] || false update_data = new_user_data @@ -432,6 +440,58 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do {:ok, data} end + # Mastodon Accept/Reject requires a non-normalized object containing the actor URIs, + # because of course it does. + def prepare_outgoing(%{"type" => "Accept"} = data) do + follow_activity_id = + if is_binary(data["object"]) do + data["object"] + else + data["object"]["id"] + end + + with follow_activity <- Activity.get_by_ap_id(follow_activity_id) do + object = %{ + "actor" => follow_activity.actor, + "object" => follow_activity.data["object"], + "id" => follow_activity.data["id"], + "type" => "Follow" + } + + data = + data + |> Map.put("object", object) + |> Map.put("@context", "https://www.w3.org/ns/activitystreams") + + {:ok, data} + end + end + + def prepare_outgoing(%{"type" => "Reject"} = data) do + follow_activity_id = + if is_binary(data["object"]) do + data["object"] + else + data["object"]["id"] + end + + with follow_activity <- Activity.get_by_ap_id(follow_activity_id) do + object = %{ + "actor" => follow_activity.actor, + "object" => follow_activity.data["object"], + "id" => follow_activity.data["id"], + "type" => "Follow" + } + + data = + data + |> Map.put("object", object) + |> Map.put("@context", "https://www.w3.org/ns/activitystreams") + + {:ok, data} + end + end + def prepare_outgoing(%{"type" => _type} = data) do data = data diff --git a/lib/pleroma/web/activity_pub/utils.ex b/lib/pleroma/web/activity_pub/utils.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.ActivityPub.Utils do alias Pleroma.Web.Endpoint alias Ecto.{Changeset, UUID} import Ecto.Query + require Logger # Some implementations send the actor URI as the actor field, others send the entire actor object, # so figure out what the actor's URI is based on what we have. @@ -217,9 +218,26 @@ defmodule Pleroma.Web.ActivityPub.Utils do #### Follow-related helpers @doc """ + Updates a follow activity's state (for locked accounts). + """ + def update_follow_state(%Activity{} = activity, state) do + with new_data <- + activity.data + |> Map.put("state", state), + changeset <- Changeset.change(activity, data: new_data), + {:ok, activity} <- Repo.update(changeset) do + {:ok, activity} + end + end + + @doc """ Makes a follow activity data for the given follower and followed """ - def make_follow_data(%User{ap_id: follower_id}, %User{ap_id: followed_id}, activity_id) do + def make_follow_data( + %User{ap_id: follower_id}, + %User{ap_id: followed_id} = followed, + activity_id + ) do data = %{ "type" => "Follow", "actor" => follower_id, @@ -228,7 +246,10 @@ defmodule Pleroma.Web.ActivityPub.Utils do "object" => followed_id } - if activity_id, do: Map.put(data, "id", activity_id), else: data + data = if activity_id, do: Map.put(data, "id", activity_id), else: data + data = if User.locked?(followed), do: Map.put(data, "state", "pending"), else: data + + data end def fetch_latest_follow(%User{ap_id: follower_id}, %User{ap_id: followed_id}) do diff --git a/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex b/lib/pleroma/web/mastodon_api/mastodon_api_controller.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do alias Pleroma.Web alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView, ListView} alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Utils alias Pleroma.Web.{CommonAPI, OStatus} alias Pleroma.Web.OAuth.{Authorization, Token, App} alias Comeonin.Pbkdf2 @@ -71,6 +72,20 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do user end + user = + if locked = params["locked"] do + with locked <- locked == "true", + new_info <- Map.put(user.info, "locked", locked), + change <- User.info_changeset(user, %{info: new_info}), + {:ok, user} <- User.update_and_set_cache(change) do + user + else + _e -> user + end + else + user + end + with changeset <- User.update_changeset(user, params), {:ok, user} <- User.update_and_set_cache(changeset) do if original_user != user do @@ -476,6 +491,53 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do end end + def follow_requests(%{assigns: %{user: followed}} = conn, _params) do + with {:ok, follow_requests} <- User.get_follow_requests(followed) do + render(conn, AccountView, "accounts.json", %{users: follow_requests, as: :user}) + end + end + + def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do + with %User{} = follower <- Repo.get(User, id), + {:ok, follower} <- User.maybe_follow(follower, followed), + %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), + {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"), + {:ok, _activity} <- + ActivityPub.accept(%{ + to: [follower.ap_id], + actor: followed.ap_id, + object: follow_activity.data["id"], + type: "Accept" + }) do + render(conn, AccountView, "relationship.json", %{user: followed, target: follower}) + else + {:error, message} -> + conn + |> put_resp_content_type("application/json") + |> send_resp(403, Jason.encode!(%{"error" => message})) + end + end + + def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do + with %User{} = follower <- Repo.get(User, id), + %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), + {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"), + {:ok, _activity} <- + ActivityPub.reject(%{ + to: [follower.ap_id], + actor: followed.ap_id, + object: follow_activity.data["id"], + type: "Reject" + }) do + render(conn, AccountView, "relationship.json", %{user: followed, target: follower}) + else + {:error, message} -> + conn + |> put_resp_content_type("application/json") + |> send_resp(403, Jason.encode!(%{"error" => message})) + end + end + def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do with %User{} = followed <- Repo.get(User, id), {:ok, follower} <- User.maybe_direct_follow(follower, followed), diff --git a/lib/pleroma/web/router.ex b/lib/pleroma/web/router.ex @@ -97,11 +97,14 @@ defmodule Pleroma.Web.Router do post("/accounts/:id/mute", MastodonAPIController, :relationship_noop) post("/accounts/:id/unmute", MastodonAPIController, :relationship_noop) + get("/follow_requests", MastodonAPIController, :follow_requests) + post("/follow_requests/:id/authorize", MastodonAPIController, :authorize_follow_request) + post("/follow_requests/:id/reject", MastodonAPIController, :reject_follow_request) + post("/follows", MastodonAPIController, :follow) get("/blocks", MastodonAPIController, :blocks) - get("/follow_requests", MastodonAPIController, :empty_array) get("/mutes", MastodonAPIController, :empty_array) get("/timelines/home", MastodonAPIController, :home_timeline) @@ -243,6 +246,10 @@ defmodule Pleroma.Web.Router do post("/statuses/retweet/:id", TwitterAPI.Controller, :retweet) post("/statuses/destroy/:id", TwitterAPI.Controller, :delete_post) + get("/pleroma/friend_requests", TwitterAPI.Controller, :friend_requests) + post("/pleroma/friendships/approve", TwitterAPI.Controller, :approve_friend_request) + post("/pleroma/friendships/deny", TwitterAPI.Controller, :deny_friend_request) + post("/friendships/create", TwitterAPI.Controller, :follow) post("/friendships/destroy", TwitterAPI.Controller, :unfollow) post("/blocks/create", TwitterAPI.Controller, :block) diff --git a/lib/pleroma/web/twitter_api/twitter_api_controller.ex b/lib/pleroma/web/twitter_api/twitter_api_controller.ex @@ -4,6 +4,7 @@ defmodule Pleroma.Web.TwitterAPI.Controller do alias Pleroma.Web.CommonAPI alias Pleroma.{Repo, Activity, User, Notification} alias Pleroma.Web.ActivityPub.ActivityPub + alias Pleroma.Web.ActivityPub.Utils alias Ecto.Changeset require Logger @@ -331,6 +332,54 @@ defmodule Pleroma.Web.TwitterAPI.Controller do end end + def friend_requests(conn, params) do + with {:ok, user} <- TwitterAPI.get_user(conn.assigns[:user], params), + {:ok, friend_requests} <- User.get_follow_requests(user) do + render(conn, UserView, "index.json", %{users: friend_requests, for: conn.assigns[:user]}) + else + _e -> bad_request_reply(conn, "Can't get friend requests") + end + end + + def approve_friend_request(conn, %{"user_id" => uid} = params) do + with followed <- conn.assigns[:user], + uid when is_number(uid) <- String.to_integer(uid), + %User{} = follower <- Repo.get(User, uid), + {:ok, follower} <- User.maybe_follow(follower, followed), + %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), + {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "accept"), + {:ok, _activity} <- + ActivityPub.accept(%{ + to: [follower.ap_id], + actor: followed.ap_id, + object: follow_activity.data["id"], + type: "Accept" + }) do + render(conn, UserView, "show.json", %{user: follower, for: followed}) + else + e -> bad_request_reply(conn, "Can't approve user: #{inspect(e)}") + end + end + + def deny_friend_request(conn, %{"user_id" => uid} = params) do + with followed <- conn.assigns[:user], + uid when is_number(uid) <- String.to_integer(uid), + %User{} = follower <- Repo.get(User, uid), + %Activity{} = follow_activity <- Utils.fetch_latest_follow(follower, followed), + {:ok, follow_activity} <- Utils.update_follow_state(follow_activity, "reject"), + {:ok, _activity} <- + ActivityPub.reject(%{ + to: [follower.ap_id], + actor: followed.ap_id, + object: follow_activity.data["id"], + type: "Reject" + }) do + render(conn, UserView, "show.json", %{user: follower, for: followed}) + else + e -> bad_request_reply(conn, "Can't deny user: #{inspect(e)}") + end + end + def friends_ids(%{assigns: %{user: user}} = conn, _params) do with {:ok, friends} <- User.get_friends(user) do ids = @@ -357,6 +406,20 @@ defmodule Pleroma.Web.TwitterAPI.Controller do params end + user = + if locked = params["locked"] do + with locked <- locked == "true", + new_info <- Map.put(user.info, "locked", locked), + change <- User.info_changeset(user, %{info: new_info}), + {:ok, user} <- User.update_and_set_cache(change) do + user + else + _e -> user + end + else + user + end + with changeset <- User.update_changeset(user, params), {:ok, user} <- User.update_and_set_cache(changeset) do CommonAPI.update(user) diff --git a/lib/pleroma/web/twitter_api/views/user_view.ex b/lib/pleroma/web/twitter_api/views/user_view.ex @@ -51,7 +51,8 @@ defmodule Pleroma.Web.TwitterAPI.UserView do "statusnet_profile_url" => user.ap_id, "cover_photo" => User.banner_url(user) |> MediaProxy.url(), "background_image" => image_url(user.info["background"]) |> MediaProxy.url(), - "is_local" => user.local + "is_local" => user.local, + "locked" => !!user.info["locked"] } if assigns[:token] do diff --git a/test/web/mastodon_api/mastodon_api_controller_test.exs b/test/web/mastodon_api/mastodon_api_controller_test.exs @@ -4,6 +4,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do alias Pleroma.Web.TwitterAPI.TwitterAPI alias Pleroma.{Repo, User, Activity, Notification} alias Pleroma.Web.{OStatus, CommonAPI} + alias Pleroma.Web.ActivityPub.ActivityPub import Pleroma.Factory import ExUnit.CaptureLog @@ -644,6 +645,73 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIControllerTest do end end + describe "locked accounts" do + test "/api/v1/follow_requests works" do + user = insert(:user, %{info: %{"locked" => true}}) + other_user = insert(:user) + + {:ok, activity} = ActivityPub.follow(other_user, user) + + user = Repo.get(User, user.id) + other_user = Repo.get(User, other_user.id) + + assert User.following?(other_user, user) == false + + conn = + build_conn() + |> assign(:user, user) + |> get("/api/v1/follow_requests") + + assert [relationship] = json_response(conn, 200) + assert to_string(other_user.id) == relationship["id"] + end + + test "/api/v1/follow_requests/:id/authorize works" do + user = insert(:user, %{info: %{"locked" => true}}) + other_user = insert(:user) + + {:ok, activity} = ActivityPub.follow(other_user, user) + + user = Repo.get(User, user.id) + other_user = Repo.get(User, other_user.id) + + assert User.following?(other_user, user) == false + + conn = + build_conn() + |> assign(:user, user) + |> post("/api/v1/follow_requests/#{other_user.id}/authorize") + + assert relationship = json_response(conn, 200) + assert to_string(other_user.id) == relationship["id"] + + user = Repo.get(User, user.id) + other_user = Repo.get(User, other_user.id) + + assert User.following?(other_user, user) == true + end + + test "/api/v1/follow_requests/:id/reject works" do + user = insert(:user, %{info: %{"locked" => true}}) + other_user = insert(:user) + + {:ok, activity} = ActivityPub.follow(other_user, user) + + conn = + build_conn() + |> assign(:user, user) + |> post("/api/v1/follow_requests/#{other_user.id}/reject") + + assert relationship = json_response(conn, 200) + assert to_string(other_user.id) == relationship["id"] + + user = Repo.get(User, user.id) + other_user = Repo.get(User, other_user.id) + + assert User.following?(other_user, user) == false + end + end + test "account fetching", %{conn: conn} do user = insert(:user) diff --git a/test/web/twitter_api/twitter_api_controller_test.exs b/test/web/twitter_api/twitter_api_controller_test.exs @@ -762,6 +762,38 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user}) end + + test "it locks an account", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> post("/api/account/update_profile.json", %{ + "locked" => "true" + }) + + user = Repo.get!(User, user.id) + assert user.info["locked"] == true + + assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user}) + end + + test "it unlocks an account", %{conn: conn} do + user = insert(:user) + + conn = + conn + |> assign(:user, user) + |> post("/api/account/update_profile.json", %{ + "locked" => "false" + }) + + user = Repo.get!(User, user.id) + assert user.info["locked"] == false + + assert json_response(conn, 200) == UserView.render("user.json", %{user: user, for: user}) + end end defp valid_user(_context) do @@ -926,4 +958,72 @@ defmodule Pleroma.Web.TwitterAPI.ControllerTest do :timer.sleep(1000) end end + + describe "GET /api/pleroma/friend_requests" do + test "it lists friend requests" do + user = insert(:user, %{info: %{"locked" => true}}) + other_user = insert(:user) + + {:ok, activity} = ActivityPub.follow(other_user, user) + + user = Repo.get(User, user.id) + other_user = Repo.get(User, other_user.id) + + assert User.following?(other_user, user) == false + + conn = + build_conn() + |> assign(:user, user) + |> get("/api/pleroma/friend_requests") + + assert [relationship] = json_response(conn, 200) + assert other_user.id == relationship["id"] + end + end + + describe "POST /api/pleroma/friendships/approve" do + test "it approves a friend request" do + user = insert(:user, %{info: %{"locked" => true}}) + other_user = insert(:user) + + {:ok, activity} = ActivityPub.follow(other_user, user) + + user = Repo.get(User, user.id) + other_user = Repo.get(User, other_user.id) + + assert User.following?(other_user, user) == false + + conn = + build_conn() + |> assign(:user, user) + |> post("/api/pleroma/friendships/approve", %{"user_id" => to_string(other_user.id)}) + + assert relationship = json_response(conn, 200) + assert other_user.id == relationship["id"] + assert relationship["follows_you"] == true + end + end + + describe "POST /api/pleroma/friendships/deny" do + test "it denies a friend request" do + user = insert(:user, %{info: %{"locked" => true}}) + other_user = insert(:user) + + {:ok, activity} = ActivityPub.follow(other_user, user) + + user = Repo.get(User, user.id) + other_user = Repo.get(User, other_user.id) + + assert User.following?(other_user, user) == false + + conn = + build_conn() + |> assign(:user, user) + |> post("/api/pleroma/friendships/deny", %{"user_id" => to_string(other_user.id)}) + + assert relationship = json_response(conn, 200) + assert other_user.id == relationship["id"] + assert relationship["follows_you"] == false + end + end end diff --git a/test/web/twitter_api/views/user_view_test.exs b/test/web/twitter_api/views/user_view_test.exs @@ -59,7 +59,8 @@ defmodule Pleroma.Web.TwitterAPI.UserViewTest do "statusnet_profile_url" => user.ap_id, "cover_photo" => banner, "background_image" => nil, - "is_local" => true + "is_local" => true, + "locked" => false } assert represented == UserView.render("show.json", %{user: user}) @@ -94,7 +95,8 @@ defmodule Pleroma.Web.TwitterAPI.UserViewTest do "statusnet_profile_url" => user.ap_id, "cover_photo" => banner, "background_image" => nil, - "is_local" => true + "is_local" => true, + "locked" => false } assert represented == UserView.render("show.json", %{user: user, for: follower}) @@ -130,7 +132,8 @@ defmodule Pleroma.Web.TwitterAPI.UserViewTest do "statusnet_profile_url" => follower.ap_id, "cover_photo" => banner, "background_image" => nil, - "is_local" => true + "is_local" => true, + "locked" => false } assert represented == UserView.render("show.json", %{user: follower, for: user}) @@ -173,7 +176,8 @@ defmodule Pleroma.Web.TwitterAPI.UserViewTest do "statusnet_profile_url" => user.ap_id, "cover_photo" => banner, "background_image" => nil, - "is_local" => true + "is_local" => true, + "locked" => false } blocker = Repo.get(User, blocker.id)