- MediaProxy: Parse name from content disposition headers even for non-whitelisted types
- MediaProxy: S3 link encoding
- Rich Media: Reject any data which cannot be explicitly encoded into JSON
+- Pleroma API: Importing follows from Mastodon 2.8+
- Mastodon API: `/api/v1/favourites` serving only public activities
- Mastodon API: Reblogs having `in_reply_to_id` - `null` even when they are replies
- Mastodon API: Streaming API broadcasting wrong activity id
- Mastodon API: 500 errors when requesting a card for a private conversation
+- Mastodon API: Handling of `reblogs` in `/api/v1/accounts/:id/follow`
+- Mastodon API: Correct `reblogged`, `favourited`, and `bookmarked` values in the reblog status JSON
## [0.9.9999] - 2019-04-05
### Security
Additional parameters can be added to the JSON body/Form data:
- `preview`: boolean, if set to `true` the post won't be actually posted, but the status entitiy would still be rendered back. This could be useful for previewing rich text/custom emoji, for example.
+- `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.
alias Pleroma.Pagination
alias Pleroma.ScheduledActivity
alias Pleroma.User
+ alias Pleroma.Web.CommonAPI
+
+ def follow(follower, followed, params \\ %{}) do
+ options = cast_params(params)
+ reblogs = options[:reblogs]
+
+ result =
+ if not User.following?(follower, followed) do
+ CommonAPI.follow(follower, followed)
+ else
+ {:ok, follower, followed, nil}
+ end
+
+ with {:ok, follower, followed, _} <- result do
+ reblogs
+ |> case do
+ false -> CommonAPI.hide_reblogs(follower, followed)
+ _ -> CommonAPI.show_reblogs(follower, followed)
+ end
+ |> case do
+ {:ok, follower} -> {:ok, follower}
+ _ -> {:ok, follower}
+ end
+ end
+ end
def get_followers(user, params \\ %{}) do
user
defp cast_params(params) do
param_types = %{
- exclude_types: {:array, :string}
+ exclude_types: {:array, :string},
+ reblogs: :boolean
}
changeset = cast({%{}, param_types}, params, Map.keys(param_types))
end
def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
- with %Activity{} = activity <- Activity.get_by_id(id),
+ with %Activity{} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.visible_for_user?(activity, user) do
conn
|> put_view(StatusView)
end
def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
- with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
+ with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
+ %Activity{} = announce <- Activity.normalize(announce.data) do
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: announce, for: user, as: :activity})
def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
- %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
+ %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
conn
|> put_view(StatusView)
|> try_render("status.json", %{activity: activity, for: user, as: :activity})
def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
{_, true} <- {:followed, follower.id != followed.id},
- false <- User.following?(follower, followed),
- {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
+ {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
conn
|> put_view(AccountView)
|> render("relationship.json", %{user: follower, target: followed})
{:followed, _} ->
{:error, :not_found}
- true ->
- followed = User.get_cached_by_id(id)
-
- {:ok, follower} =
- case conn.params["reblogs"] do
- true -> CommonAPI.show_reblogs(follower, followed)
- false -> CommonAPI.hide_reblogs(follower, followed)
- end
-
- conn
- |> put_view(AccountView)
- |> render("relationship.json", %{user: follower, target: followed})
-
{:error, message} ->
conn
|> put_resp_content_type("application/json")
reblogged_activity = Activity.get_create_by_object_ap_id(object)
reblogged = render("status.json", Map.put(opts, :activity, reblogged_activity))
+ activity_object = Object.normalize(activity)
+ favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || [])
+ bookmarked = opts[:for] && activity_object.data["id"] in opts[:for].bookmarks
+
mentions =
activity.recipients
|> Enum.map(fn ap_id -> User.get_cached_by_ap_id(ap_id) end)
replies_count: 0,
favourites_count: 0,
reblogged: reblogged?(reblogged_activity, opts[:for]),
- favourited: false,
- bookmarked: false,
+ favourited: present?(favorited),
+ bookmarked: present?(bookmarked),
muted: false,
pinned: pinned?(activity, user),
sensitive: false,
action_fallback(Pleroma.Web.OAuth.FallbackController)
+ # Note: this definition is only called from error-handling methods with `conn.params` as 2nd arg
+ def authorize(conn, %{"authorization" => _} = params) do
+ {auth_attrs, params} = Map.pop(params, "authorization")
+ authorize(conn, Map.merge(params, auth_attrs))
+ end
+
def authorize(%{assigns: %{token: %Token{} = token}} = conn, params) do
if ControllerHelper.truthy_param?(params["force_login"]) do
do_authorize(conn, params)
def authorize(conn, params), do: do_authorize(conn, params)
- defp do_authorize(conn, %{"authorization" => auth_attrs}), do: do_authorize(conn, auth_attrs)
-
- defp do_authorize(conn, auth_attrs) do
- app = Repo.get_by(App, client_id: auth_attrs["client_id"])
+ defp do_authorize(conn, params) do
+ app = Repo.get_by(App, client_id: params["client_id"])
available_scopes = (app && app.scopes) || []
- scopes = oauth_scopes(auth_attrs, nil) || available_scopes
+ scopes = oauth_scopes(params, nil) || available_scopes
+ # Note: `params` might differ from `conn.params`; use `@params` not `@conn.params` in template
render(conn, Authenticator.auth_template(), %{
- response_type: auth_attrs["response_type"],
- client_id: auth_attrs["client_id"],
+ response_type: params["response_type"],
+ client_id: params["client_id"],
available_scopes: available_scopes,
scopes: scopes,
- redirect_uri: auth_attrs["redirect_uri"],
- state: auth_attrs["state"],
- params: auth_attrs
+ redirect_uri: params["redirect_uri"],
+ state: params["state"],
+ params: params
})
end
user1 = insert(:user)
user2 = insert(:user)
user3 = insert(:user)
+ CommonAPI.favorite(activity.id, user2)
+ {:ok, user2} = User.bookmark(user2, activity.data["object"]["id"])
{:ok, reblog_activity1, _object} = CommonAPI.repeat(activity.id, user1)
{:ok, _, _object} = CommonAPI.repeat(activity.id, user2)
assert %{
"reblog" => %{"id" => id, "reblogged" => false, "reblogs_count" => 2},
- "reblogged" => false
+ "reblogged" => false,
+ "favourited" => false,
+ "bookmarked" => false
} = json_response(conn_res, 200)
conn_res =
assert %{
"reblog" => %{"id" => id, "reblogged" => true, "reblogs_count" => 2},
- "reblogged" => true
+ "reblogged" => true,
+ "favourited" => true,
+ "bookmarked" => true
} = json_response(conn_res, 200)
assert to_string(activity.id) == id
assert id == to_string(other_user.id)
end
+ test "following without reblogs" do
+ follower = insert(:user)
+ followed = insert(:user)
+ other_user = insert(:user)
+
+ conn =
+ build_conn()
+ |> assign(:user, follower)
+ |> post("/api/v1/accounts/#{followed.id}/follow?reblogs=false")
+
+ assert %{"showing_reblogs" => false} = json_response(conn, 200)
+
+ {:ok, activity} = CommonAPI.post(other_user, %{"status" => "hey"})
+ {:ok, reblog, _} = CommonAPI.repeat(activity.id, followed)
+
+ conn =
+ build_conn()
+ |> assign(:user, User.get_cached_by_id(follower.id))
+ |> get("/api/v1/timelines/home")
+
+ assert [] == json_response(conn, 200)
+
+ conn =
+ build_conn()
+ |> assign(:user, follower)
+ |> post("/api/v1/accounts/#{followed.id}/follow?reblogs=true")
+
+ assert %{"showing_reblogs" => true} = json_response(conn, 200)
+
+ conn =
+ build_conn()
+ |> assign(:user, User.get_cached_by_id(follower.id))
+ |> get("/api/v1/timelines/home")
+
+ expected_activity_id = reblog.id
+ assert [%{"id" => ^expected_activity_id}] = json_response(conn, 200)
+ end
+
test "following / unfollowing errors" do
user = insert(:user)
assert html_response(conn, 200) =~ ~s(type="submit")
end
+ test "properly handles internal calls with `authorization`-wrapped params", %{
+ app: app,
+ conn: conn
+ } do
+ conn =
+ get(
+ conn,
+ "/oauth/authorize",
+ %{
+ "authorization" => %{
+ "response_type" => "code",
+ "client_id" => app.client_id,
+ "redirect_uri" => app.redirect_uris,
+ "scope" => "read"
+ }
+ }
+ )
+
+ assert html_response(conn, 200) =~ ~s(type="submit")
+ end
+
test "renders authentication page if user is already authenticated but `force_login` is tru-ish",
%{app: app, conn: conn} do
token = insert(:oauth_token, app_id: app.id)