Merge branch 'issue/1354' into 'develop'

[#1354] fix remote follow

See merge request pleroma/pleroma!2079
This commit is contained in:
feld 2020-01-12 18:20:31 +00:00
commit bb5862ea2f
13 changed files with 409 additions and 326 deletions

View file

@ -264,6 +264,10 @@ defmodule Pleroma.Web.ActivityPub.Publisher do
"rel" => "self",
"type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"",
"href" => user.ap_id
},
%{
"rel" => "http://ostatus.org/schema/1.0/subscribe",
"template" => "#{Pleroma.Web.base_url()}/ostatus_subscribe?acct={uri}"
}
]
end

View file

@ -229,9 +229,9 @@ defmodule Pleroma.Web.Router do
pipe_through(:pleroma_html)
post("/main/ostatus", UtilController, :remote_subscribe)
get("/ostatus_subscribe", UtilController, :remote_follow)
get("/ostatus_subscribe", RemoteFollowController, :follow)
post("/ostatus_subscribe", UtilController, :do_remote_follow)
post("/ostatus_subscribe", RemoteFollowController, :do_follow)
end
scope "/api/pleroma", Pleroma.Web.TwitterAPI do

View file

@ -0,0 +1,11 @@
<%= if @error == :error do %>
<h2>Error fetching user</h2>
<% else %>
<h2>Remote follow</h2>
<img height="128" width="128" src="<%= avatar_url(@followee) %>">
<p><%= @followee.nickname %></p>
<%= form_for @conn, remote_follow_path(@conn, :do_follow), [as: "user"], fn f -> %>
<%= hidden_input f, :id, value: @followee.id %>
<%= submit "Authorize" %>
<% end %>
<% end %>

View file

@ -0,0 +1,14 @@
<%= if @error do %>
<h2><%= @error %></h2>
<% end %>
<h2>Log in to follow</h2>
<p><%= @followee.nickname %></p>
<img height="128" width="128" src="<%= avatar_url(@followee) %>">
<%= form_for @conn, remote_follow_path(@conn, :do_follow), [as: "authorization"], fn f -> %>
<%= text_input f, :name, placeholder: "Username", required: true %>
<br>
<%= password_input f, :password, placeholder: "Password", required: true %>
<br>
<%= hidden_input f, :id, value: @followee.id %>
<%= submit "Authorize" %>
<% end %>

View file

@ -1,11 +0,0 @@
<%= if @error == :error do %>
<h2>Error fetching user</h2>
<% else %>
<h2>Remote follow</h2>
<img width="128" height="128" src="<%= @avatar %>">
<p><%= @name %></p>
<%= form_for @conn, util_path(@conn, :do_remote_follow), [as: "user"], fn f -> %>
<%= hidden_input f, :id, value: @id %>
<%= submit "Authorize" %>
<% end %>
<% end %>

View file

@ -1,14 +0,0 @@
<%= if @error do %>
<h2><%= @error %></h2>
<% end %>
<h2>Log in to follow</h2>
<p><%= @name %></p>
<img height="128" width="128" src="<%= @avatar %>">
<%= form_for @conn, util_path(@conn, :do_remote_follow), [as: "authorization"], fn f -> %>
<%= text_input f, :name, placeholder: "Username" %>
<br>
<%= password_input f, :password, placeholder: "Password" %>
<br>
<%= hidden_input f, :id, value: @id %>
<%= submit "Authorize" %>
<% end %>

View file

@ -0,0 +1,112 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.RemoteFollowController do
use Pleroma.Web, :controller
require Logger
alias Pleroma.Activity
alias Pleroma.Object.Fetcher
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
alias Pleroma.Web.Auth.Authenticator
alias Pleroma.Web.CommonAPI
@status_types ["Article", "Event", "Note", "Video", "Page", "Question"]
# Note: follower can submit the form (with password auth) not being signed in (having no token)
plug(
OAuthScopesPlug,
%{fallback: :proceed_unauthenticated, scopes: ["follow", "write:follows"]}
when action in [:do_follow]
)
# GET /ostatus_subscribe
#
def follow(%{assigns: %{user: user}} = conn, %{"acct" => acct}) do
case is_status?(acct) do
true -> follow_status(conn, user, acct)
_ -> follow_account(conn, user, acct)
end
end
defp follow_status(conn, _user, acct) do
with {:ok, object} <- Fetcher.fetch_object_from_id(acct),
%Activity{id: activity_id} <- Activity.get_create_by_object_ap_id(object.data["id"]) do
redirect(conn, to: o_status_path(conn, :notice, activity_id))
else
error ->
handle_follow_error(conn, error)
end
end
defp follow_account(conn, user, acct) do
with {:ok, followee} <- User.get_or_fetch(acct) do
render(conn, follow_template(user), %{error: false, followee: followee, acct: acct})
else
{:error, _reason} ->
render(conn, follow_template(user), %{error: :error})
end
end
defp follow_template(%User{} = _user), do: "follow.html"
defp follow_template(_), do: "follow_login.html"
defp is_status?(acct) do
case Fetcher.fetch_and_contain_remote_object_from_id(acct) do
{:ok, %{"type" => type}} when type in @status_types ->
true
_ ->
false
end
end
# POST /ostatus_subscribe
#
def do_follow(%{assigns: %{user: %User{} = user}} = conn, %{"user" => %{"id" => id}}) do
with {:fetch_user, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
{:ok, _, _, _} <- CommonAPI.follow(user, followee) do
render(conn, "followed.html", %{error: false})
else
error ->
handle_follow_error(conn, error)
end
end
def do_follow(conn, %{"authorization" => %{"name" => _, "password" => _, "id" => id}}) do
with {:fetch_user, %User{} = followee} <- {:fetch_user, User.get_cached_by_id(id)},
{_, {:ok, user}, _} <- {:auth, Authenticator.get_user(conn), followee},
{:ok, _, _, _} <- CommonAPI.follow(user, followee) do
render(conn, "followed.html", %{error: false})
else
error ->
handle_follow_error(conn, error)
end
end
def do_follow(%{assigns: %{user: nil}} = conn, _) do
Logger.debug("Insufficient permissions: follow | write:follows.")
render(conn, "followed.html", %{error: "Insufficient permissions: follow | write:follows."})
end
defp handle_follow_error(conn, {:auth, _, followee} = _) do
render(conn, "follow_login.html", %{error: "Wrong username or password", followee: followee})
end
defp handle_follow_error(conn, {:fetch_user, error} = _) do
Logger.debug("Remote follow failed with error #{inspect(error)}")
render(conn, "followed.html", %{error: "Could not find user"})
end
defp handle_follow_error(conn, {:error, "Could not follow user:" <> _} = _) do
render(conn, "followed.html", %{error: "Error following account"})
end
defp handle_follow_error(conn, error) do
Logger.debug("Remote follow failed with error #{inspect(error)}")
render(conn, "followed.html", %{error: "Something went wrong."})
end
end

View file

@ -7,12 +7,10 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
require Logger
alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Emoji
alias Pleroma.Healthcheck
alias Pleroma.Notification
alias Pleroma.Plugs.AuthenticationPlug
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
alias Pleroma.Web
@ -84,105 +82,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end
end
def remote_follow(%{assigns: %{user: user}} = conn, %{"acct" => acct}) do
if is_status?(acct) do
{:ok, object} = Pleroma.Object.Fetcher.fetch_object_from_id(acct)
%Activity{id: activity_id} = Activity.get_create_by_object_ap_id(object.data["id"])
redirect(conn, to: "/notice/#{activity_id}")
else
with {:ok, followee} <- User.get_or_fetch(acct) do
conn
|> render(follow_template(user), %{
error: false,
acct: acct,
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", "Event", "Note", "Video", "Page", "Question"] ->
true
_ ->
false
end
end
def do_remote_follow(%{assigns: %{user: user}} = conn, %{"user" => %{"id" => id}})
when not is_nil(user) do
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} ->
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)}")
render(conn, "followed.html", %{error: "Something went wrong."})
end
end
# Note: "id" is the id of followee user, disregard incorrect placing under "authorization"
def do_remote_follow(conn, %{
"authorization" => %{"name" => username, "password" => password, "id" => id}
}) do
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: "Error following account"})
{:auth, _, followee} ->
conn
|> render("follow_login.html", %{
error: "Wrong username or password",
id: id,
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: nil}} = conn, _) do
render(conn, "followed.html", %{error: "Insufficient permissions: follow | write:follows."})
end
def do_remote_follow(conn, _) do
render(conn, "followed.html", %{error: "Something went wrong."})
end
def notifications_read(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do
with {:ok, _} <- Notification.read_one(user, notification_id) do
json(conn, %{status: "success"})

View file

@ -0,0 +1,10 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.RemoteFollowView do
use Pleroma.Web, :view
import Phoenix.HTML.Form
defdelegate avatar_url(user), to: Pleroma.User
end

View file

@ -23,6 +23,27 @@ defmodule Pleroma.Web.ActivityPub.PublisherTest do
:ok
end
describe "gather_webfinger_links/1" do
test "it returns links" do
user = insert(:user)
expected_links = [
%{"href" => user.ap_id, "rel" => "self", "type" => "application/activity+json"},
%{
"href" => user.ap_id,
"rel" => "self",
"type" => "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
},
%{
"rel" => "http://ostatus.org/schema/1.0/subscribe",
"template" => "#{Pleroma.Web.base_url()}/ostatus_subscribe?acct={uri}"
}
]
assert expected_links == Publisher.gather_webfinger_links(user)
end
end
describe "determine_inbox/2" do
test "it returns sharedInbox for messages involving as:Public in to" do
user =

View file

@ -0,0 +1,235 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.TwitterAPI.RemoteFollowControllerTest do
use Pleroma.Web.ConnCase
alias Pleroma.User
alias Pleroma.Web.CommonAPI
import ExUnit.CaptureLog
import Pleroma.Factory
setup do
Tesla.Mock.mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
clear_config([:instance])
clear_config([:frontend_configurations, :pleroma_fe])
clear_config([:user, :deny_follow_blocked])
describe "GET /ostatus_subscribe - remote_follow/2" do
test "adds status to pleroma instance if the `acct` is a status", %{conn: conn} do
assert conn
|> get(
remote_follow_path(conn, :follow, %{
acct: "https://mastodon.social/users/emelie/statuses/101849165031453009"
})
)
|> redirected_to() =~ "/notice/"
end
test "show follow account page if the `acct` is a account link", %{conn: conn} do
response =
conn
|> get(remote_follow_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"}))
|> html_response(200)
assert response =~ "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(remote_follow_path(conn, :follow, %{acct: "https://mastodon.social/users/emelie"}))
|> html_response(200)
assert response =~ "Remote follow"
end
test "show follow page with error when user cannot fecth by `acct` link", %{conn: conn} do
user = insert(:user)
assert capture_log(fn ->
response =
conn
|> assign(:user, user)
|> get(
remote_follow_path(conn, :follow, %{
acct: "https://mastodon.social/users/not_found"
})
)
|> html_response(200)
assert response =~ "Error fetching user"
end) =~ "Object has been deleted"
end
end
describe "POST /ostatus_subscribe - do_follow/2 with assigned user " do
test "required `follow | write:follows` scope", %{conn: conn} do
user = insert(:user)
user2 = insert(:user)
read_token = insert(:oauth_token, user: user, scopes: ["read"])
assert capture_log(fn ->
response =
conn
|> assign(:user, user)
|> assign(:token, read_token)
|> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}})
|> response(200)
assert response =~ "Error following account"
end) =~ "Insufficient permissions: follow | write:follows."
end
test "follows user", %{conn: conn} do
user = insert(:user)
user2 = insert(:user)
response =
conn
|> assign(:user, user)
|> assign(:token, insert(:oauth_token, user: user, scopes: ["write:follows"]))
|> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}})
|> response(200)
assert response =~ "Account followed!"
assert user2.follower_address in User.following(user)
end
test "returns error when user is deactivated", %{conn: conn} do
user = insert(:user, deactivated: true)
user2 = insert(:user)
response =
conn
|> assign(:user, user)
|> post(remote_follow_path(conn, :do_follow), %{"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_block} = Pleroma.User.block(user2, user)
response =
conn
|> assign(:user, user)
|> post(remote_follow_path(conn, :do_follow), %{"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(remote_follow_path(conn, :do_follow), %{"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))
|> assign(:token, insert(:oauth_token, user: user, scopes: ["write:follows"]))
|> post(remote_follow_path(conn, :do_follow), %{"user" => %{"id" => user2.id}})
|> response(200)
assert response =~ "Account followed!"
end
end
describe "POST /ostatus_subscribe - follow/2 without assigned user " do
test "follows", %{conn: conn} do
user = insert(:user)
user2 = insert(:user)
response =
conn
|> post(remote_follow_path(conn, :do_follow), %{
"authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id}
})
|> response(200)
assert response =~ "Account followed!"
assert user2.follower_address in User.following(user)
end
test "returns error when followee not found", %{conn: conn} do
user = insert(:user)
response =
conn
|> post(remote_follow_path(conn, :do_follow), %{
"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(remote_follow_path(conn, :do_follow), %{
"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(remote_follow_path(conn, :do_follow), %{
"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_block} = Pleroma.User.block(user2, user)
response =
conn
|> post(remote_follow_path(conn, :do_follow), %{
"authorization" => %{"name" => user.nickname, "password" => "test", "id" => user2.id}
})
|> response(200)
assert response =~ "Error following account"
end
end
end

View file

@ -319,204 +319,6 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
end
end
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(
conn,
"/ostatus_subscribe?acct=https://mastodon.social/users/emelie/statuses/101849165031453009"
)
assert redirected_to(conn) =~ "/notice/"
end
test "show follow account page if the `acct` is a account link", %{conn: conn} do
response =
get(
conn,
"/ostatus_subscribe?acct=https://mastodon.social/users/emelie"
)
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)
assert capture_log(fn ->
response =
conn
|> assign(:user, user)
|> get("/ostatus_subscribe?acct=https://mastodon.social/users/not_found")
assert html_response(response, 200) =~ "Error fetching user"
end) =~ "Object has been deleted"
end
end
describe "POST /ostatus_subscribe - do_remote_follow/2 with assigned user" do
setup do: oauth_access(["follow"])
test "follows user", %{user: user, conn: conn} do
user2 = insert(:user)
response =
conn
|> post("/ostatus_subscribe", %{"user" => %{"id" => user2.id}})
|> response(200)
assert response =~ "Account followed!"
assert user2.follower_address in User.following(user)
end
test "returns error when user is deactivated" do
user = insert(:user, deactivated: true)
user2 = insert(:user)
response =
build_conn()
|> assign(:user, user)
|> assign(:token, insert(:oauth_token, user: user, scopes: ["follow"]))
|> post("/ostatus_subscribe", %{"user" => %{"id" => user2.id}})
|> response(200)
assert response =~ "Error following account"
end
test "returns error when user is blocked", %{user: user, conn: conn} do
Pleroma.Config.put([:user, :deny_follow_blocked], true)
user2 = insert(:user)
{:ok, _user_block} = Pleroma.User.block(user2, user)
response =
conn
|> post("/ostatus_subscribe", %{"user" => %{"id" => user2.id}})
|> response(200)
assert response =~ "Error following account"
end
test "returns error on insufficient permissions", %{user: user, conn: conn} do
user2 = insert(:user)
for token <- [nil, insert(:oauth_token, user: user, scopes: ["read"])] do
response =
conn
|> assign(:token, token)
|> post("/ostatus_subscribe", %{"user" => %{"id" => user2.id}})
|> response(200)
assert response =~ "Error following account"
end
end
test "returns error when followee not found", %{conn: conn} do
response =
conn
|> post("/ostatus_subscribe", %{"user" => %{"id" => "jimm"}})
|> response(200)
assert response =~ "Error following account"
end
test "returns success result when user already in followers", %{user: user, conn: conn} do
user2 = insert(:user)
{:ok, _, _, _} = CommonAPI.follow(user, user2)
response =
conn
|> 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 User.following(user)
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_block} = 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
clear_config([:instance, :healthcheck])