Merge branch 'develop' of https://git.pleroma.social/pleroma/pleroma into develop

This commit is contained in:
sadposter 2019-10-18 13:12:58 +01:00
commit 6b9c5e4696
50 changed files with 1814 additions and 430 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,12 @@ 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).
### Removed
- **Breaking**: Removed 1.0+ deprecated configurations `Pleroma.Upload, :strip_exif` and `:instance, :dedupe_media`
### Changed ### Changed
- **Breaking:** Elixir >=1.8 is now required (was >= 1.7) - **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
@ -29,6 +35,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`)
@ -37,6 +44,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,7 +35,6 @@ 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( |> where(
@ -54,11 +54,17 @@ def for_user_query(user, opts \\ []) do
) )
) )
|> preload([n, a, o], activity: {a, object: o}) |> preload([n, a, o], activity: {a, object: o})
|> exclude_muted(user, opts)
|> exclude_visibility(opts)
end
if opts[:with_muted] do defp exclude_muted(query, _, %{with_muted: true}) do
query query
else end
where(query, [n, a], a.actor not in ^user.info.muted_notifications)
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], a.actor not in ^user.info.blocks)
|> where( |> where(
[n, a], [n, a],
@ -69,7 +75,51 @@ def for_user_query(user, opts \\ []) do
) )
|> where([n, a, o, tm], is_nil(tm.user_id)) |> where([n, a, o, tm], is_nil(tm.user_id))
end end
@valid_visibilities ~w[direct unlisted public private]
defp exclude_visibility(query, %{exclude_visibilities: visibility})
when is_list(visibility) do
if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
query
|> where(
[n, a],
not fragment(
"activity_visibility(?, ?, ?) = ANY (?)",
a.actor,
a.recipients,
a.data,
^visibility
)
)
else
Logger.error("Could not exclude visibility to #{visibility}")
query
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

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

@ -105,7 +105,7 @@ defp get_opts(opts) do
{Pleroma.Config.get!([:instance, :upload_limit]), "Document"} {Pleroma.Config.get!([:instance, :upload_limit]), "Document"}
end end
opts = %{ %{
activity_type: Keyword.get(opts, :activity_type, activity_type), activity_type: Keyword.get(opts, :activity_type, activity_type),
size_limit: Keyword.get(opts, :size_limit, size_limit), size_limit: Keyword.get(opts, :size_limit, size_limit),
uploader: Keyword.get(opts, :uploader, Pleroma.Config.get([__MODULE__, :uploader])), uploader: Keyword.get(opts, :uploader, Pleroma.Config.get([__MODULE__, :uploader])),
@ -118,37 +118,6 @@ defp get_opts(opts) do
Pleroma.Config.get([__MODULE__, :base_url], Pleroma.Web.base_url()) Pleroma.Config.get([__MODULE__, :base_url], Pleroma.Web.base_url())
) )
} }
# TODO: 1.0+ : remove old config compatibility
opts =
if Pleroma.Config.get([__MODULE__, :strip_exif]) == true &&
!Enum.member?(opts.filters, Pleroma.Upload.Filter.Mogrify) do
Logger.warn("""
Pleroma: configuration `:instance, :strip_exif` is deprecated, please instead set:
:pleroma, Pleroma.Upload, [filters: [Pleroma.Upload.Filter.Mogrify]]
:pleroma, Pleroma.Upload.Filter.Mogrify, args: ["strip", "auto-orient"]
""")
Pleroma.Config.put([Pleroma.Upload.Filter.Mogrify], args: ["strip", "auto-orient"])
Map.put(opts, :filters, opts.filters ++ [Pleroma.Upload.Filter.Mogrify])
else
opts
end
if Pleroma.Config.get([:instance, :dedupe_media]) == true &&
!Enum.member?(opts.filters, Pleroma.Upload.Filter.Dedupe) do
Logger.warn("""
Pleroma: configuration `:instance, :dedupe_media` is deprecated, please instead set:
:pleroma, Pleroma.Upload, [filters: [Pleroma.Upload.Filter.Dedupe]]
""")
Map.put(opts, :filters, opts.filters ++ [Pleroma.Upload.Filter.Dedupe])
else
opts
end
end end
defp prepare_upload(%Plug.Upload{} = file, opts) do defp prepare_upload(%Plug.Upload{} = file, opts) do

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 ->
Ecto.Adapters.SQL.query(
Repo,
"select set_limit(#{@similarity_threshold})",
[]
)
query_string query_string
|> search_query(for_user, following) |> search_query(for_user, following)
|> Pagination.fetch_paginated(%{"offset" => offset, "limit" => result_limit}, :offset) |> 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", "type" => "Delete",
"actor" => actor, "actor" => actor,
"object" => id, "object" => id,
"to" => to, "to" => to,
"deleted_activity_id" => activity && activity.id "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_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) do def relay_follow(%{assigns: %{user: admin}} = conn, %{"relay_url" => target}) 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
)
post("/relay", AdminAPIController, :relay_follow) post("/relay", AdminAPIController, :relay_follow)
delete("/relay", AdminAPIController, :relay_unfollow) delete("/relay", AdminAPIController, :relay_unfollow)

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,31 +35,62 @@ 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
} }
update() { update() {
set -e set -e
NO_RM=false
while echo "$1" | grep "^-" >/dev/null; do
case "$1" in
--zip-url)
FULL_URI="$2"
shift 2
;;
--no-rm)
NO_RM=true
shift
;;
--flavour)
FLAVOUR="$2"
shift 2
;;
--branch)
BRANCH="$2"
shift 2
;;
--tmp-dir)
TMP_DIR="$2"
shift 2
;;
-*)
echo "invalid option: $1" 1>&2
shift
;;
esac
done
RELEASE_ROOT=$(dirname "$SCRIPTPATH") RELEASE_ROOT=$(dirname "$SCRIPTPATH")
uri="${PLEROMA_CTL_URI:-https://git.pleroma.social}" uri="https://git.pleroma.social"
project_id="${PLEROMA_CTL_PROJECT_ID:-2}" project_id="2"
project_branch="$(detect_branch)" project_branch="${BRANCH:-$(detect_branch)}"
flavour="${PLEROMA_CTL_FLAVOUR:-$(detect_flavour)}" flavour="${FLAVOUR:-$(detect_flavour)}"
echo "Detected flavour: $flavour" tmp="${TMP_DIR:-/tmp}"
tmp="${PLEROMA_CTL_TMP_DIR:-/tmp}"
artifact="$tmp/pleroma.zip" artifact="$tmp/pleroma.zip"
full_uri="${uri}/api/v4/projects/${project_id}/jobs/artifacts/${project_branch}/download?job=${flavour}" full_uri="${FULL_URI:-${uri}/api/v4/projects/${project_id}/jobs/artifacts/${project_branch}/download?job=${flavour}}"
echo "Downloading the artifact from ${full_uri} to ${artifact}" echo "Downloading the artifact from ${full_uri} to ${artifact}"
curl "$full_uri" -o "${artifact}" curl "$full_uri" -o "${artifact}"
echo "Unpacking ${artifact} to ${tmp}" echo "Unpacking ${artifact} to ${tmp}"
unzip -q "$artifact" -d "$tmp" unzip -q "$artifact" -d "$tmp"
echo "Copying files over to $RELEASE_ROOT" echo "Copying files over to $RELEASE_ROOT"
if [ "$1" != "--no-rm" ]; then if [ "$NO_RM" = false ]; then
echo "Removing files from the previous release"
rm -r "${RELEASE_ROOT:-?}"/* rm -r "${RELEASE_ROOT:-?}"/*
fi fi
cp -rf "$tmp/release"/* "$RELEASE_ROOT" cp -rf "$tmp/release"/* "$RELEASE_ROOT"
@ -86,36 +117,41 @@ if [ -z "$1" ] || [ "$1" = "help" ]; then
Rollback database migrations (needs to be done before downgrading) Rollback database migrations (needs to be done before downgrading)
update [OPTIONS] update [OPTIONS]
Update the instance using the latest CI artifact for the current branch. Update the instance.
The only supported option is --no-rm, when set the script won't delete the whole directory, but
just force copy over files from the new release. This wastes more space, but may be useful if
some files are stored inside of the release directories (although you really shouldn't store them
there), or if you want to be able to quickly revert a broken update.
The script will try to detect your architecture and ABI and set a flavour automatically,
but if it is wrong, you can overwrite it by setting PLEROMA_CTL_FLAVOUR to the desired flavour.
By default the artifact will be downloaded from https://git.pleroma.social for pleroma/pleroma (project id: 2)
to /tmp/, you can overwrite these settings by setting PLEROMA_CTL_URI, PLEROMA_CTL_PROJECT_ID and PLEROMA_CTL_TMP_DIR
respectively.
Options:
--branch Update to a specified branch, instead of the latest version of the current one.
--flavour Update to a specified flavour (CPU architecture+libc), instead of the current one.
--zip-url Get the release from a specified url. If set, renders the previous 2 options inactive.
--no-rm Do not erase previous release's files.
--tmp-dir Download the temporary files to a specified directory.
and any mix tasks under Pleroma namespace, for example \`mix pleroma.user COMMAND\` is and any mix tasks under Pleroma namespace, for example \`mix pleroma.user COMMAND\` is
equivalent to \`$(basename "$0") user COMMAND\` equivalent to \`$(basename "$0") user COMMAND\`
By default pleroma_ctl will try calling into a running instance to execute non migration-related commands, By default pleroma_ctl will try calling into a running instance to execute non migration-related commands,
if for some reason this is undesired, set PLEROMA_CTL_RPC_DISABLED environment variable if for some reason this is undesired, set PLEROMA_CTL_RPC_DISABLED environment variable.
" "
else else
SCRIPT=$(readlink -f "$0") SCRIPT=$(readlink -f "$0")
SCRIPTPATH=$(dirname "$SCRIPT") SCRIPTPATH=$(dirname "$SCRIPT")
if [ "$1" = "update" ]; then FULL_ARGS="$*"
update "$2"
elif [ "$1" = "migrate" ] || [ "$1" = "rollback" ] || [ "$1" = "create" ] || [ "$1 $2" = "instance gen" ] || [ -n "$PLEROMA_CTL_RPC_DISABLED" ]; then ACTION="$1"
"$SCRIPTPATH"/pleroma eval 'Pleroma.ReleaseTasks.run("'"$*"'")' shift
if [ "$(echo \"$1\" | grep \"^-\" >/dev/null)" = false ]; then
SUBACTION="$1"
shift
fi
if [ "$ACTION" = "update" ]; then
update "$@"
elif [ "$ACTION" = "migrate" ] || [ "$ACTION" = "rollback" ] || [ "$ACTION" = "create" ] || [ "$ACTION $SUBACTION" = "instance gen" ] || [ "$PLEROMA_CTL_RPC_DISABLED" = true ]; then
"$SCRIPTPATH"/pleroma eval 'Pleroma.ReleaseTasks.run("'"$FULL_ARGS"'")'
else else
"$SCRIPTPATH"/pleroma rpc 'Pleroma.ReleaseTasks.run("'"$*"'")' "$SCRIPTPATH"/pleroma rpc 'Pleroma.ReleaseTasks.run("'"$FULL_ARGS"'")'
fi fi
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

@ -17,8 +17,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
alias Pleroma.Web.MediaProxy alias Pleroma.Web.MediaProxy
import Pleroma.Factory import Pleroma.Factory
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)
@ -30,15 +30,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})
@ -404,6 +425,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})
@ -423,63 +467,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
@ -1029,6 +1040,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)
@ -1053,7 +1108,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,8 +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)
[account] == results["accounts"] [status] = results["statuses"]
assert account["id"] == to_string(user_three.id) assert status["id"] == to_string(activity.id)
end end
end end

View file

@ -20,6 +20,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
:ok :ok
end end
describe "home" do
test "the home timeline", %{conn: conn} do test "the home timeline", %{conn: conn} do
user = insert(:user) user = insert(:user)
following = insert(:user) following = insert(:user)
@ -43,6 +44,30 @@ test "the home timeline", %{conn: conn} do
assert [%{"content" => "test"}] = json_response(conn, :ok) assert [%{"content" => "test"}] = json_response(conn, :ok)
end end
test "the home timeline when the direct messages are excluded", %{conn: conn} do
user = insert(:user)
{:ok, public_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "public"})
{:ok, direct_activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
{:ok, unlisted_activity} =
CommonAPI.post(user, %{"status" => ".", "visibility" => "unlisted"})
{:ok, private_activity} =
CommonAPI.post(user, %{"status" => ".", "visibility" => "private"})
conn =
conn
|> assign(:user, user)
|> get("/api/v1/timelines/home", %{"exclude_visibilities" => ["direct"]})
assert status_ids = json_response(conn, :ok) |> Enum.map(& &1["id"])
assert public_activity.id in status_ids
assert unlisted_activity.id in status_ids
assert private_activity.id in status_ids
refute direct_activity.id in status_ids
end
end
describe "public" do describe "public" do
@tag capture_log: true @tag capture_log: true
test "the public timeline", %{conn: conn} do test "the public timeline", %{conn: conn} 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"
}) })