forked from AkkomaGang/akkoma
Merge pull request 'Add /api/v1/followed_tags' (#410) from followed-tags into develop
Reviewed-on: AkkomaGang/akkoma#410
This commit is contained in:
commit
6be3383a09
9 changed files with 154 additions and 6 deletions
|
@ -13,6 +13,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Argon2 password hashing
|
- Argon2 password hashing
|
||||||
- Ability to "verify" links in profile fields via rel=me
|
- Ability to "verify" links in profile fields via rel=me
|
||||||
- Mix tasks to dump/load config to/from json for bulk editing
|
- Mix tasks to dump/load config to/from json for bulk editing
|
||||||
|
- Followed hashtag list at /api/v1/followed\_tags, API parity with mastodon
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
- Non-finch HTTP adapters
|
- Non-finch HTTP adapters
|
||||||
|
|
|
@ -88,9 +88,9 @@ def paginate(query, options, :offset, table_binding) do
|
||||||
|
|
||||||
defp cast_params(params) do
|
defp cast_params(params) do
|
||||||
param_types = %{
|
param_types = %{
|
||||||
min_id: :string,
|
min_id: params[:id_type] || :string,
|
||||||
since_id: :string,
|
since_id: params[:id_type] || :string,
|
||||||
max_id: :string,
|
max_id: params[:id_type] || :string,
|
||||||
offset: :integer,
|
offset: :integer,
|
||||||
limit: :integer,
|
limit: :integer,
|
||||||
skip_extra_order: :boolean,
|
skip_extra_order: :boolean,
|
||||||
|
|
|
@ -43,7 +43,13 @@ def get(%User{} = user, %Hashtag{} = hashtag) do
|
||||||
end
|
end
|
||||||
|
|
||||||
def get_by_user(%User{} = user) do
|
def get_by_user(%User{} = user) do
|
||||||
Ecto.assoc(user, :followed_hashtags)
|
user
|
||||||
|
|> followed_hashtags_query()
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def followed_hashtags_query(%User{} = user) do
|
||||||
|
Ecto.assoc(user, :followed_hashtags)
|
||||||
|
|> Ecto.Query.order_by([h], desc: h.id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -44,7 +44,7 @@ def unfollow_operation do
|
||||||
tags: ["Tags"],
|
tags: ["Tags"],
|
||||||
summary: "Unfollow a hashtag",
|
summary: "Unfollow a hashtag",
|
||||||
description: "Unfollow a hashtag",
|
description: "Unfollow a hashtag",
|
||||||
security: [%{"oAuth" => ["write:follow"]}],
|
security: [%{"oAuth" => ["write:follows"]}],
|
||||||
parameters: [id_param()],
|
parameters: [id_param()],
|
||||||
operationId: "TagController.unfollow",
|
operationId: "TagController.unfollow",
|
||||||
responses: %{
|
responses: %{
|
||||||
|
@ -54,6 +54,26 @@ def unfollow_operation do
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def show_followed_operation do
|
||||||
|
%Operation{
|
||||||
|
tags: ["Tags"],
|
||||||
|
summary: "Followed hashtags",
|
||||||
|
description: "View a list of hashtags the currently authenticated user is following",
|
||||||
|
parameters: pagination_params(),
|
||||||
|
security: [%{"oAuth" => ["read:follows"]}],
|
||||||
|
operationId: "TagController.show_followed",
|
||||||
|
responses: %{
|
||||||
|
200 =>
|
||||||
|
Operation.response("Hashtags", "application/json", %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: Tag
|
||||||
|
}),
|
||||||
|
403 => Operation.response("Forbidden", "application/json", ApiError),
|
||||||
|
404 => Operation.response("Not Found", "application/json", ApiError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
defp id_param do
|
defp id_param do
|
||||||
Operation.parameter(
|
Operation.parameter(
|
||||||
:id,
|
:id,
|
||||||
|
@ -62,4 +82,22 @@ defp id_param do
|
||||||
"Name of the hashtag"
|
"Name of the hashtag"
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def pagination_params do
|
||||||
|
[
|
||||||
|
Operation.parameter(:max_id, :query, :integer, "Return items older than this ID"),
|
||||||
|
Operation.parameter(
|
||||||
|
:min_id,
|
||||||
|
:query,
|
||||||
|
:integer,
|
||||||
|
"Return the oldest items newer than this ID"
|
||||||
|
),
|
||||||
|
Operation.parameter(
|
||||||
|
:limit,
|
||||||
|
:query,
|
||||||
|
%Schema{type: :integer, default: 20},
|
||||||
|
"Maximum number of items to return. Will be ignored if it's more than 40"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -21,6 +21,12 @@ defmodule Pleroma.Web.ApiSpec.Schemas.Tag do
|
||||||
following: %Schema{
|
following: %Schema{
|
||||||
type: :boolean,
|
type: :boolean,
|
||||||
description: "Whether the authenticated user is following the hashtag"
|
description: "Whether the authenticated user is following the hashtag"
|
||||||
|
},
|
||||||
|
history: %Schema{
|
||||||
|
type: :array,
|
||||||
|
items: %Schema{type: :string},
|
||||||
|
description:
|
||||||
|
"A list of historical uses of the hashtag (not implemented, for compatibility only)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
example: %{
|
example: %{
|
||||||
|
|
|
@ -4,9 +4,24 @@ defmodule Pleroma.Web.MastodonAPI.TagController do
|
||||||
|
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Hashtag
|
alias Pleroma.Hashtag
|
||||||
|
alias Pleroma.Pagination
|
||||||
|
|
||||||
|
import Pleroma.Web.ControllerHelper,
|
||||||
|
only: [
|
||||||
|
add_link_headers: 2
|
||||||
|
]
|
||||||
|
|
||||||
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
plug(Pleroma.Web.ApiSpec.CastAndValidate)
|
||||||
plug(Pleroma.Web.Plugs.OAuthScopesPlug, %{scopes: ["read"]} when action in [:show])
|
|
||||||
|
plug(
|
||||||
|
Pleroma.Web.Plugs.OAuthScopesPlug,
|
||||||
|
%{scopes: ["read"]} when action in [:show]
|
||||||
|
)
|
||||||
|
|
||||||
|
plug(
|
||||||
|
Pleroma.Web.Plugs.OAuthScopesPlug,
|
||||||
|
%{scopes: ["read:follows"]} when action in [:show_followed]
|
||||||
|
)
|
||||||
|
|
||||||
plug(
|
plug(
|
||||||
Pleroma.Web.Plugs.OAuthScopesPlug,
|
Pleroma.Web.Plugs.OAuthScopesPlug,
|
||||||
|
@ -44,4 +59,19 @@ def unfollow(conn, %{id: id}) do
|
||||||
_ -> render_error(conn, :not_found, "Hashtag not found")
|
_ -> render_error(conn, :not_found, "Hashtag not found")
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def show_followed(conn, params) do
|
||||||
|
with %{assigns: %{user: %User{} = user}} <- conn do
|
||||||
|
params = Map.put(params, :id_type, :integer)
|
||||||
|
|
||||||
|
hashtags =
|
||||||
|
user
|
||||||
|
|> User.HashtagFollow.followed_hashtags_query()
|
||||||
|
|> Pagination.fetch_paginated(params)
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> add_link_headers(hashtags)
|
||||||
|
|> render("index.json", tags: hashtags, for_user: user)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -3,6 +3,10 @@ defmodule Pleroma.Web.MastodonAPI.TagView do
|
||||||
alias Pleroma.User
|
alias Pleroma.User
|
||||||
alias Pleroma.Web.Router.Helpers
|
alias Pleroma.Web.Router.Helpers
|
||||||
|
|
||||||
|
def render("index.json", %{tags: tags, for_user: user}) do
|
||||||
|
safe_render_many(tags, __MODULE__, "show.json", %{for_user: user})
|
||||||
|
end
|
||||||
|
|
||||||
def render("show.json", %{tag: tag, for_user: user}) do
|
def render("show.json", %{tag: tag, for_user: user}) do
|
||||||
following =
|
following =
|
||||||
with %User{} <- user do
|
with %User{} <- user do
|
||||||
|
|
|
@ -606,6 +606,7 @@ defmodule Pleroma.Web.Router do
|
||||||
get("/tags/:id", TagController, :show)
|
get("/tags/:id", TagController, :show)
|
||||||
post("/tags/:id/follow", TagController, :follow)
|
post("/tags/:id/follow", TagController, :follow)
|
||||||
post("/tags/:id/unfollow", TagController, :unfollow)
|
post("/tags/:id/unfollow", TagController, :unfollow)
|
||||||
|
get("/followed_tags", TagController, :show_followed)
|
||||||
end
|
end
|
||||||
|
|
||||||
scope "/api/web", Pleroma.Web do
|
scope "/api/web", Pleroma.Web do
|
||||||
|
|
|
@ -94,4 +94,66 @@ test "should 404 if hashtag doesn't exist" do
|
||||||
assert response["error"] == "Hashtag not found"
|
assert response["error"] == "Hashtag not found"
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "GET /api/v1/followed_tags" do
|
||||||
|
test "should list followed tags" do
|
||||||
|
%{user: user, conn: conn} = oauth_access(["read:follows"])
|
||||||
|
|
||||||
|
response =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/followed_tags")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert Enum.empty?(response)
|
||||||
|
|
||||||
|
hashtag = insert(:hashtag, name: "jubjub")
|
||||||
|
{:ok, _user} = User.follow_hashtag(user, hashtag)
|
||||||
|
|
||||||
|
response =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/followed_tags")
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert [%{"name" => "jubjub"}] = response
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should include a link header to paginate" do
|
||||||
|
%{user: user, conn: conn} = oauth_access(["read:follows"])
|
||||||
|
|
||||||
|
for i <- 1..21 do
|
||||||
|
hashtag = insert(:hashtag, name: "jubjub#{i}}")
|
||||||
|
{:ok, _user} = User.follow_hashtag(user, hashtag)
|
||||||
|
end
|
||||||
|
|
||||||
|
response =
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/followed_tags")
|
||||||
|
|
||||||
|
json = json_response_and_validate_schema(response, 200)
|
||||||
|
assert Enum.count(json) == 20
|
||||||
|
assert [link_header] = get_resp_header(response, "link")
|
||||||
|
assert link_header =~ "rel=\"next\""
|
||||||
|
next_link = extract_next_link_header(link_header)
|
||||||
|
|
||||||
|
response =
|
||||||
|
conn
|
||||||
|
|> get(next_link)
|
||||||
|
|> json_response_and_validate_schema(200)
|
||||||
|
|
||||||
|
assert Enum.count(response) == 1
|
||||||
|
end
|
||||||
|
|
||||||
|
test "should refuse access without read:follows scope" do
|
||||||
|
%{conn: conn} = oauth_access(["write"])
|
||||||
|
|
||||||
|
conn
|
||||||
|
|> get("/api/v1/followed_tags")
|
||||||
|
|> json_response_and_validate_schema(403)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp extract_next_link_header(header) do
|
||||||
|
[_, next_link] = Regex.run(~r{<(?<next_link>.*)>; rel="next"}, header)
|
||||||
|
next_link
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue