Merge branch 'develop' into feature/relay-list

This commit is contained in:
Maxim Filippov 2019-10-18 10:24:29 +02:00
commit 2473702be2
49 changed files with 1745 additions and 368 deletions

View file

@ -15,6 +15,7 @@ cache:
stages: stages:
- build - build
- test - test
- benchmark
- deploy - deploy
- release - release
@ -28,6 +29,36 @@ build:
- mix deps.get - mix deps.get
- mix compile --force - 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: unit-testing:
stage: test stage: test
services: services:
@ -70,7 +101,7 @@ docs-deploy:
stage: deploy stage: deploy
image: alpine:latest image: alpine:latest
only: only:
- master@pleroma/pleroma - stable@pleroma/pleroma
- develop@pleroma/pleroma - develop@pleroma/pleroma
before_script: before_script:
- apk add curl - apk add curl
@ -127,9 +158,10 @@ amd64:
# TODO: Replace with upstream image when 1.9.0 comes out # TODO: Replace with upstream image when 1.9.0 comes out
image: rinpatch/elixir:1.9.0-rc.0 image: rinpatch/elixir:1.9.0-rc.0
only: &release-only only: &release-only
- master@pleroma/pleroma - stable@pleroma/pleroma
- develop@pleroma/pleroma - develop@pleroma/pleroma
- /^maint/.*$/@pleroma/pleroma - /^maint/.*$/@pleroma/pleroma
- /^release/.*$/@pleroma/pleroma
artifacts: &release-artifacts artifacts: &release-artifacts
name: "pleroma-$CI_COMMIT_REF_NAME-$CI_COMMIT_SHORT_SHA-$CI_JOB_NAME" name: "pleroma-$CI_COMMIT_REF_NAME-$CI_COMMIT_SHORT_SHA-$CI_JOB_NAME"
paths: paths:

View file

@ -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 - Authentication: Added rate limit for password-authorized actions / login existence checks
- Metadata Link: Atom syndication Feed - Metadata Link: Atom syndication Feed
- Mix task to re-count statuses for all users (`mix pleroma.count_statuses`) - 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 - Admin API: Add `GET /api/pleroma/admin/relay` endpoint - lists all followed relays
### Changed ### 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 - MRF (Simple Policy): Also use `:accept`/`:reject` on the actors rather than only their activities
- OStatus: Extract RSS functionality - OStatus: Extract RSS functionality
- Mastodon API: Add `pleroma.direct_conversation_id` to the status endpoint (`GET /api/v1/statuses/:id`) - 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 ### 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`) - 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 - Report emails now include functional links to profiles of remote user accounts
## [1.1.0] - 2019-??-?? ## [1.1.0] - 2019-??-??
**Breaking:** The stable branch has been changed from `master` to `stable`, `master` now points to `release/1.0`
### Security ### Security
- Mastodon API: respect post privacy in `/api/v1/statuses/:id/{favourited,reblogged}_by` - Mastodon API: respect post privacy in `/api/v1/statuses/:id/{favourited,reblogged}_by`

View 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

View 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

View 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

View 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
View 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

View file

@ -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 ### Remove a user
@ -56,6 +56,15 @@ Authentication is required and the user must be an admin.
- `nickname` - `nickname`
- Response: Users nickname - Response: Users nickname
## `DELETE /api/pleroma/admin/users`
### Remove a user
- Method `DELETE`
- Params:
- `nicknames`
- Response: Array of user nicknames
### Create a user ### Create a user
- Method: `POST` - 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 - Params: none
- Response: - Response:
- On failure: `{"error": "…"}` - On failure: `{"error": "…"}`
- On success: JSON of the `user.info` - 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 ### Remove user from permission group
- Method: `DELETE`
- Params: none - Params: none
- Response: - Response:
- On failure: `{"error": "…"}` - On failure: `{"error": "…"}`
- On success: JSON of the `user.info` - On success: JSON of the `user.info`
- Note: An admin cannot revoke their own admin status. - 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 ### Active or deactivate a user
- Method: `PUT`
- Params: - Params:
- `nickname` - `nickname`
- `status` BOOLEAN field, false value means deactivation. - `status` BOOLEAN field, false value means deactivation.

View file

@ -13,6 +13,7 @@ Some apps operate under the assumption that no more than 4 attachments can be re
## Timelines ## Timelines
Adding the parameter `with_muted=true` to the timeline queries will also return activities by muted (not by blocked!) users. 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 ## Statuses
@ -84,6 +85,12 @@ Has these additional fields under the `pleroma` object:
- `is_seen`: true if the notification was read by the user - `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` ## POST `/api/v1/statuses`
Additional parameters can be added to the JSON body/Form data: Additional parameters can be added to the JSON body/Form data:

View file

@ -91,7 +91,7 @@ sudo adduser -S -s /bin/false -h /opt/pleroma -H -G pleroma pleroma
```shell ```shell
sudo mkdir -p /opt/pleroma sudo mkdir -p /opt/pleroma
sudo chown -R pleroma:pleroma /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: * Change to the new directory:

View file

@ -66,7 +66,7 @@ sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma
```shell ```shell
sudo mkdir -p /opt/pleroma sudo mkdir -p /opt/pleroma
sudo chown -R pleroma:pleroma /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: * Change to the new directory:

View file

@ -143,7 +143,7 @@ sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma
```shell ```shell
sudo mkdir -p /opt/pleroma sudo mkdir -p /opt/pleroma
sudo chown -R pleroma:pleroma /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: * Change to the new directory:

View file

@ -68,7 +68,7 @@ sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma
```shell ```shell
sudo mkdir -p /opt/pleroma sudo mkdir -p /opt/pleroma
sudo chown -R pleroma:pleroma /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: * Change to the new directory:

View file

@ -68,7 +68,7 @@ sudo useradd -r -s /bin/false -m -d /var/lib/pleroma -U pleroma
``` ```
sudo mkdir -p /opt/pleroma sudo mkdir -p /opt/pleroma
sudo chown -R pleroma:pleroma /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
``` ```
* 新しいディレクトリに移動します。 * 新しいディレクトリに移動します。

View file

@ -106,7 +106,7 @@ It is highly recommended you use your own fork for the `https://path/to/repo` pa
```shell ```shell
pleroma$ cd ~ 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: * Change to the new directory:

View file

@ -96,9 +96,9 @@ rm -r ~pleroma/*
export FLAVOUR="arm64-musl" export FLAVOUR="arm64-musl"
# Clone the release build into a temporary directory and unpack it # 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 " 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/ unzip /tmp/pleroma.zip -d /tmp/
" "

View file

@ -58,7 +58,7 @@ Clone the repository:
``` ```
$ cd /home/pleroma $ 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: Configure Pleroma. Note that you need a domain name at this point:

View file

@ -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` 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 #### 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 #### Postgresql
Start a shell as the \_postgresql user (as root run `su _postgresql -` then run the `initdb` command to initialize postgresql: Start a shell as the \_postgresql user (as root run `su _postgresql -` then run the `initdb` command to initialize postgresql:

View file

@ -44,7 +44,7 @@ Vaihda pleroma-käyttäjään ja mene kotihakemistoosi:
Lataa pleroman lähdekoodi: 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` `$ cd pleroma`

View file

@ -80,7 +80,7 @@ export FLAVOUR="arm64-musl"
# Clone the release build into a temporary directory and unpack it # Clone the release build into a temporary directory and unpack it
su pleroma -s $SHELL -lc " 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/ unzip /tmp/pleroma.zip -d /tmp/
" "

View file

@ -48,6 +48,12 @@ def read_cng(struct, params) do
|> validate_required([:read]) |> validate_required([:read])
end 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 def mark_as_read(participation) do
participation participation
|> read_cng(%{read: true}) |> read_cng(%{read: true})

View file

@ -86,18 +86,18 @@ defp parse_datetime(datetime) do
parsed_datetime parsed_datetime
end 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} {:ok, ModerationLog} | {:error, any}
def insert_log(%{ def insert_log(%{
actor: %User{} = actor, actor: %User{} = actor,
subject: %User{} = subject, subject: subjects,
action: action, action: action,
permission: permission permission: permission
}) do }) do
%ModerationLog{ %ModerationLog{
data: %{ data: %{
"actor" => user_to_map(actor), "actor" => user_to_map(actor),
"subject" => user_to_map(subject), "subject" => user_to_map(subjects),
"action" => action, "action" => action,
"permission" => permission, "permission" => permission,
"message" => "" "message" => ""
@ -303,13 +303,16 @@ def insert_log(%{
end end
@spec insert_log_entry_with_message(ModerationLog) :: {:ok, ModerationLog} | {:error, any} @spec insert_log_entry_with_message(ModerationLog) :: {:ok, ModerationLog} | {:error, any}
defp insert_log_entry_with_message(entry) do defp insert_log_entry_with_message(entry) do
entry.data["message"] entry.data["message"]
|> put_in(get_log_entry_message(entry)) |> put_in(get_log_entry_message(entry))
|> Repo.insert() |> Repo.insert()
end 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 defp user_to_map(%User{} = user) do
user user
|> Map.from_struct() |> Map.from_struct()
@ -349,10 +352,10 @@ def get_log_entry_message(%ModerationLog{
data: %{ data: %{
"actor" => %{"nickname" => actor_nickname}, "actor" => %{"nickname" => actor_nickname},
"action" => "delete", "action" => "delete",
"subject" => %{"nickname" => subject_nickname, "type" => "user"} "subject" => subjects
} }
}) do }) do
"@#{actor_nickname} deleted user @#{subject_nickname}" "@#{actor_nickname} deleted users: #{users_to_nicknames_string(subjects)}"
end end
@spec get_log_entry_message(ModerationLog) :: String.t() @spec get_log_entry_message(ModerationLog) :: String.t()
@ -363,12 +366,7 @@ def get_log_entry_message(%ModerationLog{
"subjects" => subjects "subjects" => subjects
} }
}) do }) do
nicknames = "@#{actor_nickname} created users: #{users_to_nicknames_string(subjects)}"
subjects
|> Enum.map(&"@#{&1["nickname"]}")
|> Enum.join(", ")
"@#{actor_nickname} created users: #{nicknames}"
end end
@spec get_log_entry_message(ModerationLog) :: String.t() @spec get_log_entry_message(ModerationLog) :: String.t()
@ -376,10 +374,10 @@ def get_log_entry_message(%ModerationLog{
data: %{ data: %{
"actor" => %{"nickname" => actor_nickname}, "actor" => %{"nickname" => actor_nickname},
"action" => "activate", "action" => "activate",
"subject" => %{"nickname" => subject_nickname, "type" => "user"} "subject" => users
} }
}) do }) do
"@#{actor_nickname} activated user @#{subject_nickname}" "@#{actor_nickname} activated users: #{users_to_nicknames_string(users)}"
end end
@spec get_log_entry_message(ModerationLog) :: String.t() @spec get_log_entry_message(ModerationLog) :: String.t()
@ -387,10 +385,10 @@ def get_log_entry_message(%ModerationLog{
data: %{ data: %{
"actor" => %{"nickname" => actor_nickname}, "actor" => %{"nickname" => actor_nickname},
"action" => "deactivate", "action" => "deactivate",
"subject" => %{"nickname" => subject_nickname, "type" => "user"} "subject" => users
} }
}) do }) do
"@#{actor_nickname} deactivated user @#{subject_nickname}" "@#{actor_nickname} deactivated users: #{users_to_nicknames_string(users)}"
end end
@spec get_log_entry_message(ModerationLog) :: String.t() @spec get_log_entry_message(ModerationLog) :: String.t()
@ -402,14 +400,9 @@ def get_log_entry_message(%ModerationLog{
"action" => "tag" "action" => "tag"
} }
}) do }) do
nicknames_string =
nicknames
|> Enum.map(&"@#{&1}")
|> Enum.join(", ")
tags_string = tags |> 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 end
@spec get_log_entry_message(ModerationLog) :: String.t() @spec get_log_entry_message(ModerationLog) :: String.t()
@ -421,14 +414,9 @@ def get_log_entry_message(%ModerationLog{
"action" => "untag" "action" => "untag"
} }
}) do }) do
nicknames_string =
nicknames
|> Enum.map(&"@#{&1}")
|> Enum.join(", ")
tags_string = tags |> 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 end
@spec get_log_entry_message(ModerationLog) :: String.t() @spec get_log_entry_message(ModerationLog) :: String.t()
@ -436,11 +424,11 @@ def get_log_entry_message(%ModerationLog{
data: %{ data: %{
"actor" => %{"nickname" => actor_nickname}, "actor" => %{"nickname" => actor_nickname},
"action" => "grant", "action" => "grant",
"subject" => %{"nickname" => subject_nickname}, "subject" => users,
"permission" => permission "permission" => permission
} }
}) do }) do
"@#{actor_nickname} made @#{subject_nickname} #{permission}" "@#{actor_nickname} made #{users_to_nicknames_string(users)} #{permission}"
end end
@spec get_log_entry_message(ModerationLog) :: String.t() @spec get_log_entry_message(ModerationLog) :: String.t()
@ -448,11 +436,11 @@ def get_log_entry_message(%ModerationLog{
data: %{ data: %{
"actor" => %{"nickname" => actor_nickname}, "actor" => %{"nickname" => actor_nickname},
"action" => "revoke", "action" => "revoke",
"subject" => %{"nickname" => subject_nickname}, "subject" => users,
"permission" => permission "permission" => permission
} }
}) do }) do
"@#{actor_nickname} revoked #{permission} role from @#{subject_nickname}" "@#{actor_nickname} revoked #{permission} role from #{users_to_nicknames_string(users)}"
end end
@spec get_log_entry_message(ModerationLog) :: String.t() @spec get_log_entry_message(ModerationLog) :: String.t()
@ -551,4 +539,16 @@ def get_log_entry_message(%ModerationLog{
}) do }) do
"@#{actor_nickname} deleted status ##{subject_id}" "@#{actor_nickname} deleted status ##{subject_id}"
end 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 end

View file

@ -17,6 +17,7 @@ defmodule Pleroma.Notification do
import Ecto.Query import Ecto.Query
import Ecto.Changeset import Ecto.Changeset
require Logger
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
@ -34,43 +35,92 @@ def changeset(%Notification{} = notification, attrs) do
end end
def for_user_query(user, opts \\ []) do def for_user_query(user, opts \\ []) do
query = Notification
Notification |> where(user_id: ^user.id)
|> 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( |> where(
[n, a], [n, a],
fragment( not fragment(
"? not in (SELECT ap_id FROM users WHERE info->'deactivated' @> 'true')", "activity_visibility(?, ?, ?) = ANY (?)",
a.actor 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 else
where(query, [n, a], a.actor not in ^user.info.muted_notifications) Logger.error("Could not exclude visibility to #{visibility}")
|> where([n, a], a.actor not in ^user.info.blocks) query
|> 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 end
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 def for_user(user, opts \\ %{}) do
user user
|> for_user_query(opts) |> for_user_query(opts)

View file

@ -401,11 +401,9 @@ defp increase_read_duration(_) do
defp client, do: Pleroma.ReverseProxy.Client defp client, do: Pleroma.ReverseProxy.Client
defp track_failed_url(url, code, opts) do defp track_failed_url(url, error, opts) do
code = to_string(code)
ttl = 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) Keyword.get(opts, :failed_request_ttl, @failed_request_ttl)
else else
nil nil

View file

@ -437,7 +437,9 @@ def follow(%User{} = follower, %User{info: info} = followed) do
{:error, "Could not follow user: #{followed.nickname} blocked you."} {:error, "Could not follow user: #{followed.nickname} blocked you."}
true -> 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) Websub.subscribe(follower, followed)
end end
@ -1059,7 +1061,15 @@ def deactivate_async(user, status \\ true) do
BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status}) BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
end 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 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_followers(user), &invalidate_cache/1)
Enum.each(get_friends(user), &update_follower_count/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)) update_info(user, &User.Info.update_notification_settings(&1, settings))
end end
def delete(users) when is_list(users) do
for user <- users, do: delete(user)
end
def delete(%User{} = user) do def delete(%User{} = user) do
BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id}) BackgroundWorker.enqueue("delete_user", %{"user_id" => user.id})
end end
@ -1625,6 +1639,12 @@ def change_info(user, fun) do
`fun` is called with the `user.info`. `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 def update_info(user, fun) do
user user
|> change_info(fun) |> change_info(fun)

View file

@ -4,11 +4,9 @@
defmodule Pleroma.User.Search do defmodule Pleroma.User.Search do
alias Pleroma.Pagination alias Pleroma.Pagination
alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
import Ecto.Query import Ecto.Query
@similarity_threshold 0.25
@limit 20 @limit 20
def search(query_string, opts \\ []) do def search(query_string, opts \\ []) do
@ -23,18 +21,10 @@ def search(query_string, opts \\ []) do
maybe_resolve(resolve, for_user, query_string) maybe_resolve(resolve, for_user, query_string)
{:ok, results} = results =
Repo.transaction(fn -> query_string
Ecto.Adapters.SQL.query( |> search_query(for_user, following)
Repo, |> Pagination.fetch_paginated(%{"offset" => offset, "limit" => result_limit}, :offset)
"select set_limit(#{@similarity_threshold})",
[]
)
query_string
|> search_query(for_user, following)
|> Pagination.fetch_paginated(%{"offset" => offset, "limit" => result_limit}, :offset)
end)
results results
end end
@ -56,15 +46,65 @@ defp search_query(query_string, for_user, following) do
|> base_query(following) |> base_query(following)
|> filter_blocked_user(for_user) |> filter_blocked_user(for_user)
|> filter_blocked_domains(for_user) |> filter_blocked_domains(for_user)
|> search_subqueries(query_string) |> fts_search(query_string)
|> union_subqueries |> trigram_rank(query_string)
|> distinct_query() |> boost_search_rank(for_user)
|> boost_search_rank_query(for_user)
|> subquery() |> subquery()
|> order_by(desc: :search_rank) |> order_by(desc: :search_rank)
|> maybe_restrict_local(for_user) |> maybe_restrict_local(for_user)
end 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, false), do: User
defp base_query(user, true), do: User.get_followers_query(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 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 defp maybe_resolve(true, user, query) do
case {limit(), user} do case {limit(), user} do
{:all, _} -> :noop {: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 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) friends_ids = User.get_friends_ids(for_user)
followers_ids = User.get_followers_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: search_rank:
fragment( fragment(
""" """
CASE WHEN (?) THEN 0.5 + (?) * 1.3 CASE WHEN (?) THEN (?) * 1.5
WHEN (?) THEN 0.5 + (?) * 1.2 WHEN (?) THEN (?) * 1.3
WHEN (?) THEN (?) * 1.1 WHEN (?) THEN (?) * 1.1
ELSE (?) END ELSE (?) END
""", """,
@ -154,70 +179,5 @@ defp boost_search_rank_query(query, for_user) do
) )
end end
@spec fts_search_subquery(User.t() | Ecto.Query.t(), String.t()) :: Ecto.Query.t() defp boost_search_rank(query, _for_user), do: query
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])
end end

View file

@ -7,6 +7,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
alias Pleroma.Activity.Ir.Topics alias Pleroma.Activity.Ir.Topics
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.Conversation alias Pleroma.Conversation
alias Pleroma.Conversation.Participation
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Object.Containment alias Pleroma.Object.Containment
@ -153,11 +154,8 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when
Notification.create_notifications(activity) Notification.create_notifications(activity)
participations = conversation = create_or_bump_conversation(activity, map["actor"])
activity participations = get_participations(conversation)
|> Conversation.create_or_bump_for()
|> get_participations()
stream_out(activity) stream_out(activity)
stream_out_participations(participations) stream_out_participations(participations)
{:ok, activity} {:ok, activity}
@ -182,7 +180,20 @@ def insert(map, local \\ true, fake \\ false, bypass_actor_check \\ false) when
end end
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: [] defp get_participations(_), do: []
def stream_out_participations(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 # only accept false as false value
local = !(params[:local] == false) local = !(params[:local] == false)
published = params[:published] published = params[:published]
quick_insert? = Pleroma.Config.get([:env]) == :benchmark
with create_data <- with create_data <-
make_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}, {:fake, false, activity} <- {:fake, fake, activity},
_ <- increase_replies_count_if_reply(create_data), _ <- increase_replies_count_if_reply(create_data),
_ <- increase_poll_votes_if_vote(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 # Changing note count prior to enqueuing federation task in order to avoid
# race conditions on updating user.info # race conditions on updating user.info
{:ok, _actor} <- increase_note_count_if_public(actor, activity), {:ok, _actor} <- increase_note_count_if_public(actor, activity),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity} {:ok, activity}
else else
{:quick_insert, true, activity} ->
{:ok, activity}
{:fake, true, activity} -> {:fake, true, activity} ->
{:ok, activity} {:ok, activity}
@ -269,22 +285,21 @@ def listen(%{to: to, actor: actor, context: context, object: object} = params) d
end end
end end
def accept(%{to: to, actor: actor, object: object} = params) do def accept(params) do
# only accept false as false value accept_or_reject("Accept", params)
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
end end
def reject(%{to: to, actor: actor, object: object} = params) do def reject(params) do
# only accept false as false value accept_or_reject("Reject", params)
local = !(params[:local] == false) 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, activity} <- insert(data, local),
:ok <- maybe_federate(activity) do :ok <- maybe_federate(activity) do
{:ok, activity} {:ok, activity}
@ -409,18 +424,24 @@ def delete(%User{ap_id: ap_id, follower_address: follower_address} = user) do
end end
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) user = User.get_cached_by_ap_id(actor)
to = (object.data["to"] || []) ++ (object.data["cc"] || []) to = (object.data["to"] || []) ++ (object.data["cc"] || [])
with {:ok, object, activity} <- Object.delete(object), with {:ok, object, activity} <- Object.delete(object),
data <- %{ data <-
"type" => "Delete", %{
"actor" => actor, "type" => "Delete",
"object" => id, "actor" => actor,
"to" => to, "object" => id,
"deleted_activity_id" => activity && activity.id "to" => to,
}, "deleted_activity_id" => activity && activity.id
}
|> maybe_put("id", activity_id),
{:ok, activity} <- insert(data, local, false), {:ok, activity} <- insert(data, local, false),
stream_out_participations(object, user), stream_out_participations(object, user),
_ <- decrease_replies_count_if_reply(object), _ <- decrease_replies_count_if_reply(object),
@ -591,6 +612,49 @@ defp restrict_visibility(_query, %{visibility: visibility})
defp restrict_visibility(query, _visibility), do: query 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} = _), defp restrict_thread_visibility(query, _, %{skip_thread_containment: true} = _),
do: query do: query
@ -955,6 +1019,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|> restrict_muted_reblogs(opts) |> restrict_muted_reblogs(opts)
|> Activity.restrict_deactivated_users() |> Activity.restrict_deactivated_users()
|> exclude_poll_votes(opts) |> exclude_poll_votes(opts)
|> exclude_visibility(opts)
end end
def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do def fetch_activities(recipients, opts \\ %{}, pagination \\ :keyset) do

View file

@ -514,7 +514,7 @@ def handle_incoming(
end end
def handle_incoming( def handle_incoming(
%{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => _id} = data, %{"type" => "Accept", "object" => follow_object, "actor" => _actor, "id" => id} = data,
_options _options
) do ) do
with actor <- Containment.get_actor(data), with actor <- Containment.get_actor(data),
@ -528,7 +528,8 @@ def handle_incoming(
type: "Accept", type: "Accept",
actor: followed, actor: followed,
object: follow_activity.data["id"], object: follow_activity.data["id"],
local: false local: false,
activity_id: id
}) })
else else
_e -> :error _e -> :error
@ -536,7 +537,7 @@ def handle_incoming(
end end
def handle_incoming( def handle_incoming(
%{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => _id} = data, %{"type" => "Reject", "object" => follow_object, "actor" => _actor, "id" => id} = data,
_options _options
) do ) do
with actor <- Containment.get_actor(data), with actor <- Containment.get_actor(data),
@ -550,7 +551,8 @@ def handle_incoming(
type: "Reject", type: "Reject",
actor: followed, actor: followed,
object: follow_activity.data["id"], object: follow_activity.data["id"],
local: false local: false,
activity_id: id
}) do }) do
User.unfollow(follower, followed) 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 # an error or a tombstone. This would allow us to verify that a deletion actually took
# place. # place.
def handle_incoming( def handle_incoming(
%{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => _id} = data, %{"type" => "Delete", "object" => object_id, "actor" => actor, "id" => id} = data,
_options _options
) do ) do
object_id = Utils.get_ap_id(object_id) 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, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
{:ok, object} <- get_obj_helper(object_id), {:ok, object} <- get_obj_helper(object_id),
:ok <- Containment.contain_origin(actor.ap_id, object.data), :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} {:ok, activity}
else else
nil -> nil ->

View file

@ -46,6 +46,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
:user_delete, :user_delete,
:users_create, :users_create,
:user_toggle_activation, :user_toggle_activation,
:user_activate,
:user_deactivate,
:tag_users, :tag_users,
:untag_users, :untag_users,
:right_add, :right_add,
@ -98,7 +100,7 @@ def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
ModerationLog.insert_log(%{ ModerationLog.insert_log(%{
actor: admin, actor: admin,
subject: user, subject: [user],
action: "delete" action: "delete"
}) })
@ -106,6 +108,20 @@ def user_delete(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
|> json(nickname) |> json(nickname)
end 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, %{ def user_follow(%{assigns: %{user: admin}} = conn, %{
"follower" => follower_nick, "follower" => follower_nick,
"followed" => followed_nick "followed" => followed_nick
@ -240,7 +256,7 @@ def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => ni
ModerationLog.insert_log(%{ ModerationLog.insert_log(%{
actor: admin, actor: admin,
subject: user, subject: [user],
action: action action: action
}) })
@ -249,6 +265,36 @@ def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => ni
|> render("show.json", %{user: updated_user}) |> render("show.json", %{user: updated_user})
end 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 def tag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "tags" => tags}) do
with {:ok, _} <- User.tag(nicknames, tags) do with {:ok, _} <- User.tag(nicknames, tags) do
ModerationLog.insert_log(%{ ModerationLog.insert_log(%{
@ -313,6 +359,31 @@ defp maybe_parse_filters(filters) do
|> Enum.into(%{}, &{&1, true}) |> Enum.into(%{}, &{&1, true})
end 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, %{ def right_add(%{assigns: %{user: admin}} = conn, %{
"permission_group" => permission_group, "permission_group" => permission_group,
"nickname" => nickname "nickname" => nickname
@ -328,7 +399,7 @@ def right_add(%{assigns: %{user: admin}} = conn, %{
ModerationLog.insert_log(%{ ModerationLog.insert_log(%{
action: "grant", action: "grant",
actor: admin, actor: admin,
subject: user, subject: [user],
permission: permission_group permission: permission_group
}) })
@ -349,8 +420,36 @@ def right_get(conn, %{"nickname" => nickname}) do
}) })
end end
def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do def right_delete_multiple(
render_error(conn, :forbidden, "You can't revoke your own admin status.") %{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 end
def right_delete( def right_delete(
@ -371,34 +470,15 @@ def right_delete(
ModerationLog.insert_log(%{ ModerationLog.insert_log(%{
action: "revoke", action: "revoke",
actor: admin, actor: admin,
subject: user, subject: [user],
permission: permission_group permission: permission_group
}) })
json(conn, info) json(conn, info)
end end
def right_delete(conn, _) do def right_delete(%{assigns: %{user: %{nickname: nickname}}} = conn, %{"nickname" => nickname}) do
render_error(conn, :not_found, "No such permission_group") render_error(conn, :forbidden, "You can't revoke your own admin status.")
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
end end
def relay_list(conn, _params) do def relay_list(conn, _params) do

View file

@ -19,6 +19,12 @@ def render("index.json", %{users: users, count: count, page_size: page_size}) do
} }
end 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 def render("show.json", %{user: user}) do
avatar = User.avatar_url(user) |> MediaProxy.url() avatar = User.avatar_url(user) |> MediaProxy.url()
display_name = HTML.strip_tags(user.name || user.nickname) display_name = HTML.strip_tags(user.name || user.nickname)

View file

@ -71,6 +71,7 @@ def get_scheduled_activities(user, params \\ %{}) do
defp cast_params(params) do defp cast_params(params) do
param_types = %{ param_types = %{
exclude_types: {:array, :string}, exclude_types: {:array, :string},
exclude_visibilities: {:array, :string},
reblogs: :boolean, reblogs: :boolean,
with_muted: :boolean with_muted: :boolean
} }

View file

@ -35,6 +35,13 @@ def init(%{qs: qs} = req, state) do
{_, stream} <- List.keyfind(params, "stream", 0), {_, stream} <- List.keyfind(params, "stream", 0),
{:ok, user} <- allow_request(stream, [access_token, sec_websocket]), {:ok, user} <- allow_request(stream, [access_token, sec_websocket]),
topic when is_binary(topic) <- expand_topic(stream, params) do 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}} {:cowboy_websocket, req, %{user: user, topic: topic}, %{idle_timeout: @timeout}}
else else
{:error, code} -> {:error, code} ->

View file

@ -11,7 +11,7 @@ defmodule Pleroma.Web.OStatus.DeleteHandler do
def handle_delete(entry, _doc \\ nil) do def handle_delete(entry, _doc \\ nil) do
with id <- XML.string_from_xpath("//id", entry), with id <- XML.string_from_xpath("//id", entry),
%Object{} = object <- Object.normalize(id), %Object{} = object <- Object.normalize(id),
{:ok, delete} <- ActivityPub.delete(object, false) do {:ok, delete} <- ActivityPub.delete(object, local: false) do
delete delete
end end
end end

View file

@ -137,11 +137,14 @@ defmodule Pleroma.Web.Router do
delete("/users", AdminAPIController, :user_delete) delete("/users", AdminAPIController, :user_delete)
post("/users", AdminAPIController, :users_create) post("/users", AdminAPIController, :users_create)
patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation) 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) put("/users/tag", AdminAPIController, :tag_users)
delete("/users/tag", AdminAPIController, :untag_users) delete("/users/tag", AdminAPIController, :untag_users)
get("/users/:nickname/permission_group", AdminAPIController, :right_get) get("/users/:nickname/permission_group", AdminAPIController, :right_get)
get("/users/:nickname/permission_group/: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) post("/users/:nickname/permission_group/:permission_group", AdminAPIController, :right_add)
delete( delete(
@ -150,7 +153,13 @@ defmodule Pleroma.Web.Router do
:right_delete :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) get("/relay", AdminAPIController, :relay_list)
post("/relay", AdminAPIController, :relay_follow) post("/relay", AdminAPIController, :relay_follow)

View file

@ -49,7 +49,7 @@ defp handle_should_send(:test) do
end end
end end
defp handle_should_send(_) do defp handle_should_send(:benchmark), do: false
true
end defp handle_should_send(_), do: true
end end

View file

@ -69,6 +69,7 @@ def application do
end end
# Specifies which paths to compile per environment. # 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(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"] 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"]), with {branch_name, 0} <- System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]),
branch_name <- String.trim(branch_name), branch_name <- String.trim(branch_name),
branch_name <- System.get_env("PLEROMA_BUILD_BRANCH") || 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 =
branch_name branch_name
|> String.trim() |> String.trim()

View file

@ -35,11 +35,11 @@ detect_branch() {
if [ "$branch" = "develop" ]; then if [ "$branch" = "develop" ]; then
echo "develop" echo "develop"
elif [ "$branch" = "" ]; then elif [ "$branch" = "" ]; then
echo "master" echo "stable"
else else
# Note: branch name in version is of SemVer format and may only contain [0-9a-zA-Z-] symbols — # 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. # 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 exit 1
fi fi
} }

View file

@ -23,6 +23,39 @@ test "getting a participation will also preload things" do
assert %Pleroma.Conversation{} = participation.conversation assert %Pleroma.Conversation{} = participation.conversation
end 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 test "for a new conversation, it sets the recipents of the participation" do
user = insert(:user) user = insert(:user)
other_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"}) CommonAPI.post(user, %{"status" => "Hey @#{other_user.nickname}.", "visibility" => "direct"})
user = User.get_cached_by_id(user.id) 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] = Participation.for_user(user)
participation = Pleroma.Repo.preload(participation, :recipients) participation = Pleroma.Repo.preload(participation, :recipients)

View file

@ -24,13 +24,13 @@ test "logging user deletion by moderator", %{moderator: moderator, subject1: sub
{:ok, _} = {:ok, _} =
ModerationLog.insert_log(%{ ModerationLog.insert_log(%{
actor: moderator, actor: moderator,
subject: subject1, subject: [subject1],
action: "delete" action: "delete"
}) })
log = Repo.one(ModerationLog) 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 end
test "logging user creation by moderator", %{ test "logging user creation by moderator", %{
@ -128,7 +128,7 @@ test "logging user grant by moderator", %{moderator: moderator, subject1: subjec
{:ok, _} = {:ok, _} =
ModerationLog.insert_log(%{ ModerationLog.insert_log(%{
actor: moderator, actor: moderator,
subject: subject1, subject: [subject1],
action: "grant", action: "grant",
permission: "moderator" permission: "moderator"
}) })
@ -142,7 +142,7 @@ test "logging user revoke by moderator", %{moderator: moderator, subject1: subje
{:ok, _} = {:ok, _} =
ModerationLog.insert_log(%{ ModerationLog.insert_log(%{
actor: moderator, actor: moderator,
subject: subject1, subject: [subject1],
action: "revoke", action: "revoke",
permission: "moderator" permission: "moderator"
}) })

View file

@ -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) assert [u2.id, u1.id] == Enum.map(User.search("bar word"), & &1.id)
end 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 test "finds users, boosting ranks of friends and followers" do
u1 = insert(:user) u1 = insert(:user)
u2 = insert(:user, %{name: "Doe"}) 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) Pleroma.Config.put([:instance, :limit_to_local_content], :unauthenticated)
end 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 test "does not yield false-positive matches" do
insert(:user, %{name: "John Doe"}) insert(:user, %{name: "John Doe"})

View file

@ -41,6 +41,27 @@ test "it streams them out" do
assert called(Pleroma.Web.Streamer.stream("participation", participations)) assert called(Pleroma.Web.Streamer.stream("participation", participations))
end end
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 end
describe "fetching restricted by visibility" do describe "fetching restricted by visibility" do
@ -87,6 +108,66 @@ test "it restricts by the appropriate visibility" do
end end
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 describe "building a user from his ap id" do
test "it returns a user" do test "it returns a user" do
user_id = "http://mastodon.example.org/users/admin" user_id = "http://mastodon.example.org/users/admin"

View file

@ -682,6 +682,7 @@ test "it works for incoming update activities which lock the account" do
test "it works for incoming deletes" do test "it works for incoming deletes" do
activity = insert(:note_activity) activity = insert(:note_activity)
deleting_user = insert(:user)
data = data =
File.read!("test/fixtures/mastodon-delete.json") File.read!("test/fixtures/mastodon-delete.json")
@ -694,11 +695,14 @@ test "it works for incoming deletes" do
data = data =
data data
|> Map.put("object", object) |> 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) refute Activity.get_by_id(activity.id)
assert actor == deleting_user.ap_id
end end
test "it fails for incoming deletes with spoofed origin" do 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["object"] == follow_activity.data["id"]
assert activity.data["id"] == accept_data["id"]
follower = User.get_cached_by_id(follower.id) follower = User.get_cached_by_id(follower.id)
assert User.following?(follower, followed) == true 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) {:ok, activity} = Transmogrifier.handle_incoming(reject_data)
refute activity.local refute activity.local
assert activity.data["id"] == reject_data["id"]
follower = User.get_cached_by_id(follower.id) follower = User.get_cached_by_id(follower.id)

View file

@ -23,8 +23,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
:ok :ok
end end
describe "/api/pleroma/admin/users" do describe "DELETE /api/pleroma/admin/users" do
test "Delete" do test "single user" do
admin = insert(:user, info: %{is_admin: true}) admin = insert(:user, info: %{is_admin: true})
user = insert(:user) user = insert(:user)
@ -36,15 +36,36 @@ test "Delete" do
log_entry = Repo.one(ModerationLog) 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) == 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 assert json_response(conn, 200) == user.nickname
end 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 test "Create" do
admin = insert(:user, info: %{is_admin: true}) 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" "@#{admin.nickname} made @#{user.nickname} admin"
end 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 test "/:right DELETE, can remove from a permission group" do
admin = insert(:user, info: %{is_admin: true}) admin = insert(:user, info: %{is_admin: true})
user = 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) == assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} revoked admin role from @#{user.nickname}" "@#{admin.nickname} revoked admin role from @#{user.nickname}"
end end
end
describe "PUT /api/pleroma/admin/users/:nickname/activation_status" do test "/:right DELETE, can remove from a permission group (multiple)" do
setup %{conn: conn} do
admin = insert(:user, info: %{is_admin: true}) 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 =
conn build_conn()
|> assign(:user, admin) |> assign(:user, admin)
|> put_req_header("accept", "application/json") |> 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} assert json_response(conn, 200) == %{
end "is_admin" => false
}
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)
log_entry = Repo.one(ModerationLog) log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) == assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} deactivated user @#{user.nickname}" "@#{admin.nickname} revoked admin role from @#{user_one.nickname}, @#{
end user_two.nickname
}"
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)
end end
end end
@ -1035,6 +1046,50 @@ test "it works with multiple filters" do
end end
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 test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do
admin = insert(:user, info: %{is_admin: true}) admin = insert(:user, info: %{is_admin: true})
user = insert(:user) user = insert(:user)
@ -1059,7 +1114,7 @@ test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do
log_entry = Repo.one(ModerationLog) log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) == assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} deactivated user @#{user.nickname}" "@#{admin.nickname} deactivated users: @#{user.nickname}"
end end
describe "POST /api/pleroma/admin/users/invite_token" do describe "POST /api/pleroma/admin/users/invite_token" do

View file

@ -237,6 +237,20 @@ test "filters user's statuses by a hashtag", %{conn: conn} do
assert [%{"id" => id}] = json_response(conn, 200) assert [%{"id" => id}] = json_response(conn, 200)
assert id == to_string(post.id) assert id == to_string(post.id)
end 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 end
describe "followers" do describe "followers" do

View file

@ -54,9 +54,9 @@ test "returns a list of conversations", %{conn: conn} do
assert user_two.id in account_ids assert user_two.id in account_ids
assert user_three.id in account_ids assert user_three.id in account_ids
assert is_binary(res_id) assert is_binary(res_id)
assert unread == true assert unread == false
assert res_last_status["id"] == direct.id 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 end
test "updates the last_status on reply", %{conn: conn} do 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" "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}] = [%{"id" => direct_conversation_id, "unread" => true}] =
conn conn
|> assign(:user, user_one) |> assign(:user, user_two)
|> get("/api/v1/conversations") |> get("/api/v1/conversations")
|> json_response(200) |> json_response(200)
%{"unread" => false} = %{"unread" => false} =
conn conn
|> assign(:user, user_one) |> assign(:user, user_two)
|> post("/api/v1/conversations/#{direct_conversation_id}/read") |> post("/api/v1/conversations/#{direct_conversation_id}/read")
|> json_response(200) |> json_response(200)
assert User.get_cached_by_id(user_one.id).info.unread_conversation_count == 0 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 # The conversation is marked as unread on reply
{:ok, _} = {:ok, _} =
@ -124,6 +128,7 @@ test "the user marks a conversation as read", %{conn: conn} do
|> json_response(200) |> json_response(200)
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 == 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 # A reply doesn't increment the user's unread_conversation_count if the conversation is unread
{:ok, _} = {: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_one.id).info.unread_conversation_count == 1
assert User.get_cached_by_id(user_two.id).info.unread_conversation_count == 0
end end
test "(vanilla) Mastodon frontend behaviour", %{conn: conn} do test "(vanilla) Mastodon frontend behaviour", %{conn: conn} do

View file

@ -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 assert [%{"id" => ^notification3_id}, %{"id" => ^notification2_id}] = result
end 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 test "filters notifications using exclude_types", %{conn: conn} do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)

View file

@ -40,9 +40,9 @@ test "it returns empty result if user or status search return undefined error",
test "search", %{conn: conn} do test "search", %{conn: conn} do
user = insert(:user) user = insert(:user)
user_two = insert(:user, %{nickname: "shp@shitposter.club"}) 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} = {:ok, _activity} =
CommonAPI.post(user, %{ CommonAPI.post(user, %{
@ -70,7 +70,8 @@ test "search", %{conn: conn} do
get(conn, "/api/v2/search", %{"q" => "天子"}) get(conn, "/api/v2/search", %{"q" => "天子"})
|> json_response(200) |> json_response(200)
assert account["id"] == to_string(user_three.id) [status] = results["statuses"]
assert status["id"] == to_string(activity.id)
end end
end end

View file

@ -20,27 +20,52 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
:ok :ok
end end
test "the home timeline", %{conn: conn} do describe "home" do
user = insert(:user) test "the home timeline", %{conn: conn} do
following = insert(:user) user = insert(:user)
following = insert(:user)
{:ok, _activity} = CommonAPI.post(following, %{"status" => "test"}) {:ok, _activity} = CommonAPI.post(following, %{"status" => "test"})
conn = conn =
conn conn
|> assign(:user, user) |> assign(:user, user)
|> get("/api/v1/timelines/home") |> 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 = conn =
build_conn() build_conn()
|> assign(:user, user) |> assign(:user, user)
|> get("/api/v1/timelines/home") |> 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 end
describe "public" do describe "public" do

View file

@ -424,8 +424,8 @@ test "shows unread_conversation_count only to the account owner" do
other_user = insert(:user) other_user = insert(:user)
{:ok, _activity} = {:ok, _activity} =
CommonAPI.post(user, %{ CommonAPI.post(other_user, %{
"status" => "Hey @#{other_user.nickname}.", "status" => "Hey @#{user.nickname}.",
"visibility" => "direct" "visibility" => "direct"
}) })