forked from YokaiRick/akkoma
Merge branch 'develop' into feature/relay-list
This commit is contained in:
commit
2473702be2
49 changed files with 1745 additions and 368 deletions
|
@ -15,6 +15,7 @@ cache:
|
|||
stages:
|
||||
- build
|
||||
- test
|
||||
- benchmark
|
||||
- deploy
|
||||
- release
|
||||
|
||||
|
@ -28,6 +29,36 @@ build:
|
|||
- mix deps.get
|
||||
- mix compile --force
|
||||
|
||||
docs-build:
|
||||
stage: build
|
||||
only:
|
||||
- master@pleroma/pleroma
|
||||
- develop@pleroma/pleroma
|
||||
variables:
|
||||
MIX_ENV: dev
|
||||
PLEROMA_BUILD_ENV: prod
|
||||
script:
|
||||
- mix deps.get
|
||||
- mix compile
|
||||
- mix docs
|
||||
artifacts:
|
||||
paths:
|
||||
- priv/static/doc
|
||||
|
||||
benchmark:
|
||||
stage: benchmark
|
||||
variables:
|
||||
MIX_ENV: benchmark
|
||||
services:
|
||||
- name: lainsoykaf/postgres-with-rum
|
||||
alias: postgres
|
||||
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
|
||||
script:
|
||||
- mix deps.get
|
||||
- mix ecto.create
|
||||
- mix ecto.migrate
|
||||
- mix pleroma.load_testing
|
||||
|
||||
unit-testing:
|
||||
stage: test
|
||||
services:
|
||||
|
@ -70,7 +101,7 @@ docs-deploy:
|
|||
stage: deploy
|
||||
image: alpine:latest
|
||||
only:
|
||||
- master@pleroma/pleroma
|
||||
- stable@pleroma/pleroma
|
||||
- develop@pleroma/pleroma
|
||||
before_script:
|
||||
- apk add curl
|
||||
|
@ -127,9 +158,10 @@ amd64:
|
|||
# TODO: Replace with upstream image when 1.9.0 comes out
|
||||
image: rinpatch/elixir:1.9.0-rc.0
|
||||
only: &release-only
|
||||
- master@pleroma/pleroma
|
||||
- stable@pleroma/pleroma
|
||||
- develop@pleroma/pleroma
|
||||
- /^maint/.*$/@pleroma/pleroma
|
||||
- /^release/.*$/@pleroma/pleroma
|
||||
artifacts: &release-artifacts
|
||||
name: "pleroma-$CI_COMMIT_REF_NAME-$CI_COMMIT_SHORT_SHA-$CI_JOB_NAME"
|
||||
paths:
|
||||
|
|
|
@ -17,6 +17,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Authentication: Added rate limit for password-authorized actions / login existence checks
|
||||
- Metadata Link: Atom syndication Feed
|
||||
- Mix task to re-count statuses for all users (`mix pleroma.count_statuses`)
|
||||
- Mastodon API: Add `exclude_visibilities` parameter to the timeline and notification endpoints
|
||||
- Admin API: `/users/:nickname/toggle_activation` endpoint is now deprecated in favor of: `/users/activate`, `/users/deactivate`, both accept `nicknames` array
|
||||
- Admin API: `POST/DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group` are deprecated in favor of: `POST/DELETE /api/pleroma/admin/users/permission_group/:permission_group` (both accept `nicknames` array), `DELETE /api/pleroma/admin/users` (`nickname` query param or `nickname` sent in JSON body) is deprecated in favor of: `DELETE /api/pleroma/admin/users` (`nicknames` query array param or `nicknames` sent in JSON body).
|
||||
- Admin API: Add `GET /api/pleroma/admin/relay` endpoint - lists all followed relays
|
||||
|
||||
### Changed
|
||||
|
@ -30,6 +33,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- MRF (Simple Policy): Also use `:accept`/`:reject` on the actors rather than only their activities
|
||||
- OStatus: Extract RSS functionality
|
||||
- Mastodon API: Add `pleroma.direct_conversation_id` to the status endpoint (`GET /api/v1/statuses/:id`)
|
||||
- Mastodon API: Mark the direct conversation as read for the author when they send a new direct message
|
||||
|
||||
### Fixed
|
||||
- Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`)
|
||||
|
@ -38,6 +42,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|||
- Report emails now include functional links to profiles of remote user accounts
|
||||
|
||||
## [1.1.0] - 2019-??-??
|
||||
**Breaking:** The stable branch has been changed from `master` to `stable`, `master` now points to `release/1.0`
|
||||
### Security
|
||||
- Mastodon API: respect post privacy in `/api/v1/statuses/:id/{favourited,reblogged}_by`
|
||||
|
||||
|
|
229
benchmarks/load_testing/fetcher.ex
Normal file
229
benchmarks/load_testing/fetcher.ex
Normal file
|
@ -0,0 +1,229 @@
|
|||
defmodule Pleroma.LoadTesting.Fetcher do
|
||||
use Pleroma.LoadTesting.Helper
|
||||
|
||||
def fetch_user(user) do
|
||||
Benchee.run(%{
|
||||
"By id" => fn -> Repo.get_by(User, id: user.id) end,
|
||||
"By ap_id" => fn -> Repo.get_by(User, ap_id: user.ap_id) end,
|
||||
"By email" => fn -> Repo.get_by(User, email: user.email) end,
|
||||
"By nickname" => fn -> Repo.get_by(User, nickname: user.nickname) end
|
||||
})
|
||||
end
|
||||
|
||||
def query_timelines(user) do
|
||||
home_timeline_params = %{
|
||||
"count" => 20,
|
||||
"with_muted" => true,
|
||||
"type" => ["Create", "Announce"],
|
||||
"blocking_user" => user,
|
||||
"muting_user" => user,
|
||||
"user" => user
|
||||
}
|
||||
|
||||
mastodon_public_timeline_params = %{
|
||||
"count" => 20,
|
||||
"local_only" => true,
|
||||
"only_media" => "false",
|
||||
"type" => ["Create", "Announce"],
|
||||
"with_muted" => "true",
|
||||
"blocking_user" => user,
|
||||
"muting_user" => user
|
||||
}
|
||||
|
||||
mastodon_federated_timeline_params = %{
|
||||
"count" => 20,
|
||||
"only_media" => "false",
|
||||
"type" => ["Create", "Announce"],
|
||||
"with_muted" => "true",
|
||||
"blocking_user" => user,
|
||||
"muting_user" => user
|
||||
}
|
||||
|
||||
Benchee.run(%{
|
||||
"User home timeline" => fn ->
|
||||
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities(
|
||||
[user.ap_id | user.following],
|
||||
home_timeline_params
|
||||
)
|
||||
end,
|
||||
"User mastodon public timeline" => fn ->
|
||||
Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities(
|
||||
mastodon_public_timeline_params
|
||||
)
|
||||
end,
|
||||
"User mastodon federated public timeline" => fn ->
|
||||
Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities(
|
||||
mastodon_federated_timeline_params
|
||||
)
|
||||
end
|
||||
})
|
||||
|
||||
home_activities =
|
||||
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities(
|
||||
[user.ap_id | user.following],
|
||||
home_timeline_params
|
||||
)
|
||||
|
||||
public_activities =
|
||||
Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities(mastodon_public_timeline_params)
|
||||
|
||||
public_federated_activities =
|
||||
Pleroma.Web.ActivityPub.ActivityPub.fetch_public_activities(
|
||||
mastodon_federated_timeline_params
|
||||
)
|
||||
|
||||
Benchee.run(%{
|
||||
"Rendering home timeline" => fn ->
|
||||
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
|
||||
activities: home_activities,
|
||||
for: user,
|
||||
as: :activity
|
||||
})
|
||||
end,
|
||||
"Rendering public timeline" => fn ->
|
||||
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
|
||||
activities: public_activities,
|
||||
for: user,
|
||||
as: :activity
|
||||
})
|
||||
end,
|
||||
"Rendering public federated timeline" => fn ->
|
||||
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
|
||||
activities: public_federated_activities,
|
||||
for: user,
|
||||
as: :activity
|
||||
})
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
def query_notifications(user) do
|
||||
without_muted_params = %{"count" => "20", "with_muted" => "false"}
|
||||
with_muted_params = %{"count" => "20", "with_muted" => "true"}
|
||||
|
||||
Benchee.run(%{
|
||||
"Notifications without muted" => fn ->
|
||||
Pleroma.Web.MastodonAPI.MastodonAPI.get_notifications(user, without_muted_params)
|
||||
end,
|
||||
"Notifications with muted" => fn ->
|
||||
Pleroma.Web.MastodonAPI.MastodonAPI.get_notifications(user, with_muted_params)
|
||||
end
|
||||
})
|
||||
|
||||
without_muted_notifications =
|
||||
Pleroma.Web.MastodonAPI.MastodonAPI.get_notifications(user, without_muted_params)
|
||||
|
||||
with_muted_notifications =
|
||||
Pleroma.Web.MastodonAPI.MastodonAPI.get_notifications(user, with_muted_params)
|
||||
|
||||
Benchee.run(%{
|
||||
"Render notifications without muted" => fn ->
|
||||
Pleroma.Web.MastodonAPI.NotificationView.render("index.json", %{
|
||||
notifications: without_muted_notifications,
|
||||
for: user
|
||||
})
|
||||
end,
|
||||
"Render notifications with muted" => fn ->
|
||||
Pleroma.Web.MastodonAPI.NotificationView.render("index.json", %{
|
||||
notifications: with_muted_notifications,
|
||||
for: user
|
||||
})
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
def query_dms(user) do
|
||||
params = %{
|
||||
"count" => "20",
|
||||
"with_muted" => "true",
|
||||
"type" => "Create",
|
||||
"blocking_user" => user,
|
||||
"user" => user,
|
||||
visibility: "direct"
|
||||
}
|
||||
|
||||
Benchee.run(%{
|
||||
"Direct messages with muted" => fn ->
|
||||
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query([user.ap_id], params)
|
||||
|> Pleroma.Pagination.fetch_paginated(params)
|
||||
end,
|
||||
"Direct messages without muted" => fn ->
|
||||
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query([user.ap_id], params)
|
||||
|> Pleroma.Pagination.fetch_paginated(Map.put(params, "with_muted", false))
|
||||
end
|
||||
})
|
||||
|
||||
dms_with_muted =
|
||||
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query([user.ap_id], params)
|
||||
|> Pleroma.Pagination.fetch_paginated(params)
|
||||
|
||||
dms_without_muted =
|
||||
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_query([user.ap_id], params)
|
||||
|> Pleroma.Pagination.fetch_paginated(Map.put(params, "with_muted", false))
|
||||
|
||||
Benchee.run(%{
|
||||
"Rendering dms with muted" => fn ->
|
||||
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
|
||||
activities: dms_with_muted,
|
||||
for: user,
|
||||
as: :activity
|
||||
})
|
||||
end,
|
||||
"Rendering dms without muted" => fn ->
|
||||
Pleroma.Web.MastodonAPI.StatusView.render("index.json", %{
|
||||
activities: dms_without_muted,
|
||||
for: user,
|
||||
as: :activity
|
||||
})
|
||||
end
|
||||
})
|
||||
end
|
||||
|
||||
def query_long_thread(user, activity) do
|
||||
Benchee.run(%{
|
||||
"Fetch main post" => fn ->
|
||||
Pleroma.Activity.get_by_id_with_object(activity.id)
|
||||
end,
|
||||
"Fetch context of main post" => fn ->
|
||||
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_for_context(
|
||||
activity.data["context"],
|
||||
%{
|
||||
"blocking_user" => user,
|
||||
"user" => user,
|
||||
"exclude_id" => activity.id
|
||||
}
|
||||
)
|
||||
end
|
||||
})
|
||||
|
||||
activity = Pleroma.Activity.get_by_id_with_object(activity.id)
|
||||
|
||||
context =
|
||||
Pleroma.Web.ActivityPub.ActivityPub.fetch_activities_for_context(
|
||||
activity.data["context"],
|
||||
%{
|
||||
"blocking_user" => user,
|
||||
"user" => user,
|
||||
"exclude_id" => activity.id
|
||||
}
|
||||
)
|
||||
|
||||
Benchee.run(%{
|
||||
"Render status" => fn ->
|
||||
Pleroma.Web.MastodonAPI.StatusView.render("show.json", %{
|
||||
activity: activity,
|
||||
for: user
|
||||
})
|
||||
end,
|
||||
"Render context" => fn ->
|
||||
Pleroma.Web.MastodonAPI.StatusView.render(
|
||||
"index.json",
|
||||
for: user,
|
||||
activities: context,
|
||||
as: :activity
|
||||
)
|
||||
|> Enum.reverse()
|
||||
end
|
||||
})
|
||||
end
|
||||
end
|
352
benchmarks/load_testing/generator.ex
Normal file
352
benchmarks/load_testing/generator.ex
Normal file
|
@ -0,0 +1,352 @@
|
|||
defmodule Pleroma.LoadTesting.Generator do
|
||||
use Pleroma.LoadTesting.Helper
|
||||
alias Pleroma.Web.CommonAPI
|
||||
|
||||
def generate_users(opts) do
|
||||
IO.puts("Starting generating #{opts[:users_max]} users...")
|
||||
{time, _} = :timer.tc(fn -> do_generate_users(opts) end)
|
||||
|
||||
IO.puts("Inserting users take #{to_sec(time)} sec.\n")
|
||||
end
|
||||
|
||||
defp do_generate_users(opts) do
|
||||
max = Keyword.get(opts, :users_max)
|
||||
|
||||
Task.async_stream(
|
||||
1..max,
|
||||
&generate_user_data(&1),
|
||||
max_concurrency: 10,
|
||||
timeout: 30_000
|
||||
)
|
||||
|> Enum.to_list()
|
||||
end
|
||||
|
||||
defp generate_user_data(i) do
|
||||
remote = Enum.random([true, false])
|
||||
|
||||
user = %User{
|
||||
name: "Test テスト User #{i}",
|
||||
email: "user#{i}@example.com",
|
||||
nickname: "nick#{i}",
|
||||
password_hash:
|
||||
"$pbkdf2-sha512$160000$bU.OSFI7H/yqWb5DPEqyjw$uKp/2rmXw12QqnRRTqTtuk2DTwZfF8VR4MYW2xMeIlqPR/UX1nT1CEKVUx2CowFMZ5JON8aDvURrZpJjSgqXrg",
|
||||
bio: "Tester Number #{i}",
|
||||
info: %{},
|
||||
local: remote
|
||||
}
|
||||
|
||||
user_urls =
|
||||
if remote do
|
||||
base_url =
|
||||
Enum.random(["https://domain1.com", "https://domain2.com", "https://domain3.com"])
|
||||
|
||||
ap_id = "#{base_url}/users/#{user.nickname}"
|
||||
|
||||
%{
|
||||
ap_id: ap_id,
|
||||
follower_address: ap_id <> "/followers",
|
||||
following_address: ap_id <> "/following",
|
||||
following: [ap_id]
|
||||
}
|
||||
else
|
||||
%{
|
||||
ap_id: User.ap_id(user),
|
||||
follower_address: User.ap_followers(user),
|
||||
following_address: User.ap_following(user),
|
||||
following: [User.ap_id(user)]
|
||||
}
|
||||
end
|
||||
|
||||
user = Map.merge(user, user_urls)
|
||||
|
||||
Repo.insert!(user)
|
||||
end
|
||||
|
||||
def generate_activities(user, users) do
|
||||
do_generate_activities(user, users)
|
||||
end
|
||||
|
||||
defp do_generate_activities(user, users) do
|
||||
IO.puts("Starting generating 20000 common activities...")
|
||||
|
||||
{time, _} =
|
||||
:timer.tc(fn ->
|
||||
Task.async_stream(
|
||||
1..20_000,
|
||||
fn _ ->
|
||||
do_generate_activity([user | users])
|
||||
end,
|
||||
max_concurrency: 10,
|
||||
timeout: 30_000
|
||||
)
|
||||
|> Stream.run()
|
||||
end)
|
||||
|
||||
IO.puts("Inserting common activities take #{to_sec(time)} sec.\n")
|
||||
|
||||
IO.puts("Starting generating 20000 activities with mentions...")
|
||||
|
||||
{time, _} =
|
||||
:timer.tc(fn ->
|
||||
Task.async_stream(
|
||||
1..20_000,
|
||||
fn _ ->
|
||||
do_generate_activity_with_mention(user, users)
|
||||
end,
|
||||
max_concurrency: 10,
|
||||
timeout: 30_000
|
||||
)
|
||||
|> Stream.run()
|
||||
end)
|
||||
|
||||
IO.puts("Inserting activities with menthions take #{to_sec(time)} sec.\n")
|
||||
|
||||
IO.puts("Starting generating 10000 activities with threads...")
|
||||
|
||||
{time, _} =
|
||||
:timer.tc(fn ->
|
||||
Task.async_stream(
|
||||
1..10_000,
|
||||
fn _ ->
|
||||
do_generate_threads([user | users])
|
||||
end,
|
||||
max_concurrency: 10,
|
||||
timeout: 30_000
|
||||
)
|
||||
|> Stream.run()
|
||||
end)
|
||||
|
||||
IO.puts("Inserting activities with threads take #{to_sec(time)} sec.\n")
|
||||
end
|
||||
|
||||
defp do_generate_activity(users) do
|
||||
post = %{
|
||||
"status" => "Some status without mention with random user"
|
||||
}
|
||||
|
||||
CommonAPI.post(Enum.random(users), post)
|
||||
end
|
||||
|
||||
defp do_generate_activity_with_mention(user, users) do
|
||||
mentions_cnt = Enum.random([2, 3, 4, 5])
|
||||
with_user = Enum.random([true, false])
|
||||
users = Enum.shuffle(users)
|
||||
mentions_users = Enum.take(users, mentions_cnt)
|
||||
mentions_users = if with_user, do: [user | mentions_users], else: mentions_users
|
||||
|
||||
mentions_str =
|
||||
Enum.map(mentions_users, fn user -> "@" <> user.nickname end) |> Enum.join(", ")
|
||||
|
||||
post = %{
|
||||
"status" => mentions_str <> "some status with mentions random users"
|
||||
}
|
||||
|
||||
CommonAPI.post(Enum.random(users), post)
|
||||
end
|
||||
|
||||
defp do_generate_threads(users) do
|
||||
thread_length = Enum.random([2, 3, 4, 5])
|
||||
actor = Enum.random(users)
|
||||
|
||||
post = %{
|
||||
"status" => "Start of the thread"
|
||||
}
|
||||
|
||||
{:ok, activity} = CommonAPI.post(actor, post)
|
||||
|
||||
Enum.each(1..thread_length, fn _ ->
|
||||
user = Enum.random(users)
|
||||
|
||||
post = %{
|
||||
"status" => "@#{actor.nickname} reply to thread",
|
||||
"in_reply_to_status_id" => activity.id
|
||||
}
|
||||
|
||||
CommonAPI.post(user, post)
|
||||
end)
|
||||
end
|
||||
|
||||
def generate_remote_activities(user, users) do
|
||||
do_generate_remote_activities(user, users)
|
||||
end
|
||||
|
||||
defp do_generate_remote_activities(user, users) do
|
||||
IO.puts("Starting generating 10000 remote activities...")
|
||||
|
||||
{time, _} =
|
||||
:timer.tc(fn ->
|
||||
Task.async_stream(
|
||||
1..10_000,
|
||||
fn i ->
|
||||
do_generate_remote_activity(i, user, users)
|
||||
end,
|
||||
max_concurrency: 10,
|
||||
timeout: 30_000
|
||||
)
|
||||
|> Stream.run()
|
||||
end)
|
||||
|
||||
IO.puts("Inserting remote activities take #{to_sec(time)} sec.\n")
|
||||
end
|
||||
|
||||
defp do_generate_remote_activity(i, user, users) do
|
||||
actor = Enum.random(users)
|
||||
%{host: host} = URI.parse(actor.ap_id)
|
||||
date = Date.utc_today()
|
||||
datetime = DateTime.utc_now()
|
||||
|
||||
map = %{
|
||||
"actor" => actor.ap_id,
|
||||
"cc" => [actor.follower_address, user.ap_id],
|
||||
"context" => "tag:mastodon.example.org,#{date}:objectId=#{i}:objectType=Conversation",
|
||||
"id" => actor.ap_id <> "/statuses/#{i}/activity",
|
||||
"object" => %{
|
||||
"actor" => actor.ap_id,
|
||||
"atomUri" => actor.ap_id <> "/statuses/#{i}",
|
||||
"attachment" => [],
|
||||
"attributedTo" => actor.ap_id,
|
||||
"bcc" => [],
|
||||
"bto" => [],
|
||||
"cc" => [actor.follower_address, user.ap_id],
|
||||
"content" =>
|
||||
"<p><span class=\"h-card\"><a href=\"" <>
|
||||
user.ap_id <>
|
||||
"\" class=\"u-url mention\">@<span>" <> user.nickname <> "</span></a></span></p>",
|
||||
"context" => "tag:mastodon.example.org,#{date}:objectId=#{i}:objectType=Conversation",
|
||||
"conversation" =>
|
||||
"tag:mastodon.example.org,#{date}:objectId=#{i}:objectType=Conversation",
|
||||
"emoji" => %{},
|
||||
"id" => actor.ap_id <> "/statuses/#{i}",
|
||||
"inReplyTo" => nil,
|
||||
"inReplyToAtomUri" => nil,
|
||||
"published" => datetime,
|
||||
"sensitive" => true,
|
||||
"summary" => "cw",
|
||||
"tag" => [
|
||||
%{
|
||||
"href" => user.ap_id,
|
||||
"name" => "@#{user.nickname}@#{host}",
|
||||
"type" => "Mention"
|
||||
}
|
||||
],
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type" => "Note",
|
||||
"url" => "http://#{host}/@#{actor.nickname}/#{i}"
|
||||
},
|
||||
"published" => datetime,
|
||||
"to" => ["https://www.w3.org/ns/activitystreams#Public"],
|
||||
"type" => "Create"
|
||||
}
|
||||
|
||||
Pleroma.Web.ActivityPub.ActivityPub.insert(map, false)
|
||||
end
|
||||
|
||||
def generate_dms(user, users, opts) do
|
||||
IO.puts("Starting generating #{opts[:dms_max]} DMs")
|
||||
{time, _} = :timer.tc(fn -> do_generate_dms(user, users, opts) end)
|
||||
IO.puts("Inserting dms take #{to_sec(time)} sec.\n")
|
||||
end
|
||||
|
||||
defp do_generate_dms(user, users, opts) do
|
||||
Task.async_stream(
|
||||
1..opts[:dms_max],
|
||||
fn _ ->
|
||||
do_generate_dm(user, users)
|
||||
end,
|
||||
max_concurrency: 10,
|
||||
timeout: 30_000
|
||||
)
|
||||
|> Stream.run()
|
||||
end
|
||||
|
||||
defp do_generate_dm(user, users) do
|
||||
post = %{
|
||||
"status" => "@#{user.nickname} some direct message",
|
||||
"visibility" => "direct"
|
||||
}
|
||||
|
||||
CommonAPI.post(Enum.random(users), post)
|
||||
end
|
||||
|
||||
def generate_long_thread(user, users, opts) do
|
||||
IO.puts("Starting generating long thread with #{opts[:thread_length]} replies")
|
||||
{time, activity} = :timer.tc(fn -> do_generate_long_thread(user, users, opts) end)
|
||||
IO.puts("Inserting long thread replies take #{to_sec(time)} sec.\n")
|
||||
{:ok, activity}
|
||||
end
|
||||
|
||||
defp do_generate_long_thread(user, users, opts) do
|
||||
{:ok, %{id: id} = activity} = CommonAPI.post(user, %{"status" => "Start of long thread"})
|
||||
|
||||
Task.async_stream(
|
||||
1..opts[:thread_length],
|
||||
fn _ -> do_generate_thread(users, id) end,
|
||||
max_concurrency: 10,
|
||||
timeout: 30_000
|
||||
)
|
||||
|> Stream.run()
|
||||
|
||||
activity
|
||||
end
|
||||
|
||||
defp do_generate_thread(users, activity_id) do
|
||||
CommonAPI.post(Enum.random(users), %{
|
||||
"status" => "reply to main post",
|
||||
"in_reply_to_status_id" => activity_id
|
||||
})
|
||||
end
|
||||
|
||||
def generate_non_visible_message(user, users) do
|
||||
IO.puts("Starting generating 1000 non visible posts")
|
||||
|
||||
{time, _} =
|
||||
:timer.tc(fn ->
|
||||
do_generate_non_visible_posts(user, users)
|
||||
end)
|
||||
|
||||
IO.puts("Inserting non visible posts take #{to_sec(time)} sec.\n")
|
||||
end
|
||||
|
||||
defp do_generate_non_visible_posts(user, users) do
|
||||
[not_friend | users] = users
|
||||
|
||||
make_friends(user, users)
|
||||
|
||||
Task.async_stream(1..1000, fn _ -> do_generate_non_visible_post(not_friend, users) end,
|
||||
max_concurrency: 10,
|
||||
timeout: 30_000
|
||||
)
|
||||
|> Stream.run()
|
||||
end
|
||||
|
||||
defp make_friends(_user, []), do: nil
|
||||
|
||||
defp make_friends(user, [friend | users]) do
|
||||
{:ok, _} = User.follow(user, friend)
|
||||
{:ok, _} = User.follow(friend, user)
|
||||
make_friends(user, users)
|
||||
end
|
||||
|
||||
defp do_generate_non_visible_post(not_friend, users) do
|
||||
post = %{
|
||||
"status" => "some non visible post",
|
||||
"visibility" => "private"
|
||||
}
|
||||
|
||||
{:ok, activity} = CommonAPI.post(not_friend, post)
|
||||
|
||||
thread_length = Enum.random([2, 3, 4, 5])
|
||||
|
||||
Enum.each(1..thread_length, fn _ ->
|
||||
user = Enum.random(users)
|
||||
|
||||
post = %{
|
||||
"status" => "@#{not_friend.nickname} reply to non visible post",
|
||||
"in_reply_to_status_id" => activity.id,
|
||||
"visibility" => "private"
|
||||
}
|
||||
|
||||
CommonAPI.post(user, post)
|
||||
end)
|
||||
end
|
||||
end
|
11
benchmarks/load_testing/helper.ex
Normal file
11
benchmarks/load_testing/helper.ex
Normal file
|
@ -0,0 +1,11 @@
|
|||
defmodule Pleroma.LoadTesting.Helper do
|
||||
defmacro __using__(_) do
|
||||
quote do
|
||||
import Ecto.Query
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
|
||||
defp to_sec(microseconds), do: microseconds / 1_000_000
|
||||
end
|
||||
end
|
||||
end
|
134
benchmarks/mix/tasks/pleroma/load_testing.ex
Normal file
134
benchmarks/mix/tasks/pleroma/load_testing.ex
Normal file
|
@ -0,0 +1,134 @@
|
|||
defmodule Mix.Tasks.Pleroma.LoadTesting do
|
||||
use Mix.Task
|
||||
use Pleroma.LoadTesting.Helper
|
||||
import Mix.Pleroma
|
||||
import Pleroma.LoadTesting.Generator
|
||||
import Pleroma.LoadTesting.Fetcher
|
||||
|
||||
@shortdoc "Factory for generation data"
|
||||
@moduledoc """
|
||||
Generates data like:
|
||||
- local/remote users
|
||||
- local/remote activities with notifications
|
||||
- direct messages
|
||||
- long thread
|
||||
- non visible posts
|
||||
|
||||
## Generate data
|
||||
MIX_ENV=benchmark mix pleroma.load_testing --users 20000 --dms 20000 --thread_length 2000
|
||||
MIX_ENV=benchmark mix pleroma.load_testing -u 20000 -d 20000 -t 2000
|
||||
|
||||
Options:
|
||||
- `--users NUMBER` - number of users to generate. Defaults to: 20000. Alias: `-u`
|
||||
- `--dms NUMBER` - number of direct messages to generate. Defaults to: 20000. Alias `-d`
|
||||
- `--thread_length` - number of messages in thread. Defaults to: 2000. ALias `-t`
|
||||
"""
|
||||
|
||||
@aliases [u: :users, d: :dms, t: :thread_length]
|
||||
@switches [
|
||||
users: :integer,
|
||||
dms: :integer,
|
||||
thread_length: :integer
|
||||
]
|
||||
@users_default 20_000
|
||||
@dms_default 1_000
|
||||
@thread_length_default 2_000
|
||||
|
||||
def run(args) do
|
||||
start_pleroma()
|
||||
Pleroma.Config.put([:instance, :skip_thread_containment], true)
|
||||
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)
|
||||
|
||||
users_max = Keyword.get(opts, :users, @users_default)
|
||||
dms_max = Keyword.get(opts, :dms, @dms_default)
|
||||
thread_length = Keyword.get(opts, :thread_length, @thread_length_default)
|
||||
|
||||
clean_tables()
|
||||
|
||||
opts =
|
||||
Keyword.put(opts, :users_max, users_max)
|
||||
|> Keyword.put(:dms_max, dms_max)
|
||||
|> Keyword.put(:thread_length, thread_length)
|
||||
|
||||
generate_users(opts)
|
||||
|
||||
# main user for queries
|
||||
IO.puts("Fetching local main user...")
|
||||
|
||||
{time, user} =
|
||||
:timer.tc(fn ->
|
||||
Repo.one(
|
||||
from(u in User, where: u.local == true, order_by: fragment("RANDOM()"), limit: 1)
|
||||
)
|
||||
end)
|
||||
|
||||
IO.puts("Fetching main user take #{to_sec(time)} sec.\n")
|
||||
|
||||
IO.puts("Fetching local users...")
|
||||
|
||||
{time, users} =
|
||||
:timer.tc(fn ->
|
||||
Repo.all(
|
||||
from(u in User,
|
||||
where: u.id != ^user.id,
|
||||
where: u.local == true,
|
||||
order_by: fragment("RANDOM()"),
|
||||
limit: 10
|
||||
)
|
||||
)
|
||||
end)
|
||||
|
||||
IO.puts("Fetching local users take #{to_sec(time)} sec.\n")
|
||||
|
||||
IO.puts("Fetching remote users...")
|
||||
|
||||
{time, remote_users} =
|
||||
:timer.tc(fn ->
|
||||
Repo.all(
|
||||
from(u in User,
|
||||
where: u.id != ^user.id,
|
||||
where: u.local == false,
|
||||
order_by: fragment("RANDOM()"),
|
||||
limit: 10
|
||||
)
|
||||
)
|
||||
end)
|
||||
|
||||
IO.puts("Fetching remote users take #{to_sec(time)} sec.\n")
|
||||
|
||||
generate_activities(user, users)
|
||||
|
||||
generate_remote_activities(user, remote_users)
|
||||
|
||||
generate_dms(user, users, opts)
|
||||
|
||||
{:ok, activity} = generate_long_thread(user, users, opts)
|
||||
|
||||
generate_non_visible_message(user, users)
|
||||
|
||||
IO.puts("Users in DB: #{Repo.aggregate(from(u in User), :count, :id)}")
|
||||
|
||||
IO.puts("Activities in DB: #{Repo.aggregate(from(a in Pleroma.Activity), :count, :id)}")
|
||||
|
||||
IO.puts("Objects in DB: #{Repo.aggregate(from(o in Pleroma.Object), :count, :id)}")
|
||||
|
||||
IO.puts(
|
||||
"Notifications in DB: #{Repo.aggregate(from(n in Pleroma.Notification), :count, :id)}"
|
||||
)
|
||||
|
||||
fetch_user(user)
|
||||
query_timelines(user)
|
||||
query_notifications(user)
|
||||
query_dms(user)
|
||||
query_long_thread(user, activity)
|
||||
Pleroma.Config.put([:instance, :skip_thread_containment], false)
|
||||
query_timelines(user)
|
||||
end
|
||||
|
||||
defp clean_tables do
|
||||
IO.puts("Deleting old data...\n")
|
||||
Ecto.Adapters.SQL.query!(Repo, "TRUNCATE users CASCADE;")
|
||||
Ecto.Adapters.SQL.query!(Repo, "TRUNCATE activities CASCADE;")
|
||||
Ecto.Adapters.SQL.query!(Repo, "TRUNCATE objects CASCADE;")
|
||||
end
|
||||
end
|
84
config/benchmark.exs
Normal file
84
config/benchmark.exs
Normal file
|
@ -0,0 +1,84 @@
|
|||
use Mix.Config
|
||||
|
||||
# We don't run a server during test. If one is required,
|
||||
# you can enable the server option below.
|
||||
config :pleroma, Pleroma.Web.Endpoint,
|
||||
http: [port: 4001],
|
||||
url: [port: 4001],
|
||||
server: true
|
||||
|
||||
# Disable captha for tests
|
||||
config :pleroma, Pleroma.Captcha,
|
||||
# It should not be enabled for automatic tests
|
||||
enabled: false,
|
||||
# A fake captcha service for tests
|
||||
method: Pleroma.Captcha.Mock
|
||||
|
||||
# Print only warnings and errors during test
|
||||
config :logger, level: :warn
|
||||
|
||||
config :pleroma, :auth, oauth_consumer_strategies: []
|
||||
|
||||
config :pleroma, Pleroma.Upload, filters: [], link_name: false
|
||||
|
||||
config :pleroma, Pleroma.Uploaders.Local, uploads: "test/uploads"
|
||||
|
||||
config :pleroma, Pleroma.Emails.Mailer, adapter: Swoosh.Adapters.Test, enabled: true
|
||||
|
||||
config :pleroma, :instance,
|
||||
email: "admin@example.com",
|
||||
notify_email: "noreply@example.com",
|
||||
skip_thread_containment: false,
|
||||
federating: false,
|
||||
external_user_synchronization: false
|
||||
|
||||
config :pleroma, :activitypub, sign_object_fetches: false
|
||||
|
||||
# Configure your database
|
||||
config :pleroma, Pleroma.Repo,
|
||||
adapter: Ecto.Adapters.Postgres,
|
||||
username: "postgres",
|
||||
password: "postgres",
|
||||
database: "pleroma_test",
|
||||
hostname: System.get_env("DB_HOST") || "localhost",
|
||||
pool_size: 10
|
||||
|
||||
# Reduce hash rounds for testing
|
||||
config :pbkdf2_elixir, rounds: 1
|
||||
|
||||
config :tesla, adapter: Tesla.Mock
|
||||
|
||||
config :pleroma, :rich_media,
|
||||
enabled: false,
|
||||
ignore_hosts: [],
|
||||
ignore_tld: ["local", "localdomain", "lan"]
|
||||
|
||||
config :web_push_encryption, :vapid_details,
|
||||
subject: "mailto:administrator@example.com",
|
||||
public_key:
|
||||
"BLH1qVhJItRGCfxgTtONfsOKDc9VRAraXw-3NsmjMngWSh7NxOizN6bkuRA7iLTMPS82PjwJAr3UoK9EC1IFrz4",
|
||||
private_key: "_-XZ0iebPrRfZ_o0-IatTdszYa8VCH1yLN-JauK7HHA"
|
||||
|
||||
config :web_push_encryption, :http_client, Pleroma.Web.WebPushHttpClientMock
|
||||
|
||||
config :pleroma_job_queue, disabled: true
|
||||
|
||||
config :pleroma, Pleroma.ScheduledActivity,
|
||||
daily_user_limit: 2,
|
||||
total_user_limit: 3,
|
||||
enabled: false
|
||||
|
||||
config :pleroma, :rate_limit,
|
||||
search: [{1000, 30}, {1000, 30}],
|
||||
app_account_creation: {10_000, 5},
|
||||
password_reset: {1000, 30}
|
||||
|
||||
config :pleroma, :http_security, report_uri: "https://endpoint.com"
|
||||
|
||||
config :pleroma, :http, send_user_agent: false
|
||||
|
||||
rum_enabled = System.get_env("RUM_ENABLED") == "true"
|
||||
config :pleroma, :database, rum_enabled: rum_enabled
|
||||
IO.puts("RUM enabled: #{rum_enabled}")
|
||||
|
||||
config :pleroma, Pleroma.ReverseProxy.Client, Pleroma.ReverseProxy.ClientMock
|
|
@ -47,7 +47,7 @@ Authentication is required and the user must be an admin.
|
|||
}
|
||||
```
|
||||
|
||||
## `/api/pleroma/admin/users`
|
||||
## DEPRECATED `DELETE /api/pleroma/admin/users`
|
||||
|
||||
### Remove a user
|
||||
|
||||
|
@ -56,6 +56,15 @@ Authentication is required and the user must be an admin.
|
|||
- `nickname`
|
||||
- Response: User’s nickname
|
||||
|
||||
## `DELETE /api/pleroma/admin/users`
|
||||
|
||||
### Remove a user
|
||||
|
||||
- Method `DELETE`
|
||||
- Params:
|
||||
- `nicknames`
|
||||
- Response: Array of user nicknames
|
||||
|
||||
### Create a user
|
||||
|
||||
- Method: `POST`
|
||||
|
@ -154,28 +163,86 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
|
|||
}
|
||||
```
|
||||
|
||||
### Add user in permission group
|
||||
## DEPRECATED `POST /api/pleroma/admin/users/:nickname/permission_group/:permission_group`
|
||||
|
||||
### Add user to permission group
|
||||
|
||||
- Method: `POST`
|
||||
- Params: none
|
||||
- Response:
|
||||
- On failure: `{"error": "…"}`
|
||||
- On success: JSON of the `user.info`
|
||||
|
||||
## `POST /api/pleroma/admin/users/permission_group/:permission_group`
|
||||
|
||||
### Add users to permission group
|
||||
|
||||
- Params:
|
||||
- `nicknames`: nicknames array
|
||||
- Response:
|
||||
- On failure: `{"error": "…"}`
|
||||
- On success: JSON of the `user.info`
|
||||
|
||||
## DEPRECATED `DELETE /api/pleroma/admin/users/:nickname/permission_group/:permission_group`
|
||||
|
||||
### Remove user from permission group
|
||||
|
||||
- Method: `DELETE`
|
||||
- Params: none
|
||||
- Response:
|
||||
- On failure: `{"error": "…"}`
|
||||
- On success: JSON of the `user.info`
|
||||
- Note: An admin cannot revoke their own admin status.
|
||||
|
||||
## `/api/pleroma/admin/users/:nickname/activation_status`
|
||||
## `DELETE /api/pleroma/admin/users/permission_group/:permission_group`
|
||||
|
||||
### Remove users from permission group
|
||||
|
||||
- Params:
|
||||
- `nicknames`: nicknames array
|
||||
- Response:
|
||||
- On failure: `{"error": "…"}`
|
||||
- On success: JSON of the `user.info`
|
||||
- Note: An admin cannot revoke their own admin status.
|
||||
|
||||
## `PATCH /api/pleroma/admin/users/activate`
|
||||
|
||||
### Activate user
|
||||
|
||||
- Params:
|
||||
- `nicknames`: nicknames array
|
||||
- Response:
|
||||
|
||||
```json
|
||||
{
|
||||
users: [
|
||||
{
|
||||
// user object
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## `PATCH /api/pleroma/admin/users/deactivate`
|
||||
|
||||
### Deactivate user
|
||||
|
||||
- Params:
|
||||
- `nicknames`: nicknames array
|
||||
- Response:
|
||||
|
||||
```json
|
||||
{
|
||||
users: [
|
||||
{
|
||||
// user object
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## DEPRECATED `PATCH /api/pleroma/admin/users/:nickname/activation_status`
|
||||
|
||||
### Active or deactivate a user
|
||||
|
||||
- Method: `PUT`
|
||||
- Params:
|
||||
- `nickname`
|
||||
- `status` BOOLEAN field, false value means deactivation.
|
||||
|
|
|
@ -13,6 +13,7 @@ Some apps operate under the assumption that no more than 4 attachments can be re
|
|||
## Timelines
|
||||
|
||||
Adding the parameter `with_muted=true` to the timeline queries will also return activities by muted (not by blocked!) users.
|
||||
Adding the parameter `exclude_visibilities` to the timeline queries will exclude the statuses with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`), e.g., `exclude_visibilities[]=direct&exclude_visibilities[]=private`.
|
||||
|
||||
## Statuses
|
||||
|
||||
|
@ -84,6 +85,12 @@ Has these additional fields under the `pleroma` object:
|
|||
|
||||
- `is_seen`: true if the notification was read by the user
|
||||
|
||||
## GET `/api/v1/notifications`
|
||||
|
||||
Accepts additional parameters:
|
||||
|
||||
- `exclude_visibilities`: will exclude the notifications for activities with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`). Usage example: `GET /api/v1/notifications?exclude_visibilities[]=direct&exclude_visibilities[]=private`.
|
||||
|
||||
## POST `/api/v1/statuses`
|
||||
|
||||
Additional parameters can be added to the JSON body/Form data:
|
||||
|
|
|
@ -91,7 +91,7 @@ sudo adduser -S -s /bin/false -h /opt/pleroma -H -G pleroma pleroma
|
|||
```shell
|
||||
sudo mkdir -p /opt/pleroma
|
||||
sudo chown -R pleroma:pleroma /opt/pleroma
|
||||
sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma
|
||||
sudo -Hu pleroma git clone -b stable https://git.pleroma.social/pleroma/pleroma /opt/pleroma
|
||||
```
|
||||
|
||||
* Change to the new directory:
|
||||
|
|
|
@ -66,7 +66,7 @@ sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma
|
|||
```shell
|
||||
sudo mkdir -p /opt/pleroma
|
||||
sudo chown -R pleroma:pleroma /opt/pleroma
|
||||
sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma
|
||||
sudo -Hu pleroma git clone -b stable https://git.pleroma.social/pleroma/pleroma /opt/pleroma
|
||||
```
|
||||
|
||||
* Change to the new directory:
|
||||
|
|
|
@ -143,7 +143,7 @@ sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma
|
|||
```shell
|
||||
sudo mkdir -p /opt/pleroma
|
||||
sudo chown -R pleroma:pleroma /opt/pleroma
|
||||
sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma
|
||||
sudo -Hu pleroma git clone -b stable https://git.pleroma.social/pleroma/pleroma /opt/pleroma
|
||||
```
|
||||
|
||||
* Change to the new directory:
|
||||
|
|
|
@ -68,7 +68,7 @@ sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma
|
|||
```shell
|
||||
sudo mkdir -p /opt/pleroma
|
||||
sudo chown -R pleroma:pleroma /opt/pleroma
|
||||
sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma
|
||||
sudo -Hu pleroma git clone -b stable https://git.pleroma.social/pleroma/pleroma /opt/pleroma
|
||||
```
|
||||
|
||||
* Change to the new directory:
|
||||
|
|
|
@ -68,7 +68,7 @@ sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma
|
|||
```
|
||||
sudo mkdir -p /opt/pleroma
|
||||
sudo chown -R pleroma:pleroma /opt/pleroma
|
||||
sudo -Hu pleroma git clone -b master https://git.pleroma.social/pleroma/pleroma /opt/pleroma
|
||||
sudo -Hu pleroma git clone -b stable https://git.pleroma.social/pleroma/pleroma /opt/pleroma
|
||||
```
|
||||
|
||||
* 新しいディレクトリに移動します。
|
||||
|
|
|
@ -106,7 +106,7 @@ It is highly recommended you use your own fork for the `https://path/to/repo` pa
|
|||
|
||||
```shell
|
||||
pleroma$ cd ~
|
||||
pleroma$ git clone -b master https://path/to/repo
|
||||
pleroma$ git clone -b stable https://path/to/repo
|
||||
```
|
||||
|
||||
* Change to the new directory:
|
||||
|
|
|
@ -96,9 +96,9 @@ rm -r ~pleroma/*
|
|||
export FLAVOUR="arm64-musl"
|
||||
|
||||
# Clone the release build into a temporary directory and unpack it
|
||||
# Replace `master` with `develop` if you want to run the develop branch
|
||||
# Replace `stable` with `unstable` if you want to run the unstable branch
|
||||
su pleroma -s $SHELL -lc "
|
||||
curl 'https://git.pleroma.social/api/v4/projects/2/jobs/artifacts/master/download?job=$FLAVOUR' -o /tmp/pleroma.zip
|
||||
curl 'https://git.pleroma.social/api/v4/projects/2/jobs/artifacts/stable/download?job=$FLAVOUR' -o /tmp/pleroma.zip
|
||||
unzip /tmp/pleroma.zip -d /tmp/
|
||||
"
|
||||
|
||||
|
|
|
@ -58,7 +58,7 @@ Clone the repository:
|
|||
|
||||
```
|
||||
$ cd /home/pleroma
|
||||
$ git clone -b master https://git.pleroma.social/pleroma/pleroma.git
|
||||
$ git clone -b stable https://git.pleroma.social/pleroma/pleroma.git
|
||||
```
|
||||
|
||||
Configure Pleroma. Note that you need a domain name at this point:
|
||||
|
|
|
@ -29,7 +29,7 @@ This creates a "pleroma" login class and sets higher values than default for dat
|
|||
Create the \_pleroma user, assign it the pleroma login class and create its home directory (/home/\_pleroma/): `useradd -m -L pleroma _pleroma`
|
||||
|
||||
#### Clone pleroma's directory
|
||||
Enter a shell as the \_pleroma user. As root, run `su _pleroma -;cd`. Then clone the repository with `git clone -b master https://git.pleroma.social/pleroma/pleroma.git`. Pleroma is now installed in /home/\_pleroma/pleroma/, it will be configured and started at the end of this guide.
|
||||
Enter a shell as the \_pleroma user. As root, run `su _pleroma -;cd`. Then clone the repository with `git clone -b stable https://git.pleroma.social/pleroma/pleroma.git`. Pleroma is now installed in /home/\_pleroma/pleroma/, it will be configured and started at the end of this guide.
|
||||
|
||||
#### Postgresql
|
||||
Start a shell as the \_postgresql user (as root run `su _postgresql -` then run the `initdb` command to initialize postgresql:
|
||||
|
|
|
@ -44,7 +44,7 @@ Vaihda pleroma-käyttäjään ja mene kotihakemistoosi:
|
|||
|
||||
Lataa pleroman lähdekoodi:
|
||||
|
||||
`$ git clone -b master https://git.pleroma.social/pleroma/pleroma.git`
|
||||
`$ git clone -b stable https://git.pleroma.social/pleroma/pleroma.git`
|
||||
|
||||
`$ cd pleroma`
|
||||
|
||||
|
|
|
@ -80,7 +80,7 @@ export FLAVOUR="arm64-musl"
|
|||
|
||||
# Clone the release build into a temporary directory and unpack it
|
||||
su pleroma -s $SHELL -lc "
|
||||
curl 'https://git.pleroma.social/api/v4/projects/2/jobs/artifacts/master/download?job=$FLAVOUR' -o /tmp/pleroma.zip
|
||||
curl 'https://git.pleroma.social/api/v4/projects/2/jobs/artifacts/stable/download?job=$FLAVOUR' -o /tmp/pleroma.zip
|
||||
unzip /tmp/pleroma.zip -d /tmp/
|
||||
"
|
||||
|
||||
|
|
|
@ -48,6 +48,12 @@ def read_cng(struct, params) do
|
|||
|> validate_required([:read])
|
||||
end
|
||||
|
||||
def mark_as_read(%User{} = user, %Conversation{} = conversation) do
|
||||
with %__MODULE__{} = participation <- for_user_and_conversation(user, conversation) do
|
||||
mark_as_read(participation)
|
||||
end
|
||||
end
|
||||
|
||||
def mark_as_read(participation) do
|
||||
participation
|
||||
|> read_cng(%{read: true})
|
||||
|
|
|
@ -86,18 +86,18 @@ defp parse_datetime(datetime) do
|
|||
parsed_datetime
|
||||
end
|
||||
|
||||
@spec insert_log(%{actor: User, subject: User, action: String.t(), permission: String.t()}) ::
|
||||
@spec insert_log(%{actor: User, subject: [User], action: String.t(), permission: String.t()}) ::
|
||||
{:ok, ModerationLog} | {:error, any}
|
||||
def insert_log(%{
|
||||
actor: %User{} = actor,
|
||||
subject: %User{} = subject,
|
||||
subject: subjects,
|
||||
action: action,
|
||||
permission: permission
|
||||
}) do
|
||||
%ModerationLog{
|
||||
data: %{
|
||||
"actor" => user_to_map(actor),
|
||||
"subject" => user_to_map(subject),
|
||||
"subject" => user_to_map(subjects),
|
||||
"action" => action,
|
||||
"permission" => permission,
|
||||
"message" => ""
|
||||
|
@ -303,13 +303,16 @@ def insert_log(%{
|
|||
end
|
||||
|
||||
@spec insert_log_entry_with_message(ModerationLog) :: {:ok, ModerationLog} | {:error, any}
|
||||
|
||||
defp insert_log_entry_with_message(entry) do
|
||||
entry.data["message"]
|
||||
|> put_in(get_log_entry_message(entry))
|
||||
|> Repo.insert()
|
||||
end
|
||||
|
||||
defp user_to_map(users) when is_list(users) do
|
||||
users |> Enum.map(&user_to_map/1)
|
||||
end
|
||||
|
||||
defp user_to_map(%User{} = user) do
|
||||
user
|
||||
|> Map.from_struct()
|
||||
|
@ -349,10 +352,10 @@ def get_log_entry_message(%ModerationLog{
|
|||
data: %{
|
||||
"actor" => %{"nickname" => actor_nickname},
|
||||
"action" => "delete",
|
||||
"subject" => %{"nickname" => subject_nickname, "type" => "user"}
|
||||
"subject" => subjects
|
||||
}
|
||||
}) do
|
||||
"@#{actor_nickname} deleted user @#{subject_nickname}"
|
||||
"@#{actor_nickname} deleted users: #{users_to_nicknames_string(subjects)}"
|
||||
end
|
||||
|
||||
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||
|
@ -363,12 +366,7 @@ def get_log_entry_message(%ModerationLog{
|
|||
"subjects" => subjects
|
||||
}
|
||||
}) do
|
||||
nicknames =
|
||||
subjects
|
||||
|> Enum.map(&"@#{&1["nickname"]}")
|
||||
|> Enum.join(", ")
|
||||
|
||||
"@#{actor_nickname} created users: #{nicknames}"
|
||||
"@#{actor_nickname} created users: #{users_to_nicknames_string(subjects)}"
|
||||
end
|
||||
|
||||
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||
|
@ -376,10 +374,10 @@ def get_log_entry_message(%ModerationLog{
|
|||
data: %{
|
||||
"actor" => %{"nickname" => actor_nickname},
|
||||
"action" => "activate",
|
||||
"subject" => %{"nickname" => subject_nickname, "type" => "user"}
|
||||
"subject" => users
|
||||
}
|
||||
}) do
|
||||
"@#{actor_nickname} activated user @#{subject_nickname}"
|
||||
"@#{actor_nickname} activated users: #{users_to_nicknames_string(users)}"
|
||||
end
|
||||
|
||||
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||
|
@ -387,10 +385,10 @@ def get_log_entry_message(%ModerationLog{
|
|||
data: %{
|
||||
"actor" => %{"nickname" => actor_nickname},
|
||||
"action" => "deactivate",
|
||||
"subject" => %{"nickname" => subject_nickname, "type" => "user"}
|
||||
"subject" => users
|
||||
}
|
||||
}) do
|
||||
"@#{actor_nickname} deactivated user @#{subject_nickname}"
|
||||
"@#{actor_nickname} deactivated users: #{users_to_nicknames_string(users)}"
|
||||
end
|
||||
|
||||
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||
|
@ -402,14 +400,9 @@ def get_log_entry_message(%ModerationLog{
|
|||
"action" => "tag"
|
||||
}
|
||||
}) do
|
||||
nicknames_string =
|
||||
nicknames
|
||||
|> Enum.map(&"@#{&1}")
|
||||
|> Enum.join(", ")
|
||||
|
||||
tags_string = tags |> Enum.join(", ")
|
||||
|
||||
"@#{actor_nickname} added tags: #{tags_string} to users: #{nicknames_string}"
|
||||
"@#{actor_nickname} added tags: #{tags_string} to users: #{nicknames_to_string(nicknames)}"
|
||||
end
|
||||
|
||||
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||
|
@ -421,14 +414,9 @@ def get_log_entry_message(%ModerationLog{
|
|||
"action" => "untag"
|
||||
}
|
||||
}) do
|
||||
nicknames_string =
|
||||
nicknames
|
||||
|> Enum.map(&"@#{&1}")
|
||||
|> Enum.join(", ")
|
||||
|
||||
tags_string = tags |> Enum.join(", ")
|
||||
|
||||
"@#{actor_nickname} removed tags: #{tags_string} from users: #{nicknames_string}"
|
||||
"@#{actor_nickname} removed tags: #{tags_string} from users: #{nicknames_to_string(nicknames)}"
|
||||
end
|
||||
|
||||
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||
|
@ -436,11 +424,11 @@ def get_log_entry_message(%ModerationLog{
|
|||
data: %{
|
||||
"actor" => %{"nickname" => actor_nickname},
|
||||
"action" => "grant",
|
||||
"subject" => %{"nickname" => subject_nickname},
|
||||
"subject" => users,
|
||||
"permission" => permission
|
||||
}
|
||||
}) do
|
||||
"@#{actor_nickname} made @#{subject_nickname} #{permission}"
|
||||
"@#{actor_nickname} made #{users_to_nicknames_string(users)} #{permission}"
|
||||
end
|
||||
|
||||
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||
|
@ -448,11 +436,11 @@ def get_log_entry_message(%ModerationLog{
|
|||
data: %{
|
||||
"actor" => %{"nickname" => actor_nickname},
|
||||
"action" => "revoke",
|
||||
"subject" => %{"nickname" => subject_nickname},
|
||||
"subject" => users,
|
||||
"permission" => permission
|
||||
}
|
||||
}) do
|
||||
"@#{actor_nickname} revoked #{permission} role from @#{subject_nickname}"
|
||||
"@#{actor_nickname} revoked #{permission} role from #{users_to_nicknames_string(users)}"
|
||||
end
|
||||
|
||||
@spec get_log_entry_message(ModerationLog) :: String.t()
|
||||
|
@ -551,4 +539,16 @@ def get_log_entry_message(%ModerationLog{
|
|||
}) do
|
||||
"@#{actor_nickname} deleted status ##{subject_id}"
|
||||
end
|
||||
|
||||
defp nicknames_to_string(nicknames) do
|
||||
nicknames
|
||||
|> Enum.map(&"@#{&1}")
|
||||
|> Enum.join(", ")
|
||||
end
|
||||
|
||||
defp users_to_nicknames_string(users) do
|
||||
users
|
||||
|> Enum.map(&"@#{&1["nickname"]}")
|
||||
|> Enum.join(", ")
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,6 +17,7 @@ defmodule Pleroma.Notification do
|
|||
|
||||
import Ecto.Query
|
||||
import Ecto.Changeset
|
||||
require Logger
|
||||
|
||||
@type t :: %__MODULE__{}
|
||||
|
||||
|
@ -34,43 +35,92 @@ def changeset(%Notification{} = notification, attrs) do
|
|||
end
|
||||
|
||||
def for_user_query(user, opts \\ []) do
|
||||
query =
|
||||
Notification
|
||||
|> where(user_id: ^user.id)
|
||||
Notification
|
||||
|> where(user_id: ^user.id)
|
||||
|> where(
|
||||
[n, a],
|
||||
fragment(
|
||||
"? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')",
|
||||
a.actor
|
||||
)
|
||||
)
|
||||
|> join(:inner, [n], activity in assoc(n, :activity))
|
||||
|> join(:left, [n, a], object in Object,
|
||||
on:
|
||||
fragment(
|
||||
"(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
|
||||
object.data,
|
||||
a.data
|
||||
)
|
||||
)
|
||||
|> preload([n, a, o], activity: {a, object: o})
|
||||
|> exclude_muted(user, opts)
|
||||
|> exclude_visibility(opts)
|
||||
end
|
||||
|
||||
defp exclude_muted(query, _, %{with_muted: true}) do
|
||||
query
|
||||
end
|
||||
|
||||
defp exclude_muted(query, user, _opts) do
|
||||
query
|
||||
|> where([n, a], a.actor not in ^user.info.muted_notifications)
|
||||
|> where([n, a], a.actor not in ^user.info.blocks)
|
||||
|> where(
|
||||
[n, a],
|
||||
fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.info.domain_blocks
|
||||
)
|
||||
|> join(:left, [n, a], tm in Pleroma.ThreadMute,
|
||||
on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data)
|
||||
)
|
||||
|> where([n, a, o, tm], is_nil(tm.user_id))
|
||||
end
|
||||
|
||||
@valid_visibilities ~w[direct unlisted public private]
|
||||
|
||||
defp exclude_visibility(query, %{exclude_visibilities: visibility})
|
||||
when is_list(visibility) do
|
||||
if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
|
||||
query
|
||||
|> where(
|
||||
[n, a],
|
||||
fragment(
|
||||
"? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')",
|
||||
a.actor
|
||||
not fragment(
|
||||
"activity_visibility(?, ?, ?) = ANY (?)",
|
||||
a.actor,
|
||||
a.recipients,
|
||||
a.data,
|
||||
^visibility
|
||||
)
|
||||
)
|
||||
|> join(:inner, [n], activity in assoc(n, :activity))
|
||||
|> join(:left, [n, a], object in Object,
|
||||
on:
|
||||
fragment(
|
||||
"(?->>'id') = COALESCE((? -> 'object'::text) ->> 'id'::text)",
|
||||
object.data,
|
||||
a.data
|
||||
)
|
||||
)
|
||||
|> preload([n, a, o], activity: {a, object: o})
|
||||
|
||||
if opts[:with_muted] do
|
||||
query
|
||||
else
|
||||
where(query, [n, a], a.actor not in ^user.info.muted_notifications)
|
||||
|> where([n, a], a.actor not in ^user.info.blocks)
|
||||
|> where(
|
||||
[n, a],
|
||||
fragment("substring(? from '.*://([^/]*)')", a.actor) not in ^user.info.domain_blocks
|
||||
)
|
||||
|> join(:left, [n, a], tm in Pleroma.ThreadMute,
|
||||
on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data)
|
||||
)
|
||||
|> where([n, a, o, tm], is_nil(tm.user_id))
|
||||
Logger.error("Could not exclude visibility to #{visibility}")
|
||||
query
|
||||
end
|
||||
end
|
||||
|
||||
defp exclude_visibility(query, %{exclude_visibilities: visibility})
|
||||
when visibility in @valid_visibilities do
|
||||
query
|
||||
|> where(
|
||||
[n, a],
|
||||
not fragment(
|
||||
"activity_visibility(?, ?, ?) = (?)",
|
||||
a.actor,
|
||||
a.recipients,
|
||||
a.data,
|
||||
^visibility
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
defp exclude_visibility(query, %{exclude_visibilities: visibility})
|
||||
when visibility not in @valid_visibilities do
|
||||
Logger.error("Could not exclude visibility to #{visibility}")
|
||||
query
|
||||
end
|
||||
|
||||
defp exclude_visibility(query, _visibility), do: query
|
||||
|
||||
def for_user(user, opts \\ %{}) do
|
||||
user
|
||||
|> for_user_query(opts)
|
||||
|
|
|
@ -401,11 +401,9 @@ defp increase_read_duration(_) do
|
|||
|
||||
defp client, do: Pleroma.ReverseProxy.Client
|
||||
|
||||
defp track_failed_url(url, code, opts) do
|
||||
code = to_string(code)
|
||||
|
||||
defp track_failed_url(url, error, opts) do
|
||||
ttl =
|
||||
if code in ["403", "404"] or String.starts_with?(code, "5") do
|
||||
unless error in [:body_too_large, 400, 204] do
|
||||
Keyword.get(opts, :failed_request_ttl, @failed_request_ttl)
|
||||
else
|
||||
nil
|
||||
|
|
|
@ -437,7 +437,9 @@ def follow(%User{} = follower, %User{info: info} = followed) do
|
|||
{:error, "Could not follow user: #{followed.nickname} blocked you."}
|
||||
|
||||
true ->
|
||||
if !followed.local && follower.local && !ap_enabled?(followed) do
|
||||
benchmark? = Pleroma.Config.get([:env]) == :benchmark
|
||||
|
||||
if !followed.local && follower.local && !ap_enabled?(followed) && !benchmark? do
|
||||
Websub.subscribe(follower, followed)
|
||||
end
|
||||
|
||||
|
@ -1059,7 +1061,15 @@ def deactivate_async(user, status \\ true) do
|
|||
BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
|
||||
end
|
||||
|
||||
def deactivate(%User{} = user, status \\ true) do
|
||||
def deactivate(user, status \\ true)
|
||||
|
||||
def deactivate(users, status) when is_list(users) do
|
||||
Repo.transaction(fn ->
|
||||
for user <- users, do: deactivate(user, status)
|
||||
end)
|
||||
end
|
||||
|
||||
def deactivate(%User{} = user, status) do
|
||||
with {:ok, user} <- update_info(user, &User.Info.set_activation_status(&1, status)) do
|
||||
Enum.each(get_followers(user), &invalidate_cache/1)
|
||||
Enum.each(get_friends(user), &update_follower_count/1)
|
||||
|
@ -1072,6 +1082,10 @@ def update_notification_settings(%User{} = user, settings \\ %{}) do
|
|||
update_info(user, &User.Info.update_notification_settings(&1, settings))
|
||||
end
|
||||
|
||||
def delete(users) when is_list(users) do
|
||||
for user <- users, do: delete(user)
|
||||
end
|
||||
|
||||
def delete(%User{} = user) do
|
||||
BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
|
||||
end
|
||||
|
@ -1625,6 +1639,12 @@ def change_info(user, fun) do
|
|||
|
||||
`fun` is called with the `user.info`.
|
||||
"""
|
||||
def update_info(users, fun) when is_list(users) do
|
||||
Repo.transaction(fn ->
|
||||
for user <- users, do: update_info(user, fun)
|
||||
end)
|
||||
end
|
||||
|
||||
def update_info(user, fun) do
|
||||
user
|
||||
|> change_info(fun)
|
||||
|
|
|
@ -4,11 +4,9 @@
|
|||
|
||||
defmodule Pleroma.User.Search do
|
||||
alias Pleroma.Pagination
|
||||
alias Pleroma.Repo
|
||||
alias Pleroma.User
|
||||
import Ecto.Query
|
||||
|
||||
@similarity_threshold 0.25
|
||||
@limit 20
|
||||
|
||||
def search(query_string, opts \\ []) do
|
||||
|
@ -23,18 +21,10 @@ def search(query_string, opts \\ []) do
|
|||
|
||||
maybe_resolve(resolve, for_user, query_string)
|
||||
|
||||
{:ok, results} =
|
||||
Repo.transaction(fn ->
|
||||
Ecto.Adapters.SQL.query(
|
||||
Repo,
|
||||
"select set_limit(#{@similarity_threshold})",
|
||||
[]
|
||||
)
|
||||
|
||||
query_string
|
||||
|> search_query(for_user, following)
|
||||
|> Pagination.fetch_paginated(%{"offset" => offset, "limit" => result_limit}, :offset)
|
||||
end)
|
||||
results =
|
||||
query_string
|
||||
|> search_query(for_user, following)
|
||||
|> Pagination.fetch_paginated(%{"offset" => offset, "limit" => result_limit}, :offset)
|
||||
|
||||
results
|
||||
end
|
||||
|
@ -56,15 +46,65 @@ defp search_query(query_string, for_user, following) do
|
|||
|> base_query(following)
|
||||
|> filter_blocked_user(for_user)
|
||||
|> filter_blocked_domains(for_user)
|
||||
|> search_subqueries(query_string)
|
||||
|> union_subqueries
|
||||
|> distinct_query()
|
||||
|> boost_search_rank_query(for_user)
|
||||
|> fts_search(query_string)
|
||||
|> trigram_rank(query_string)
|
||||
|> boost_search_rank(for_user)
|
||||
|> subquery()
|
||||
|> order_by(desc: :search_rank)
|
||||
|> maybe_restrict_local(for_user)
|
||||
end
|
||||
|
||||
@nickname_regex ~r/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~\-@]+$/
|
||||
defp fts_search(query, query_string) do
|
||||
{nickname_weight, name_weight} =
|
||||
if String.match?(query_string, @nickname_regex) do
|
||||
{"A", "B"}
|
||||
else
|
||||
{"B", "A"}
|
||||
end
|
||||
|
||||
query_string = to_tsquery(query_string)
|
||||
|
||||
from(
|
||||
u in query,
|
||||
where:
|
||||
fragment(
|
||||
"""
|
||||
(setweight(to_tsvector('simple', ?), ?) || setweight(to_tsvector('simple', ?), ?)) @@ to_tsquery('simple', ?)
|
||||
""",
|
||||
u.name,
|
||||
^name_weight,
|
||||
u.nickname,
|
||||
^nickname_weight,
|
||||
^query_string
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
defp to_tsquery(query_string) do
|
||||
String.trim_trailing(query_string, "@" <> local_domain())
|
||||
|> String.replace(~r/[!-\/|@|[-`|{-~|:-?]+/, " ")
|
||||
|> String.trim()
|
||||
|> String.split()
|
||||
|> Enum.map(&(&1 <> ":*"))
|
||||
|> Enum.join(" | ")
|
||||
end
|
||||
|
||||
defp trigram_rank(query, query_string) do
|
||||
from(
|
||||
u in query,
|
||||
select_merge: %{
|
||||
search_rank:
|
||||
fragment(
|
||||
"similarity(?, trim(? || ' ' || coalesce(?, '')))",
|
||||
^query_string,
|
||||
u.nickname,
|
||||
u.name
|
||||
)
|
||||
}
|
||||
)
|
||||
end
|
||||
|
||||
defp base_query(_user, false), do: User
|
||||
defp base_query(user, true), do: User.get_followers_query(user)
|
||||
|
||||
|
@ -87,21 +127,6 @@ defp filter_blocked_domains(query, %User{info: %{domain_blocks: domain_blocks}})
|
|||
|
||||
defp filter_blocked_domains(query, _), do: query
|
||||
|
||||
defp union_subqueries({fts_subquery, trigram_subquery}) do
|
||||
from(s in trigram_subquery, union_all: ^fts_subquery)
|
||||
end
|
||||
|
||||
defp search_subqueries(base_query, query_string) do
|
||||
{
|
||||
fts_search_subquery(base_query, query_string),
|
||||
trigram_search_subquery(base_query, query_string)
|
||||
}
|
||||
end
|
||||
|
||||
defp distinct_query(q) do
|
||||
from(s in subquery(q), order_by: s.search_type, distinct: s.id)
|
||||
end
|
||||
|
||||
defp maybe_resolve(true, user, query) do
|
||||
case {limit(), user} do
|
||||
{:all, _} -> :noop
|
||||
|
@ -126,9 +151,9 @@ defp limit, do: Pleroma.Config.get([:instance, :limit_to_local_content], :unauth
|
|||
|
||||
defp restrict_local(q), do: where(q, [u], u.local == true)
|
||||
|
||||
defp boost_search_rank_query(query, nil), do: query
|
||||
defp local_domain, do: Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host])
|
||||
|
||||
defp boost_search_rank_query(query, for_user) do
|
||||
defp boost_search_rank(query, %User{} = for_user) do
|
||||
friends_ids = User.get_friends_ids(for_user)
|
||||
followers_ids = User.get_followers_ids(for_user)
|
||||
|
||||
|
@ -137,8 +162,8 @@ defp boost_search_rank_query(query, for_user) do
|
|||
search_rank:
|
||||
fragment(
|
||||
"""
|
||||
CASE WHEN (?) THEN 0.5 + (?) * 1.3
|
||||
WHEN (?) THEN 0.5 + (?) * 1.2
|
||||
CASE WHEN (?) THEN (?) * 1.5
|
||||
WHEN (?) THEN (?) * 1.3
|
||||
WHEN (?) THEN (?) * 1.1
|
||||
ELSE (?) END
|
||||
""",
|
||||
|
@ -154,70 +179,5 @@ defp boost_search_rank_query(query, for_user) do
|
|||
)
|
||||
end
|
||||
|
||||
@spec fts_search_subquery(User.t() | Ecto.Query.t(), String.t()) :: Ecto.Query.t()
|
||||
defp fts_search_subquery(query, term) do
|
||||
processed_query =
|
||||
String.trim_trailing(term, "@" <> local_domain())
|
||||
|> String.replace(~r/[!-\/|@|[-`|{-~|:-?]+/, " ")
|
||||
|> String.trim()
|
||||
|> String.split()
|
||||
|> Enum.map(&(&1 <> ":*"))
|
||||
|> Enum.join(" | ")
|
||||
|
||||
from(
|
||||
u in query,
|
||||
select_merge: %{
|
||||
search_type: ^0,
|
||||
search_rank:
|
||||
fragment(
|
||||
"""
|
||||
ts_rank_cd(
|
||||
setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
|
||||
setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B'),
|
||||
to_tsquery('simple', ?),
|
||||
32
|
||||
)
|
||||
""",
|
||||
u.nickname,
|
||||
u.name,
|
||||
^processed_query
|
||||
)
|
||||
},
|
||||
where:
|
||||
fragment(
|
||||
"""
|
||||
(setweight(to_tsvector('simple', regexp_replace(?, '\\W', ' ', 'g')), 'A') ||
|
||||
setweight(to_tsvector('simple', regexp_replace(coalesce(?, ''), '\\W', ' ', 'g')), 'B')) @@ to_tsquery('simple', ?)
|
||||
""",
|
||||
u.nickname,
|
||||
u.name,
|
||||
^processed_query
|
||||
)
|
||||
)
|
||||
|> User.restrict_deactivated()
|
||||
end
|
||||
|
||||
@spec trigram_search_subquery(User.t() | Ecto.Query.t(), String.t()) :: Ecto.Query.t()
|
||||
defp trigram_search_subquery(query, term) do
|
||||
term = String.trim_trailing(term, "@" <> local_domain())
|
||||
|
||||
from(
|
||||
u in query,
|
||||
select_merge: %{
|
||||
# ^1 gives 'Postgrex expected a binary, got 1' for some weird reason
|
||||
search_type: fragment("?", 1),
|
||||
search_rank:
|
||||
fragment(
|
||||
"similarity(?, trim(? || ' ' || coalesce(?, '')))",
|
||||
^term,
|
||||
u.nickname,
|
||||
u.name
|
||||
)
|
||||
},
|
||||
where: fragment("trim(? || ' ' || coalesce(?, '')) % ?", u.nickname, u.name, ^term)
|
||||
)
|
||||
|> User.restrict_deactivated()
|
||||
end
|
||||
|
||||
defp local_domain, do: Pleroma.Config.get([Pleroma.Web.Endpoint, :url, :host])
|
||||
defp boost_search_rank(query, _for_user), do: query
|
||||
end
|
||||
|
|
|
@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|
|||
alias Pleroma.Activity.Ir.Topics
|
||||
alias Pleroma.Config
|
||||
alias Pleroma.Conversation
|
||||
alias Pleroma.Conversation.Participation
|
||||
alias Pleroma.Notification
|
||||
alias Pleroma.Object
|
||||
alias Pleroma.Object.Containment
|
||||
|
@ -153,11 +154,8 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when
|
|||
|
||||
Notification.create_notifications(activity)
|
||||
|
||||
participations =
|
||||
activity
|
||||
|> Conversation.create_or_bump_for()
|
||||
|> get_participations()
|
||||
|
||||
conversation = create_or_bump_conversation(activity, map["actor"])
|
||||
participations = get_participations(conversation)
|
||||
stream_out(activity)
|
||||
stream_out_participations(participations)
|
||||
{:ok, activity}
|
||||
|
@ -182,7 +180,20 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when
|
|||
end
|
||||
end
|
||||
|
||||
defp get_participations({:ok, %{participations: participations}}), do: participations
|
||||
defp create_or_bump_conversation(activity, actor) do
|
||||
with {:ok, conversation} <- Conversation.create_or_bump_for(activity),
|
||||
%User{} = user <- User.get_cached_by_ap_id(actor),
|
||||
Participation.mark_as_read(user, conversation) do
|
||||
{:ok, conversation}
|
||||
end
|
||||
end
|
||||
|
||||
defp get_participations({:ok, conversation}) do
|
||||
conversation
|
||||
|> Repo.preload(:participations, force: true)
|
||||
|> Map.get(:participations)
|
||||
end
|
||||
|
||||
defp get_participations(_), do: []
|
||||
|
||||
def stream_out_participations(participations) do
|
||||
|
@ -225,6 +236,7 @@ def create(%{to: to, actor: actor, context: context, object: object} = params, f
|
|||
# only accept false as false value
|
||||
local = !(params[:local] == false)
|
||||
published = params[:published]
|
||||
quick_insert? = Pleroma.Config.get([:env]) == :benchmark
|
||||
|
||||
with create_data <-
|
||||
make_create_data(
|
||||
|
@ -235,12 +247,16 @@ def create(%{to: to, actor: actor, context: context, object: object} = params, f
|
|||
{:fake, false, activity} <- {:fake, fake, activity},
|
||||
_ <- increase_replies_count_if_reply(create_data),
|
||||
_ <- increase_poll_votes_if_vote(create_data),
|
||||
{:quick_insert, false, activity} <- {:quick_insert, quick_insert?, activity},
|
||||
# Changing note count prior to enqueuing federation task in order to avoid
|
||||
# race conditions on updating user.info
|
||||
{:ok, _actor} <- increase_note_count_if_public(actor, activity),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
else
|
||||
{:quick_insert, true, activity} ->
|
||||
{:ok, activity}
|
||||
|
||||
{:fake, true, activity} ->
|
||||
{:ok, activity}
|
||||
|
||||
|
@ -269,22 +285,21 @@ def listen(%{to: to, actor: actor, context: context, object: object} = params) d
|
|||
end
|
||||
end
|
||||
|
||||
def accept(%{to: to, actor: actor, object: object} = params) do
|
||||
# only accept false as false value
|
||||
local = !(params[:local] == false)
|
||||
|
||||
with data <- %{"to" => to, "type" => "Accept", "actor" => actor.ap_id, "object" => object},
|
||||
{:ok, activity} <- insert(data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
end
|
||||
def accept(params) do
|
||||
accept_or_reject("Accept", params)
|
||||
end
|
||||
|
||||
def reject(%{to: to, actor: actor, object: object} = params) do
|
||||
# only accept false as false value
|
||||
local = !(params[:local] == false)
|
||||
def reject(params) do
|
||||
accept_or_reject("Reject", params)
|
||||
end
|
||||
|
||||
with data <- %{"to" => to, "type" => "Reject", "actor" => actor.ap_id, "object" => object},
|
||||
def accept_or_reject(type, %{to: to, actor: actor, object: object} = params) do
|
||||
local = Map.get(params, :local, true)
|
||||
activity_id = Map.get(params, :activity_id, nil)
|
||||
|
||||
with data <-
|
||||
%{"to" => to, "type" => type, "actor" => actor.ap_id, "object" => object}
|
||||
|> Utils.maybe_put("id", activity_id),
|
||||
{:ok, activity} <- insert(data, local),
|
||||
:ok <- maybe_federate(activity) do
|
||||
{:ok, activity}
|
||||
|
@ -409,18 +424,24 @@ def delete(%User{ap_id: ap_id, follower_address: follower_address} = user) do
|
|||
end
|
||||
end
|
||||
|
||||
def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, local \\ true) do
|
||||
def delete(%Object{data: %{"id" => id, "actor" => actor}} = object, options \\ []) do
|
||||
local = Keyword.get(options, :local, true)
|
||||
activity_id = Keyword.get(options, :activity_id, nil)
|
||||
actor = Keyword.get(options, :actor, actor)
|
||||
|
||||
user = User.get_cached_by_ap_id(actor)
|
||||
to = (object.data["to"] || []) ++ (object.data["cc"] || [])
|
||||
|
||||
with {:ok, object, activity} <- Object.delete(object),
|
||||
data <- %{
|
||||
"type" => "Delete",
|
||||
"actor" => actor,
|
||||
"object" => id,
|
||||
"to" => to,
|
||||
"deleted_activity_id" => activity && activity.id
|
||||
},
|
||||
data <-
|
||||
%{
|
||||
"type" => "Delete",
|
||||
"actor" => actor,
|
||||
"object" => id,
|
||||
"to" => to,
|
||||
"deleted_activity_id" => activity && activity.id
|
||||
}
|
||||
|> maybe_put("id", activity_id),
|
||||
{:ok, activity} <- insert(data, local, false),
|
||||
stream_out_participations(object, user),
|
||||
_ <- decrease_replies_count_if_reply(object),
|
||||
|
@ -591,6 +612,49 @@ defp restrict_visibility(_query, %{visibility: visibility})
|
|||
|
||||
defp restrict_visibility(query, _visibility), do: query
|
||||
|
||||
defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
|
||||
when is_list(visibility) do
|
||||
if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
|
||||
from(
|
||||
a in query,
|
||||
where:
|
||||
not fragment(
|
||||
"activity_visibility(?, ?, ?) = ANY (?)",
|
||||
a.actor,
|
||||
a.recipients,
|
||||
a.data,
|
||||
^visibility
|
||||
)
|
||||
)
|
||||
else
|
||||
Logger.error("Could not exclude visibility to #{visibility}")
|
||||
query
|
||||
end
|
||||
end
|
||||
|
||||
defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
|
||||
when visibility in @valid_visibilities do
|
||||
from(
|
||||
a in query,
|
||||
where:
|
||||
not fragment(
|
||||
"activity_visibility(?, ?, ?) = ?",
|
||||
a.actor,
|
||||
a.recipients,
|
||||
a.data,
|
||||
^visibility
|
||||
)
|
||||
)
|
||||
end
|
||||
|
||||
defp exclude_visibility(query, %{"exclude_visibilities" => visibility})
|
||||
when visibility not in @valid_visibilities do
|
||||
Logger.error("Could not exclude visibility to #{visibility}")
|
||||
query
|
||||
end
|
||||
|
||||
defp exclude_visibility(query, _visibility), do: query
|
||||
|
||||
defp restrict_thread_visibility(query, _, %{skip_thread_containment: true} = _),
|
||||
do: query
|
||||
|
||||
|
@ -955,6 +1019,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|
|||
|> restrict_muted_reblogs(opts)
|
||||
|> Activity.restrict_deactivated_users()
|
||||
|> exclude_poll_votes(opts)
|
||||
|> exclude_visibility(opts)
|
||||
end
|
||||
|
||||
def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do
|
||||
|
|
|
@ -514,7 +514,7 @@ def handle_incoming(
|
|||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
|
||||
%{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data,
|
||||
_options
|
||||
) do
|
||||
with actor <- Containment.get_actor(data),
|
||||
|
@ -528,7 +528,8 @@ def handle_incoming(
|
|||
type: "Accept",
|
||||
actor: followed,
|
||||
object: follow_activity.data["id"],
|
||||
local: false
|
||||
local: false,
|
||||
activity_id: id
|
||||
})
|
||||
else
|
||||
_e -> :error
|
||||
|
@ -536,7 +537,7 @@ def handle_incoming(
|
|||
end
|
||||
|
||||
def handle_incoming(
|
||||
%{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data,
|
||||
%{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => id} = data,
|
||||
_options
|
||||
) do
|
||||
with actor <- Containment.get_actor(data),
|
||||
|
@ -550,7 +551,8 @@ def handle_incoming(
|
|||
type: "Reject",
|
||||
actor: followed,
|
||||
object: follow_activity.data["id"],
|
||||
local: false
|
||||
local: false,
|
||||
activity_id: id
|
||||
}) do
|
||||
User.unfollow(follower, followed)
|
||||
|
||||
|
@ -637,7 +639,7 @@ def handle_incoming(
|
|||
# an error or a tombstone. This would allow us to verify that a deletion actually took
|
||||
# place.
|
||||
def handle_incoming(
|
||||
%{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = data,
|
||||
%{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => id} = data,
|
||||
_options
|
||||
) do
|
||||
object_id = Utils.get_ap_id(object_id)
|
||||
|
@ -646,7 +648,8 @@ def handle_incoming(
|
|||
{:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
|
||||
{:ok, object} <- get_obj_helper(object_id),
|
||||
:ok <- Containment.contain_origin(actor.ap_id, object.data),
|
||||
{:ok, activity} <- ActivityPub.delete(object, false) do
|
||||
{:ok, activity} <-
|
||||
ActivityPub.delete(object, local: false, activity_id: id, actor: actor.ap_id) do
|
||||
{:ok, activity}
|
||||
else
|
||||
nil ->
|
||||
|
|
|
@ -46,6 +46,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
|
|||
:user_delete,
|
||||
:users_create,
|
||||
:user_toggle_activation,
|
||||
:user_activate,
|
||||
:user_deactivate,
|
||||
:tag_users,
|
||||
:untag_users,
|
||||
:right_add,
|
||||
|
@ -98,7 +100,7 @@ def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
|
|||
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
subject: user,
|
||||
subject: [user],
|
||||
action: "delete"
|
||||
})
|
||||
|
||||
|
@ -106,6 +108,20 @@ def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
|
|||
|> json(nickname)
|
||||
end
|
||||
|
||||
def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
|
||||
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
|
||||
User.delete(users)
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
subject: users,
|
||||
action: "delete"
|
||||
})
|
||||
|
||||
conn
|
||||
|> json(nicknames)
|
||||
end
|
||||
|
||||
def user_follow(%{assigns: %{user: admin}} = conn, %{
|
||||
"follower" => follower_nick,
|
||||
"followed" => followed_nick
|
||||
|
@ -240,7 +256,7 @@ def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => ni
|
|||
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
subject: user,
|
||||
subject: [user],
|
||||
action: action
|
||||
})
|
||||
|
||||
|
@ -249,6 +265,36 @@ def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => ni
|
|||
|> render("show.json", %{user: updated_user})
|
||||
end
|
||||
|
||||
def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
|
||||
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
|
||||
{:ok, updated_users} = User.deactivate(users, false)
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
subject: users,
|
||||
action: "activate"
|
||||
})
|
||||
|
||||
conn
|
||||
|> put_view(AccountView)
|
||||
|> render("index.json", %{users: Keyword.values(updated_users)})
|
||||
end
|
||||
|
||||
def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
|
||||
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
|
||||
{:ok, updated_users} = User.deactivate(users, true)
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
subject: users,
|
||||
action: "deactivate"
|
||||
})
|
||||
|
||||
conn
|
||||
|> put_view(AccountView)
|
||||
|> render("index.json", %{users: Keyword.values(updated_users)})
|
||||
end
|
||||
|
||||
def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
|
||||
with {:ok, _} <- User.tag(nicknames, tags) do
|
||||
ModerationLog.insert_log(%{
|
||||
|
@ -313,6 +359,31 @@ defp maybe_parse_filters(filters) do
|
|||
|> Enum.into(%{}, &{&1, true})
|
||||
end
|
||||
|
||||
def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
|
||||
"permission_group" => permission_group,
|
||||
"nicknames" => nicknames
|
||||
})
|
||||
when permission_group in ["moderator", "admin"] do
|
||||
info = Map.put(%{}, "is_" <> permission_group, true)
|
||||
|
||||
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
|
||||
|
||||
User.update_info(users, &User.Info.admin_api_update(&1, info))
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
action: "grant",
|
||||
actor: admin,
|
||||
subject: users,
|
||||
permission: permission_group
|
||||
})
|
||||
|
||||
json(conn, info)
|
||||
end
|
||||
|
||||
def right_add_multiple(conn, _) do
|
||||
render_error(conn, :not_found, "No such permission_group")
|
||||
end
|
||||
|
||||
def right_add(%{assigns: %{user: admin}} = conn, %{
|
||||
"permission_group" => permission_group,
|
||||
"nickname" => nickname
|
||||
|
@ -328,7 +399,7 @@ def right_add(%{assigns: %{user: admin}} = conn, %{
|
|||
ModerationLog.insert_log(%{
|
||||
action: "grant",
|
||||
actor: admin,
|
||||
subject: user,
|
||||
subject: [user],
|
||||
permission: permission_group
|
||||
})
|
||||
|
||||
|
@ -349,8 +420,36 @@ def right_get(conn, %{"nickname" => nickname}) do
|
|||
})
|
||||
end
|
||||
|
||||
def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
|
||||
render_error(conn, :forbidden, "You can't revoke your own admin status.")
|
||||
def right_delete_multiple(
|
||||
%{assigns: %{user: %{nickname: admin_nickname} = admin}} = conn,
|
||||
%{
|
||||
"permission_group" => permission_group,
|
||||
"nicknames" => nicknames
|
||||
}
|
||||
)
|
||||
when permission_group in ["moderator", "admin"] do
|
||||
with false <- Enum.member?(nicknames, admin_nickname) do
|
||||
info = Map.put(%{}, "is_" <> permission_group, false)
|
||||
|
||||
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
|
||||
|
||||
User.update_info(users, &User.Info.admin_api_update(&1, info))
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
action: "revoke",
|
||||
actor: admin,
|
||||
subject: users,
|
||||
permission: permission_group
|
||||
})
|
||||
|
||||
json(conn, info)
|
||||
else
|
||||
_ -> render_error(conn, :forbidden, "You can't revoke your own admin/moderator status.")
|
||||
end
|
||||
end
|
||||
|
||||
def right_delete_multiple(conn, _) do
|
||||
render_error(conn, :not_found, "No such permission_group")
|
||||
end
|
||||
|
||||
def right_delete(
|
||||
|
@ -371,34 +470,15 @@ def right_delete(
|
|||
ModerationLog.insert_log(%{
|
||||
action: "revoke",
|
||||
actor: admin,
|
||||
subject: user,
|
||||
subject: [user],
|
||||
permission: permission_group
|
||||
})
|
||||
|
||||
json(conn, info)
|
||||
end
|
||||
|
||||
def right_delete(conn, _) do
|
||||
render_error(conn, :not_found, "No such permission_group")
|
||||
end
|
||||
|
||||
def set_activation_status(%{assigns: %{user: admin}} = conn, %{
|
||||
"nickname" => nickname,
|
||||
"status" => status
|
||||
}) do
|
||||
with {:ok, status} <- Ecto.Type.cast(:boolean, status),
|
||||
%User{} = user <- User.get_cached_by_nickname(nickname),
|
||||
{:ok, _} <- User.deactivate(user, !status) do
|
||||
action = if(user.info.deactivated, do: "activate", else: "deactivate")
|
||||
|
||||
ModerationLog.insert_log(%{
|
||||
actor: admin,
|
||||
subject: user,
|
||||
action: action
|
||||
})
|
||||
|
||||
json_response(conn, :no_content, "")
|
||||
end
|
||||
def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
|
||||
render_error(conn, :forbidden, "You can't revoke your own admin status.")
|
||||
end
|
||||
|
||||
def relay_list(conn, _params) do
|
||||
|
|
|
@ -19,6 +19,12 @@ def render("index.json", %{users: users, count: count, page_size: page_size}) do
|
|||
}
|
||||
end
|
||||
|
||||
def render("index.json", %{users: users}) do
|
||||
%{
|
||||
users: render_many(users, AccountView, "show.json", as: :user)
|
||||
}
|
||||
end
|
||||
|
||||
def render("show.json", %{user: user}) do
|
||||
avatar = User.avatar_url(user) |> MediaProxy.url()
|
||||
display_name = HTML.strip_tags(user.name || user.nickname)
|
||||
|
|
|
@ -71,6 +71,7 @@ def get_scheduled_activities(user, params \\ %{}) do
|
|||
defp cast_params(params) do
|
||||
param_types = %{
|
||||
exclude_types: {:array, :string},
|
||||
exclude_visibilities: {:array, :string},
|
||||
reblogs: :boolean,
|
||||
with_muted: :boolean
|
||||
}
|
||||
|
|
|
@ -35,6 +35,13 @@ def init(%{qs: qs} = req, state) do
|
|||
{_, stream} <- List.keyfind(params, "stream", 0),
|
||||
{:ok, user} <- allow_request(stream, [access_token, sec_websocket]),
|
||||
topic when is_binary(topic) <- expand_topic(stream, params) do
|
||||
req =
|
||||
if sec_websocket do
|
||||
:cowboy_req.set_resp_header("sec-websocket-protocol", sec_websocket, req)
|
||||
else
|
||||
req
|
||||
end
|
||||
|
||||
{:cowboy_websocket, req, %{user: user, topic: topic}, %{idle_timeout: @timeout}}
|
||||
else
|
||||
{:error, code} ->
|
||||
|
|
|
@ -11,7 +11,7 @@ defmodule Pleroma.Web.OStatus.DeleteHandler do
|
|||
def handle_delete(entry, _doc \\ nil) do
|
||||
with id <- XML.string_from_xpath("//id", entry),
|
||||
%Object{} = object <- Object.normalize(id),
|
||||
{:ok, delete} <- ActivityPub.delete(object, false) do
|
||||
{:ok, delete} <- ActivityPub.delete(object, local: false) do
|
||||
delete
|
||||
end
|
||||
end
|
||||
|
|
|
@ -137,11 +137,14 @@ defmodule Pleroma.Web.Router do
|
|||
delete("/users", AdminAPIController, :user_delete)
|
||||
post("/users", AdminAPIController, :users_create)
|
||||
patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)
|
||||
patch("/users/activate", AdminAPIController, :user_activate)
|
||||
patch("/users/deactivate", AdminAPIController, :user_deactivate)
|
||||
put("/users/tag", AdminAPIController, :tag_users)
|
||||
delete("/users/tag", AdminAPIController, :untag_users)
|
||||
|
||||
get("/users/:nickname/permission_group", AdminAPIController, :right_get)
|
||||
get("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_get)
|
||||
|
||||
post("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_add)
|
||||
|
||||
delete(
|
||||
|
@ -150,7 +153,13 @@ defmodule Pleroma.Web.Router do
|
|||
:right_delete
|
||||
)
|
||||
|
||||
put("/users/:nickname/activation_status", AdminAPIController, :set_activation_status)
|
||||
post("/users/permission_group/:permission_group", AdminAPIController, :right_add_multiple)
|
||||
|
||||
delete(
|
||||
"/users/permission_group/:permission_group",
|
||||
AdminAPIController,
|
||||
:right_delete_multiple
|
||||
)
|
||||
|
||||
get("/relay", AdminAPIController, :relay_list)
|
||||
post("/relay", AdminAPIController, :relay_follow)
|
||||
|
|
|
@ -49,7 +49,7 @@ defp handle_should_send(:test) do
|
|||
end
|
||||
end
|
||||
|
||||
defp handle_should_send(_) do
|
||||
true
|
||||
end
|
||||
defp handle_should_send(:benchmark), do: false
|
||||
|
||||
defp handle_should_send(_), do: true
|
||||
end
|
||||
|
|
6
mix.exs
6
mix.exs
|
@ -69,6 +69,7 @@ def application do
|
|||
end
|
||||
|
||||
# Specifies which paths to compile per environment.
|
||||
defp elixirc_paths(:benchmark), do: ["lib", "benchmarks"]
|
||||
defp elixirc_paths(:test), do: ["lib", "test/support"]
|
||||
defp elixirc_paths(_), do: ["lib"]
|
||||
|
||||
|
@ -220,7 +221,10 @@ defp version(version) do
|
|||
with {branch_name, 0} <- System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]),
|
||||
branch_name <- String.trim(branch_name),
|
||||
branch_name <- System.get_env("PLEROMA_BUILD_BRANCH") || branch_name,
|
||||
true <- branch_name not in ["master", "HEAD"] do
|
||||
true <-
|
||||
!Enum.any?(["master", "HEAD", "release/", "stable"], fn name ->
|
||||
String.starts_with?(name, branch_name)
|
||||
end) do
|
||||
branch_name =
|
||||
branch_name
|
||||
|> String.trim()
|
||||
|
|
|
@ -35,11 +35,11 @@ detect_branch() {
|
|||
if [ "$branch" = "develop" ]; then
|
||||
echo "develop"
|
||||
elif [ "$branch" = "" ]; then
|
||||
echo "master"
|
||||
echo "stable"
|
||||
else
|
||||
# Note: branch name in version is of SemVer format and may only contain [0-9a-zA-Z-] symbols —
|
||||
# if supporting releases for more branches, need to ensure they contain only these symbols.
|
||||
echo "Releases are built only for master and develop branches" >&2
|
||||
echo "Can't detect the branch automatically, please specify it by using the --branch option." >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
|
|
@ -23,6 +23,39 @@ test "getting a participation will also preload things" do
|
|||
assert %Pleroma.Conversation{} = participation.conversation
|
||||
end
|
||||
|
||||
test "for a new conversation or a reply, it doesn't mark the author's participation as unread" do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, _} =
|
||||
CommonAPI.post(user, %{"status" => "Hey @#{other_user.nickname}.", "visibility" => "direct"})
|
||||
|
||||
user = User.get_cached_by_id(user.id)
|
||||
other_user = User.get_cached_by_id(other_user.id)
|
||||
|
||||
[%{read: true}] = Participation.for_user(user)
|
||||
[%{read: false} = participation] = Participation.for_user(other_user)
|
||||
|
||||
assert User.get_cached_by_id(user.id).info.unread_conversation_count == 0
|
||||
assert User.get_cached_by_id(other_user.id).info.unread_conversation_count == 1
|
||||
|
||||
{:ok, _} =
|
||||
CommonAPI.post(other_user, %{
|
||||
"status" => "Hey @#{user.nickname}.",
|
||||
"visibility" => "direct",
|
||||
"in_reply_to_conversation_id" => participation.id
|
||||
})
|
||||
|
||||
user = User.get_cached_by_id(user.id)
|
||||
other_user = User.get_cached_by_id(other_user.id)
|
||||
|
||||
[%{read: false}] = Participation.for_user(user)
|
||||
[%{read: true}] = Participation.for_user(other_user)
|
||||
|
||||
assert User.get_cached_by_id(user.id).info.unread_conversation_count == 1
|
||||
assert User.get_cached_by_id(other_user.id).info.unread_conversation_count == 0
|
||||
end
|
||||
|
||||
test "for a new conversation, it sets the recipents of the participation" do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
|
@ -32,7 +65,7 @@ test "for a new conversation, it sets the recipents of the participation" do
|
|||
CommonAPI.post(user, %{"status" => "Hey @#{other_user.nickname}.", "visibility" => "direct"})
|
||||
|
||||
user = User.get_cached_by_id(user.id)
|
||||
other_user = User.get_cached_by_id(user.id)
|
||||
other_user = User.get_cached_by_id(other_user.id)
|
||||
[participation] = Participation.for_user(user)
|
||||
participation = Pleroma.Repo.preload(participation, :recipients)
|
||||
|
||||
|
|
|
@ -24,13 +24,13 @@ test "logging user deletion by moderator", %{moderator: moderator, subject1: sub
|
|||
{:ok, _} =
|
||||
ModerationLog.insert_log(%{
|
||||
actor: moderator,
|
||||
subject: subject1,
|
||||
subject: [subject1],
|
||||
action: "delete"
|
||||
})
|
||||
|
||||
log = Repo.one(ModerationLog)
|
||||
|
||||
assert log.data["message"] == "@#{moderator.nickname} deleted user @#{subject1.nickname}"
|
||||
assert log.data["message"] == "@#{moderator.nickname} deleted users: @#{subject1.nickname}"
|
||||
end
|
||||
|
||||
test "logging user creation by moderator", %{
|
||||
|
@ -128,7 +128,7 @@ test "logging user grant by moderator", %{moderator: moderator, subject1: subjec
|
|||
{:ok, _} =
|
||||
ModerationLog.insert_log(%{
|
||||
actor: moderator,
|
||||
subject: subject1,
|
||||
subject: [subject1],
|
||||
action: "grant",
|
||||
permission: "moderator"
|
||||
})
|
||||
|
@ -142,7 +142,7 @@ test "logging user revoke by moderator", %{moderator: moderator, subject1: subje
|
|||
{:ok, _} =
|
||||
ModerationLog.insert_log(%{
|
||||
actor: moderator,
|
||||
subject: subject1,
|
||||
subject: [subject1],
|
||||
action: "revoke",
|
||||
permission: "moderator"
|
||||
})
|
||||
|
|
|
@ -65,21 +65,6 @@ test "finds users, considering density of matched tokens" do
|
|||
assert [u2.id, u1.id] == Enum.map(User.search("bar word"), & &1.id)
|
||||
end
|
||||
|
||||
test "finds users, ranking by similarity" do
|
||||
u1 = insert(:user, %{name: "lain"})
|
||||
_u2 = insert(:user, %{name: "ean"})
|
||||
u3 = insert(:user, %{name: "ebn", nickname: "lain@mastodon.social"})
|
||||
u4 = insert(:user, %{nickname: "lain@pleroma.soykaf.com"})
|
||||
|
||||
assert [u4.id, u3.id, u1.id] == Enum.map(User.search("lain@ple", for_user: u1), & &1.id)
|
||||
end
|
||||
|
||||
test "finds users, handling misspelled requests" do
|
||||
u1 = insert(:user, %{name: "lain"})
|
||||
|
||||
assert [u1.id] == Enum.map(User.search("laiin"), & &1.id)
|
||||
end
|
||||
|
||||
test "finds users, boosting ranks of friends and followers" do
|
||||
u1 = insert(:user)
|
||||
u2 = insert(:user, %{name: "Doe"})
|
||||
|
@ -163,17 +148,6 @@ test "find all users for unauthenticated users when `limit_to_local_content` is
|
|||
Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
|
||||
end
|
||||
|
||||
test "finds a user whose name is nil" do
|
||||
_user = insert(:user, %{name: "notamatch", nickname: "testuser@pleroma.amplifie.red"})
|
||||
user_two = insert(:user, %{name: nil, nickname: "lain@pleroma.soykaf.com"})
|
||||
|
||||
assert user_two ==
|
||||
User.search("lain@pleroma.soykaf.com")
|
||||
|> List.first()
|
||||
|> Map.put(:search_rank, nil)
|
||||
|> Map.put(:search_type, nil)
|
||||
end
|
||||
|
||||
test "does not yield false-positive matches" do
|
||||
insert(:user, %{name: "John Doe"})
|
||||
|
||||
|
|
|
@ -41,6 +41,27 @@ test "it streams them out" do
|
|||
assert called(Pleroma.Web.Streamer.stream("participation", participations))
|
||||
end
|
||||
end
|
||||
|
||||
test "streams them out on activity creation" do
|
||||
user_one = insert(:user)
|
||||
user_two = insert(:user)
|
||||
|
||||
with_mock Pleroma.Web.Streamer,
|
||||
stream: fn _, _ -> nil end do
|
||||
{:ok, activity} =
|
||||
CommonAPI.post(user_one, %{
|
||||
"status" => "@#{user_two.nickname}",
|
||||
"visibility" => "direct"
|
||||
})
|
||||
|
||||
conversation =
|
||||
activity.data["context"]
|
||||
|> Pleroma.Conversation.get_for_ap_id()
|
||||
|> Repo.preload(participations: :user)
|
||||
|
||||
assert called(Pleroma.Web.Streamer.stream("participation", conversation.participations))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetching restricted by visibility" do
|
||||
|
@ -87,6 +108,66 @@ test "it restricts by the appropriate visibility" do
|
|||
end
|
||||
end
|
||||
|
||||
describe "fetching excluded by visibility" do
|
||||
test "it excludes by the appropriate visibility" do
|
||||
user = insert(:user)
|
||||
|
||||
{:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"})
|
||||
|
||||
{:ok, direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
|
||||
|
||||
{:ok, unlisted_activity} =
|
||||
CommonAPI.post(user, %{"status" => ".", "visibility" => "unlisted"})
|
||||
|
||||
{:ok, private_activity} =
|
||||
CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
|
||||
|
||||
activities =
|
||||
ActivityPub.fetch_activities([], %{
|
||||
"exclude_visibilities" => "direct",
|
||||
"actor_id" => user.ap_id
|
||||
})
|
||||
|
||||
assert public_activity in activities
|
||||
assert unlisted_activity in activities
|
||||
assert private_activity in activities
|
||||
refute direct_activity in activities
|
||||
|
||||
activities =
|
||||
ActivityPub.fetch_activities([], %{
|
||||
"exclude_visibilities" => "unlisted",
|
||||
"actor_id" => user.ap_id
|
||||
})
|
||||
|
||||
assert public_activity in activities
|
||||
refute unlisted_activity in activities
|
||||
assert private_activity in activities
|
||||
assert direct_activity in activities
|
||||
|
||||
activities =
|
||||
ActivityPub.fetch_activities([], %{
|
||||
"exclude_visibilities" => "private",
|
||||
"actor_id" => user.ap_id
|
||||
})
|
||||
|
||||
assert public_activity in activities
|
||||
assert unlisted_activity in activities
|
||||
refute private_activity in activities
|
||||
assert direct_activity in activities
|
||||
|
||||
activities =
|
||||
ActivityPub.fetch_activities([], %{
|
||||
"exclude_visibilities" => "public",
|
||||
"actor_id" => user.ap_id
|
||||
})
|
||||
|
||||
refute public_activity in activities
|
||||
assert unlisted_activity in activities
|
||||
assert private_activity in activities
|
||||
assert direct_activity in activities
|
||||
end
|
||||
end
|
||||
|
||||
describe "building a user from his ap id" do
|
||||
test "it returns a user" do
|
||||
user_id = "http://mastodon.example.org/users/admin"
|
||||
|
|
|
@ -682,6 +682,7 @@ test "it works for incoming update activities which lock the account" do
|
|||
|
||||
test "it works for incoming deletes" do
|
||||
activity = insert(:note_activity)
|
||||
deleting_user = insert(:user)
|
||||
|
||||
data =
|
||||
File.read!("test/fixtures/mastodon-delete.json")
|
||||
|
@ -694,11 +695,14 @@ test "it works for incoming deletes" do
|
|||
data =
|
||||
data
|
||||
|> Map.put("object", object)
|
||||
|> Map.put("actor", activity.data["actor"])
|
||||
|> Map.put("actor", deleting_user.ap_id)
|
||||
|
||||
{:ok, %Activity{local: false}} = Transmogrifier.handle_incoming(data)
|
||||
{:ok, %Activity{actor: actor, local: false, data: %{"id" => id}}} =
|
||||
Transmogrifier.handle_incoming(data)
|
||||
|
||||
assert id == data["id"]
|
||||
refute Activity.get_by_id(activity.id)
|
||||
assert actor == deleting_user.ap_id
|
||||
end
|
||||
|
||||
test "it fails for incoming deletes with spoofed origin" do
|
||||
|
@ -905,6 +909,8 @@ test "it works for incoming accepts which were pre-accepted" do
|
|||
|
||||
assert activity.data["object"] == follow_activity.data["id"]
|
||||
|
||||
assert activity.data["id"] == accept_data["id"]
|
||||
|
||||
follower = User.get_cached_by_id(follower.id)
|
||||
|
||||
assert User.following?(follower, followed) == true
|
||||
|
@ -1009,6 +1015,7 @@ test "it works for incoming rejects which are orphaned" do
|
|||
|
||||
{:ok, activity} = Transmogrifier.handle_incoming(reject_data)
|
||||
refute activity.local
|
||||
assert activity.data["id"] == reject_data["id"]
|
||||
|
||||
follower = User.get_cached_by_id(follower.id)
|
||||
|
||||
|
|
|
@ -23,8 +23,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
|
|||
:ok
|
||||
end
|
||||
|
||||
describe "/api/pleroma/admin/users" do
|
||||
test "Delete" do
|
||||
describe "DELETE /api/pleroma/admin/users" do
|
||||
test "single user" do
|
||||
admin = insert(:user, info: %{is_admin: true})
|
||||
user = insert(:user)
|
||||
|
||||
|
@ -36,15 +36,36 @@ test "Delete" do
|
|||
|
||||
log_entry = Repo.one(ModerationLog)
|
||||
|
||||
assert log_entry.data["subject"]["nickname"] == user.nickname
|
||||
assert log_entry.data["action"] == "delete"
|
||||
|
||||
assert ModerationLog.get_log_entry_message(log_entry) ==
|
||||
"@#{admin.nickname} deleted user @#{user.nickname}"
|
||||
"@#{admin.nickname} deleted users: @#{user.nickname}"
|
||||
|
||||
assert json_response(conn, 200) == user.nickname
|
||||
end
|
||||
|
||||
test "multiple users" do
|
||||
admin = insert(:user, info: %{is_admin: true})
|
||||
user_one = insert(:user)
|
||||
user_two = insert(:user)
|
||||
|
||||
conn =
|
||||
build_conn()
|
||||
|> assign(:user, admin)
|
||||
|> put_req_header("accept", "application/json")
|
||||
|> delete("/api/pleroma/admin/users", %{
|
||||
nicknames: [user_one.nickname, user_two.nickname]
|
||||
})
|
||||
|
||||
log_entry = Repo.one(ModerationLog)
|
||||
|
||||
assert ModerationLog.get_log_entry_message(log_entry) ==
|
||||
"@#{admin.nickname} deleted users: @#{user_one.nickname}, @#{user_two.nickname}"
|
||||
|
||||
response = json_response(conn, 200)
|
||||
assert response -- [user_one.nickname, user_two.nickname] == []
|
||||
end
|
||||
end
|
||||
|
||||
describe "/api/pleroma/admin/users" do
|
||||
test "Create" do
|
||||
admin = insert(:user, info: %{is_admin: true})
|
||||
|
||||
|
@ -410,6 +431,29 @@ test "/:right POST, can add to a permission group" do
|
|||
"@#{admin.nickname} made @#{user.nickname} admin"
|
||||
end
|
||||
|
||||
test "/:right POST, can add to a permission group (multiple)" do
|
||||
admin = insert(:user, info: %{is_admin: true})
|
||||
user_one = insert(:user)
|
||||
user_two = insert(:user)
|
||||
|
||||
conn =
|
||||
build_conn()
|
||||
|> assign(:user, admin)
|
||||
|> put_req_header("accept", "application/json")
|
||||
|> post("/api/pleroma/admin/users/permission_group/admin", %{
|
||||
nicknames: [user_one.nickname, user_two.nickname]
|
||||
})
|
||||
|
||||
assert json_response(conn, 200) == %{
|
||||
"is_admin" => true
|
||||
}
|
||||
|
||||
log_entry = Repo.one(ModerationLog)
|
||||
|
||||
assert ModerationLog.get_log_entry_message(log_entry) ==
|
||||
"@#{admin.nickname} made @#{user_one.nickname}, @#{user_two.nickname} admin"
|
||||
end
|
||||
|
||||
test "/:right DELETE, can remove from a permission group" do
|
||||
admin = insert(:user, info: %{is_admin: true})
|
||||
user = insert(:user, info: %{is_admin: true})
|
||||
|
@ -429,63 +473,30 @@ test "/:right DELETE, can remove from a permission group" do
|
|||
assert ModerationLog.get_log_entry_message(log_entry) ==
|
||||
"@#{admin.nickname} revoked admin role from @#{user.nickname}"
|
||||
end
|
||||
end
|
||||
|
||||
describe "PUT /api/pleroma/admin/users/:nickname/activation_status" do
|
||||
setup %{conn: conn} do
|
||||
test "/:right DELETE, can remove from a permission group (multiple)" do
|
||||
admin = insert(:user, info: %{is_admin: true})
|
||||
user_one = insert(:user, info: %{is_admin: true})
|
||||
user_two = insert(:user, info: %{is_admin: true})
|
||||
|
||||
conn =
|
||||
conn
|
||||
build_conn()
|
||||
|> assign(:user, admin)
|
||||
|> put_req_header("accept", "application/json")
|
||||
|> delete("/api/pleroma/admin/users/permission_group/admin", %{
|
||||
nicknames: [user_one.nickname, user_two.nickname]
|
||||
})
|
||||
|
||||
%{conn: conn, admin: admin}
|
||||
end
|
||||
|
||||
test "deactivates the user", %{conn: conn, admin: admin} do
|
||||
user = insert(:user)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: false})
|
||||
|
||||
user = User.get_cached_by_id(user.id)
|
||||
assert user.info.deactivated == true
|
||||
assert json_response(conn, :no_content)
|
||||
assert json_response(conn, 200) == %{
|
||||
"is_admin" => false
|
||||
}
|
||||
|
||||
log_entry = Repo.one(ModerationLog)
|
||||
|
||||
assert ModerationLog.get_log_entry_message(log_entry) ==
|
||||
"@#{admin.nickname} deactivated user @#{user.nickname}"
|
||||
end
|
||||
|
||||
test "activates the user", %{conn: conn, admin: admin} do
|
||||
user = insert(:user, info: %{deactivated: true})
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: true})
|
||||
|
||||
user = User.get_cached_by_id(user.id)
|
||||
assert user.info.deactivated == false
|
||||
assert json_response(conn, :no_content)
|
||||
|
||||
log_entry = Repo.one(ModerationLog)
|
||||
|
||||
assert ModerationLog.get_log_entry_message(log_entry) ==
|
||||
"@#{admin.nickname} activated user @#{user.nickname}"
|
||||
end
|
||||
|
||||
test "returns 403 when requested by a non-admin", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> assign(:user, user)
|
||||
|> put("/api/pleroma/admin/users/#{user.nickname}/activation_status", %{status: false})
|
||||
|
||||
assert json_response(conn, :forbidden)
|
||||
"@#{admin.nickname} revoked admin role from @#{user_one.nickname}, @#{
|
||||
user_two.nickname
|
||||
}"
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1035,6 +1046,50 @@ test "it works with multiple filters" do
|
|||
end
|
||||
end
|
||||
|
||||
test "PATCH /api/pleroma/admin/users/activate" do
|
||||
admin = insert(:user, info: %{is_admin: true})
|
||||
user_one = insert(:user, info: %{deactivated: true})
|
||||
user_two = insert(:user, info: %{deactivated: true})
|
||||
|
||||
conn =
|
||||
build_conn()
|
||||
|> assign(:user, admin)
|
||||
|> patch(
|
||||
"/api/pleroma/admin/users/activate",
|
||||
%{nicknames: [user_one.nickname, user_two.nickname]}
|
||||
)
|
||||
|
||||
response = json_response(conn, 200)
|
||||
assert Enum.map(response["users"], & &1["deactivated"]) == [false, false]
|
||||
|
||||
log_entry = Repo.one(ModerationLog)
|
||||
|
||||
assert ModerationLog.get_log_entry_message(log_entry) ==
|
||||
"@#{admin.nickname} activated users: @#{user_one.nickname}, @#{user_two.nickname}"
|
||||
end
|
||||
|
||||
test "PATCH /api/pleroma/admin/users/deactivate" do
|
||||
admin = insert(:user, info: %{is_admin: true})
|
||||
user_one = insert(:user, info: %{deactivated: false})
|
||||
user_two = insert(:user, info: %{deactivated: false})
|
||||
|
||||
conn =
|
||||
build_conn()
|
||||
|> assign(:user, admin)
|
||||
|> patch(
|
||||
"/api/pleroma/admin/users/deactivate",
|
||||
%{nicknames: [user_one.nickname, user_two.nickname]}
|
||||
)
|
||||
|
||||
response = json_response(conn, 200)
|
||||
assert Enum.map(response["users"], & &1["deactivated"]) == [true, true]
|
||||
|
||||
log_entry = Repo.one(ModerationLog)
|
||||
|
||||
assert ModerationLog.get_log_entry_message(log_entry) ==
|
||||
"@#{admin.nickname} deactivated users: @#{user_one.nickname}, @#{user_two.nickname}"
|
||||
end
|
||||
|
||||
test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do
|
||||
admin = insert(:user, info: %{is_admin: true})
|
||||
user = insert(:user)
|
||||
|
@ -1059,7 +1114,7 @@ test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do
|
|||
log_entry = Repo.one(ModerationLog)
|
||||
|
||||
assert ModerationLog.get_log_entry_message(log_entry) ==
|
||||
"@#{admin.nickname} deactivated user @#{user.nickname}"
|
||||
"@#{admin.nickname} deactivated users: @#{user.nickname}"
|
||||
end
|
||||
|
||||
describe "POST /api/pleroma/admin/users/invite_token" do
|
||||
|
|
|
@ -237,6 +237,20 @@ test "filters user's statuses by a hashtag", %{conn: conn} do
|
|||
assert [%{"id" => id}] = json_response(conn, 200)
|
||||
assert id == to_string(post.id)
|
||||
end
|
||||
|
||||
test "the user views their own timelines and excludes direct messages", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
{:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"})
|
||||
{:ok, _direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> assign(:user, user)
|
||||
|> get("/api/v1/accounts/#{user.id}/statuses", %{"exclude_visibilities" => ["direct"]})
|
||||
|
||||
assert [%{"id" => id}] = json_response(conn, 200)
|
||||
assert id == to_string(public_activity.id)
|
||||
end
|
||||
end
|
||||
|
||||
describe "followers" do
|
||||
|
|
|
@ -54,9 +54,9 @@ test "returns a list of conversations", %{conn: conn} do
|
|||
assert user_two.id in account_ids
|
||||
assert user_three.id in account_ids
|
||||
assert is_binary(res_id)
|
||||
assert unread == true
|
||||
assert unread == false
|
||||
assert res_last_status["id"] == direct.id
|
||||
assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 1
|
||||
assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 0
|
||||
end
|
||||
|
||||
test "updates the last_status on reply", %{conn: conn} do
|
||||
|
@ -95,19 +95,23 @@ test "the user marks a conversation as read", %{conn: conn} do
|
|||
"visibility" => "direct"
|
||||
})
|
||||
|
||||
assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 0
|
||||
assert User.get_cached_by_id(user_two.id).info.unread_conversation_count == 1
|
||||
|
||||
[%{"id" => direct_conversation_id, "unread" => true}] =
|
||||
conn
|
||||
|> assign(:user, user_one)
|
||||
|> assign(:user, user_two)
|
||||
|> get("/api/v1/conversations")
|
||||
|> json_response(200)
|
||||
|
||||
%{"unread" => false} =
|
||||
conn
|
||||
|> assign(:user, user_one)
|
||||
|> assign(:user, user_two)
|
||||
|> post("/api/v1/conversations/#{direct_conversation_id}/read")
|
||||
|> json_response(200)
|
||||
|
||||
assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 0
|
||||
assert User.get_cached_by_id(user_two.id).info.unread_conversation_count == 0
|
||||
|
||||
# The conversation is marked as unread on reply
|
||||
{:ok, _} =
|
||||
|
@ -124,6 +128,7 @@ test "the user marks a conversation as read", %{conn: conn} do
|
|||
|> json_response(200)
|
||||
|
||||
assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 1
|
||||
assert User.get_cached_by_id(user_two.id).info.unread_conversation_count == 0
|
||||
|
||||
# A reply doesn't increment the user's unread_conversation_count if the conversation is unread
|
||||
{:ok, _} =
|
||||
|
@ -134,6 +139,7 @@ test "the user marks a conversation as read", %{conn: conn} do
|
|||
})
|
||||
|
||||
assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 1
|
||||
assert User.get_cached_by_id(user_two.id).info.unread_conversation_count == 0
|
||||
end
|
||||
|
||||
test "(vanilla) Mastodon frontend behaviour", %{conn: conn} do
|
||||
|
|
|
@ -137,6 +137,57 @@ test "paginates notifications using min_id, since_id, max_id, and limit", %{conn
|
|||
assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
|
||||
end
|
||||
|
||||
test "filters notifications using exclude_visibilities", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
|
||||
{:ok, public_activity} =
|
||||
CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "public"})
|
||||
|
||||
{:ok, direct_activity} =
|
||||
CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "direct"})
|
||||
|
||||
{:ok, unlisted_activity} =
|
||||
CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "unlisted"})
|
||||
|
||||
{:ok, private_activity} =
|
||||
CommonAPI.post(other_user, %{"status" => "@#{user.nickname}", "visibility" => "private"})
|
||||
|
||||
conn = assign(conn, :user, user)
|
||||
|
||||
conn_res =
|
||||
get(conn, "/api/v1/notifications", %{
|
||||
exclude_visibilities: ["public", "unlisted", "private"]
|
||||
})
|
||||
|
||||
assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200)
|
||||
assert id == direct_activity.id
|
||||
|
||||
conn_res =
|
||||
get(conn, "/api/v1/notifications", %{
|
||||
exclude_visibilities: ["public", "unlisted", "direct"]
|
||||
})
|
||||
|
||||
assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200)
|
||||
assert id == private_activity.id
|
||||
|
||||
conn_res =
|
||||
get(conn, "/api/v1/notifications", %{
|
||||
exclude_visibilities: ["public", "private", "direct"]
|
||||
})
|
||||
|
||||
assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200)
|
||||
assert id == unlisted_activity.id
|
||||
|
||||
conn_res =
|
||||
get(conn, "/api/v1/notifications", %{
|
||||
exclude_visibilities: ["unlisted", "private", "direct"]
|
||||
})
|
||||
|
||||
assert [%{"status" => %{"id" => id}}] = json_response(conn_res, 200)
|
||||
assert id == public_activity.id
|
||||
end
|
||||
|
||||
test "filters notifications using exclude_types", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
other_user = insert(:user)
|
||||
|
|
|
@ -40,9 +40,9 @@ test "it returns empty result if user or status search return undefined error",
|
|||
test "search", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
user_two = insert(:user, %{nickname: "shp@shitposter.club"})
|
||||
user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu 天子"})
|
||||
user_three = insert(:user, %{nickname: "shp@heldscal.la", name: "I love 2hu"})
|
||||
|
||||
{:ok, activity} = CommonAPI.post(user, %{"status" => "This is about 2hu private"})
|
||||
{:ok, activity} = CommonAPI.post(user, %{"status" => "This is about 2hu private 天子"})
|
||||
|
||||
{:ok, _activity} =
|
||||
CommonAPI.post(user, %{
|
||||
|
@ -70,7 +70,8 @@ test "search", %{conn: conn} do
|
|||
get(conn, "/api/v2/search", %{"q" => "天子"})
|
||||
|> json_response(200)
|
||||
|
||||
assert account["id"] == to_string(user_three.id)
|
||||
[status] = results["statuses"]
|
||||
assert status["id"] == to_string(activity.id)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -20,27 +20,52 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
|
|||
:ok
|
||||
end
|
||||
|
||||
test "the home timeline", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
following = insert(:user)
|
||||
describe "home" do
|
||||
test "the home timeline", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
following = insert(:user)
|
||||
|
||||
{:ok, _activity} = CommonAPI.post(following, %{"status" => "test"})
|
||||
{:ok, _activity} = CommonAPI.post(following, %{"status" => "test"})
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> assign(:user, user)
|
||||
|> get("/api/v1/timelines/home")
|
||||
conn =
|
||||
conn
|
||||
|> assign(:user, user)
|
||||
|> get("/api/v1/timelines/home")
|
||||
|
||||
assert Enum.empty?(json_response(conn, :ok))
|
||||
assert Enum.empty?(json_response(conn, :ok))
|
||||
|
||||
{:ok, user} = User.follow(user, following)
|
||||
{:ok, user} = User.follow(user, following)
|
||||
|
||||
conn =
|
||||
build_conn()
|
||||
|> assign(:user, user)
|
||||
|> get("/api/v1/timelines/home")
|
||||
conn =
|
||||
build_conn()
|
||||
|> assign(:user, user)
|
||||
|> get("/api/v1/timelines/home")
|
||||
|
||||
assert [%{"content" => "test"}] = json_response(conn, :ok)
|
||||
assert [%{"content" => "test"}] = json_response(conn, :ok)
|
||||
end
|
||||
|
||||
test "the home timeline when the direct messages are excluded", %{conn: conn} do
|
||||
user = insert(:user)
|
||||
{:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"})
|
||||
{:ok, direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
|
||||
|
||||
{:ok, unlisted_activity} =
|
||||
CommonAPI.post(user, %{"status" => ".", "visibility" => "unlisted"})
|
||||
|
||||
{:ok, private_activity} =
|
||||
CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
|
||||
|
||||
conn =
|
||||
conn
|
||||
|> assign(:user, user)
|
||||
|> get("/api/v1/timelines/home", %{"exclude_visibilities" => ["direct"]})
|
||||
|
||||
assert status_ids = json_response(conn, :ok) |> Enum.map(& &1["id"])
|
||||
assert public_activity.id in status_ids
|
||||
assert unlisted_activity.id in status_ids
|
||||
assert private_activity.id in status_ids
|
||||
refute direct_activity.id in status_ids
|
||||
end
|
||||
end
|
||||
|
||||
describe "public" do
|
||||
|
|
|
@ -424,8 +424,8 @@ test "shows unread_conversation_count only to the account owner" do
|
|||
other_user = insert(:user)
|
||||
|
||||
{:ok, _activity} =
|
||||
CommonAPI.post(user, %{
|
||||
"status" => "Hey @#{other_user.nickname}.",
|
||||
CommonAPI.post(other_user, %{
|
||||
"status" => "Hey @#{user.nickname}.",
|
||||
"visibility" => "direct"
|
||||
})
|
||||
|
||||
|
|
Loading…
Reference in a new issue