Merge branch 'develop' into feature/tag_feed
This commit is contained in:
commit
5ea8596448
|
@ -94,6 +94,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||||
- Captcha: Support native provider
|
- Captcha: Support native provider
|
||||||
- Captcha: Enable by default
|
- Captcha: Enable by default
|
||||||
- Mastodon API: Add support for `account_id` param to filter notifications by the account
|
- Mastodon API: Add support for `account_id` param to filter notifications by the account
|
||||||
|
- Mastodon API: Add `emoji_reactions` property to Statuses
|
||||||
- Configuration: `feed.logo` option for tag feed.
|
- Configuration: `feed.logo` option for tag feed.
|
||||||
- Tag feed: `/tags/:tag.rss` - list public statuses by hashtag.
|
- Tag feed: `/tags/:tag.rss` - list public statuses by hashtag.
|
||||||
</details>
|
</details>
|
||||||
|
|
|
@ -29,6 +29,7 @@ Has these additional fields under the `pleroma` object:
|
||||||
- `spoiler_text`: a map consisting of alternate representations of the `spoiler_text` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`
|
- `spoiler_text`: a map consisting of alternate representations of the `spoiler_text` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`
|
||||||
- `expires_at`: a datetime (iso8601) that states when the post will expire (be deleted automatically), or empty if the post won't expire
|
- `expires_at`: a datetime (iso8601) that states when the post will expire (be deleted automatically), or empty if the post won't expire
|
||||||
- `thread_muted`: true if the thread the post belongs to is muted
|
- `thread_muted`: true if the thread the post belongs to is muted
|
||||||
|
- `emoji_reactions`: An object with all the emoji reactions with count. Contains no information about the reacting users, for that use the `emoji_reactions_by` endpoint.
|
||||||
|
|
||||||
## Attachments
|
## Attachments
|
||||||
|
|
||||||
|
|
|
@ -312,9 +312,7 @@ def restrict_deactivated_users(query) do
|
||||||
from(u in User.Query.build(deactivated: true), select: u.ap_id)
|
from(u in User.Query.build(deactivated: true), select: u.ap_id)
|
||||||
|> Repo.all()
|
|> Repo.all()
|
||||||
|
|
||||||
from(activity in query,
|
Activity.Queries.exclude_authors(query, deactivated_users)
|
||||||
where: activity.actor not in ^deactivated_users
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search
|
defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search
|
||||||
|
|
|
@ -12,6 +12,7 @@ defmodule Pleroma.Activity.Queries do
|
||||||
@type query :: Ecto.Queryable.t() | Activity.t()
|
@type query :: Ecto.Queryable.t() | Activity.t()
|
||||||
|
|
||||||
alias Pleroma.Activity
|
alias Pleroma.Activity
|
||||||
|
alias Pleroma.User
|
||||||
|
|
||||||
@spec by_ap_id(query, String.t()) :: query
|
@spec by_ap_id(query, String.t()) :: query
|
||||||
def by_ap_id(query \\ Activity, ap_id) do
|
def by_ap_id(query \\ Activity, ap_id) do
|
||||||
|
@ -29,6 +30,11 @@ def by_actor(query \\ Activity, actor) do
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@spec by_author(query, String.t()) :: query
|
||||||
|
def by_author(query \\ Activity, %User{ap_id: ap_id}) do
|
||||||
|
from(a in query, where: a.actor == ^ap_id)
|
||||||
|
end
|
||||||
|
|
||||||
@spec by_object_id(query, String.t() | [String.t()]) :: query
|
@spec by_object_id(query, String.t() | [String.t()]) :: query
|
||||||
def by_object_id(query \\ Activity, object_id)
|
def by_object_id(query \\ Activity, object_id)
|
||||||
|
|
||||||
|
@ -72,4 +78,8 @@ def exclude_type(query \\ Activity, activity_type) do
|
||||||
where: fragment("(?)->>'type' != ?", activity.data, ^activity_type)
|
where: fragment("(?)->>'type' != ?", activity.data, ^activity_type)
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def exclude_authors(query \\ Activity, actors) do
|
||||||
|
from(activity in query, where: activity.actor not in ^actors)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -26,18 +26,23 @@ def search(user, search_query, options \\ []) do
|
||||||
|> query_with(index_type, search_query)
|
|> query_with(index_type, search_query)
|
||||||
|> maybe_restrict_local(user)
|
|> maybe_restrict_local(user)
|
||||||
|> maybe_restrict_author(author)
|
|> maybe_restrict_author(author)
|
||||||
|
|> maybe_restrict_blocked(user)
|
||||||
|> Pagination.fetch_paginated(%{"offset" => offset, "limit" => limit}, :offset)
|
|> Pagination.fetch_paginated(%{"offset" => offset, "limit" => limit}, :offset)
|
||||||
|> maybe_fetch(user, search_query)
|
|> maybe_fetch(user, search_query)
|
||||||
end
|
end
|
||||||
|
|
||||||
def maybe_restrict_author(query, %User{} = author) do
|
def maybe_restrict_author(query, %User{} = author) do
|
||||||
from([a, o] in query,
|
Activity.Queries.by_author(query, author)
|
||||||
where: a.actor == ^author.ap_id
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def maybe_restrict_author(query, _), do: query
|
def maybe_restrict_author(query, _), do: query
|
||||||
|
|
||||||
|
def maybe_restrict_blocked(query, %User{} = user) do
|
||||||
|
Activity.Queries.exclude_authors(query, User.blocked_users_ap_ids(user))
|
||||||
|
end
|
||||||
|
|
||||||
|
def maybe_restrict_blocked(query, _), do: query
|
||||||
|
|
||||||
defp restrict_public(q) do
|
defp restrict_public(q) do
|
||||||
from([a, o] in q,
|
from([a, o] in q,
|
||||||
where: fragment("?->>'type' = 'Create'", a.data),
|
where: fragment("?->>'type' = 'Create'", a.data),
|
||||||
|
|
|
@ -43,7 +43,7 @@ defp do_search(version, %{assigns: %{user: user}} = conn, %{"q" => query} = para
|
||||||
result =
|
result =
|
||||||
default_values
|
default_values
|
||||||
|> Enum.map(fn {resource, default_value} ->
|
|> Enum.map(fn {resource, default_value} ->
|
||||||
if params["type"] == nil or params["type"] == resource do
|
if params["type"] in [nil, resource] do
|
||||||
{resource, fn -> resource_search(version, resource, query, options) end}
|
{resource, fn -> resource_search(version, resource, query, options) end}
|
||||||
else
|
else
|
||||||
{resource, fn -> default_value end}
|
{resource, fn -> default_value end}
|
||||||
|
|
|
@ -253,6 +253,16 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
|
||||||
nil
|
nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
emoji_reactions =
|
||||||
|
with %{data: %{"reactions" => emoji_reactions}} <- object do
|
||||||
|
Enum.map(emoji_reactions, fn {emoji, users} ->
|
||||||
|
{emoji, length(users)}
|
||||||
|
end)
|
||||||
|
|> Enum.into(%{})
|
||||||
|
else
|
||||||
|
_ -> %{}
|
||||||
|
end
|
||||||
|
|
||||||
%{
|
%{
|
||||||
id: to_string(activity.id),
|
id: to_string(activity.id),
|
||||||
uri: object.data["id"],
|
uri: object.data["id"],
|
||||||
|
@ -293,7 +303,8 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
|
||||||
spoiler_text: %{"text/plain" => summary_plaintext},
|
spoiler_text: %{"text/plain" => summary_plaintext},
|
||||||
expires_at: expires_at,
|
expires_at: expires_at,
|
||||||
direct_conversation_id: direct_conversation_id,
|
direct_conversation_id: direct_conversation_id,
|
||||||
thread_muted: thread_muted?
|
thread_muted: thread_muted?,
|
||||||
|
emoji_reactions: emoji_reactions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
|
@ -53,7 +53,8 @@ test "search", %{conn: conn} do
|
||||||
{:ok, _} = CommonAPI.post(user_two, %{"status" => "This isn't"})
|
{:ok, _} = CommonAPI.post(user_two, %{"status" => "This isn't"})
|
||||||
|
|
||||||
results =
|
results =
|
||||||
get(conn, "/api/v2/search", %{"q" => "2hu #private"})
|
conn
|
||||||
|
|> get("/api/v2/search", %{"q" => "2hu #private"})
|
||||||
|> json_response(200)
|
|> json_response(200)
|
||||||
|
|
||||||
[account | _] = results["accounts"]
|
[account | _] = results["accounts"]
|
||||||
|
@ -73,6 +74,30 @@ test "search", %{conn: conn} do
|
||||||
[status] = results["statuses"]
|
[status] = results["statuses"]
|
||||||
assert status["id"] == to_string(activity.id)
|
assert status["id"] == to_string(activity.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "excludes a blocked users from search results", %{conn: conn} do
|
||||||
|
user = insert(:user)
|
||||||
|
user_smith = insert(:user, %{nickname: "Agent", name: "I love 2hu"})
|
||||||
|
user_neo = insert(:user, %{nickname: "Agent Neo", name: "Agent"})
|
||||||
|
|
||||||
|
{:ok, act1} = CommonAPI.post(user, %{"status" => "This is about 2hu private 天子"})
|
||||||
|
{:ok, act2} = CommonAPI.post(user_smith, %{"status" => "Agent Smith"})
|
||||||
|
{:ok, act3} = CommonAPI.post(user_neo, %{"status" => "Agent Smith"})
|
||||||
|
Pleroma.User.block(user, user_smith)
|
||||||
|
|
||||||
|
results =
|
||||||
|
conn
|
||||||
|
|> assign(:user, user)
|
||||||
|
|> assign(:token, insert(:oauth_token, user: user, scopes: ["read"]))
|
||||||
|
|> get("/api/v2/search", %{"q" => "Agent"})
|
||||||
|
|> json_response(200)
|
||||||
|
|
||||||
|
status_ids = Enum.map(results["statuses"], fn g -> g["id"] end)
|
||||||
|
|
||||||
|
assert act3.id in status_ids
|
||||||
|
refute act2.id in status_ids
|
||||||
|
refute act1.id in status_ids
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe ".account_search" do
|
describe ".account_search" do
|
||||||
|
@ -146,11 +171,10 @@ test "search", %{conn: conn} do
|
||||||
|
|
||||||
{:ok, _} = CommonAPI.post(user_two, %{"status" => "This isn't"})
|
{:ok, _} = CommonAPI.post(user_two, %{"status" => "This isn't"})
|
||||||
|
|
||||||
conn =
|
results =
|
||||||
conn
|
conn
|
||||||
|> get("/api/v1/search", %{"q" => "2hu"})
|
|> get("/api/v1/search", %{"q" => "2hu"})
|
||||||
|
|> json_response(200)
|
||||||
assert results = json_response(conn, 200)
|
|
||||||
|
|
||||||
[account | _] = results["accounts"]
|
[account | _] = results["accounts"]
|
||||||
assert account["id"] == to_string(user_three.id)
|
assert account["id"] == to_string(user_three.id)
|
||||||
|
@ -168,11 +192,10 @@ test "search fetches remote statuses and prefers them over other results", %{con
|
||||||
"status" => "check out https://shitposter.club/notice/2827873"
|
"status" => "check out https://shitposter.club/notice/2827873"
|
||||||
})
|
})
|
||||||
|
|
||||||
conn =
|
results =
|
||||||
conn
|
conn
|
||||||
|> get("/api/v1/search", %{"q" => "https://shitposter.club/notice/2827873"})
|
|> get("/api/v1/search", %{"q" => "https://shitposter.club/notice/2827873"})
|
||||||
|
|> json_response(200)
|
||||||
assert results = json_response(conn, 200)
|
|
||||||
|
|
||||||
[status, %{"id" => ^activity_id}] = results["statuses"]
|
[status, %{"id" => ^activity_id}] = results["statuses"]
|
||||||
|
|
||||||
|
@ -189,11 +212,10 @@ test "search doesn't show statuses that it shouldn't", %{conn: conn} do
|
||||||
})
|
})
|
||||||
|
|
||||||
capture_log(fn ->
|
capture_log(fn ->
|
||||||
conn =
|
results =
|
||||||
conn
|
conn
|
||||||
|> get("/api/v1/search", %{"q" => Object.normalize(activity).data["id"]})
|
|> get("/api/v1/search", %{"q" => Object.normalize(activity).data["id"]})
|
||||||
|
|> json_response(200)
|
||||||
assert results = json_response(conn, 200)
|
|
||||||
|
|
||||||
[] = results["statuses"]
|
[] = results["statuses"]
|
||||||
end)
|
end)
|
||||||
|
@ -202,23 +224,23 @@ test "search doesn't show statuses that it shouldn't", %{conn: conn} do
|
||||||
test "search fetches remote accounts", %{conn: conn} do
|
test "search fetches remote accounts", %{conn: conn} do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
|
|
||||||
conn =
|
results =
|
||||||
conn
|
conn
|
||||||
|> assign(:user, user)
|
|> assign(:user, user)
|
||||||
|> assign(:token, insert(:oauth_token, user: user, scopes: ["read"]))
|
|> assign(:token, insert(:oauth_token, user: user, scopes: ["read"]))
|
||||||
|> get("/api/v1/search", %{"q" => "mike@osada.macgirvin.com", "resolve" => "true"})
|
|> get("/api/v1/search", %{"q" => "mike@osada.macgirvin.com", "resolve" => "true"})
|
||||||
|
|> json_response(200)
|
||||||
|
|
||||||
assert results = json_response(conn, 200)
|
|
||||||
[account] = results["accounts"]
|
[account] = results["accounts"]
|
||||||
assert account["acct"] == "mike@osada.macgirvin.com"
|
assert account["acct"] == "mike@osada.macgirvin.com"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "search doesn't fetch remote accounts if resolve is false", %{conn: conn} do
|
test "search doesn't fetch remote accounts if resolve is false", %{conn: conn} do
|
||||||
conn =
|
results =
|
||||||
conn
|
conn
|
||||||
|> get("/api/v1/search", %{"q" => "mike@osada.macgirvin.com", "resolve" => "false"})
|
|> get("/api/v1/search", %{"q" => "mike@osada.macgirvin.com", "resolve" => "false"})
|
||||||
|
|> json_response(200)
|
||||||
|
|
||||||
assert results = json_response(conn, 200)
|
|
||||||
assert [] == results["accounts"]
|
assert [] == results["accounts"]
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,22 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
|
||||||
:ok
|
:ok
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "has an emoji reaction list" do
|
||||||
|
user = insert(:user)
|
||||||
|
other_user = insert(:user)
|
||||||
|
third_user = insert(:user)
|
||||||
|
{:ok, activity} = CommonAPI.post(user, %{"status" => "dae cofe??"})
|
||||||
|
|
||||||
|
{:ok, _, _} = CommonAPI.react_with_emoji(activity.id, user, "☕")
|
||||||
|
{:ok, _, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
|
||||||
|
{:ok, _, _} = CommonAPI.react_with_emoji(activity.id, third_user, "🍵")
|
||||||
|
activity = Repo.get(Activity, activity.id)
|
||||||
|
status = StatusView.render("show.json", activity: activity)
|
||||||
|
|
||||||
|
assert status[:pleroma][:emoji_reactions]["🍵"] == 1
|
||||||
|
assert status[:pleroma][:emoji_reactions]["☕"] == 2
|
||||||
|
end
|
||||||
|
|
||||||
test "loads and returns the direct conversation id when given the `with_direct_conversation_id` option" do
|
test "loads and returns the direct conversation id when given the `with_direct_conversation_id` option" do
|
||||||
user = insert(:user)
|
user = insert(:user)
|
||||||
|
|
||||||
|
@ -172,7 +188,8 @@ test "a note activity" do
|
||||||
spoiler_text: %{"text/plain" => HTML.strip_tags(object_data["summary"])},
|
spoiler_text: %{"text/plain" => HTML.strip_tags(object_data["summary"])},
|
||||||
expires_at: nil,
|
expires_at: nil,
|
||||||
direct_conversation_id: nil,
|
direct_conversation_id: nil,
|
||||||
thread_muted: false
|
thread_muted: false,
|
||||||
|
emoji_reactions: %{}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue