Merge remote-tracking branch 'origin/develop' into feature/account-export

This commit is contained in:
Egor Kislitsyn 2020-10-30 19:34:02 +04:00
commit d2113428c0
No known key found for this signature in database
GPG Key ID: 1B49CB15B71E7805
37 changed files with 1883 additions and 1731 deletions

View File

@ -12,6 +12,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Media preview proxy (requires `ffmpeg` and `ImageMagick` to be installed and media proxy to be enabled; see `:media_preview_proxy` config for more details). - Media preview proxy (requires `ffmpeg` and `ImageMagick` to be installed and media proxy to be enabled; see `:media_preview_proxy` config for more details).
- Pleroma API: Importing the mutes users from CSV files. - Pleroma API: Importing the mutes users from CSV files.
- Experimental websocket-based federation between Pleroma instances. - Experimental websocket-based federation between Pleroma instances.
- Support pagination of blocks and mutes
- App metrics: ability to restrict access to specified IP whitelist. - App metrics: ability to restrict access to specified IP whitelist.
- Account backup - Account backup
- Configuration: Add `:instance, autofollowing_nicknames` setting to provide a way to make accounts automatically follow new users that register on the local Pleroma instance. - Configuration: Add `:instance, autofollowing_nicknames` setting to provide a way to make accounts automatically follow new users that register on the local Pleroma instance.
@ -36,6 +37,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Pleroma API: Importing the mutes users from CSV files. - Pleroma API: Importing the mutes users from CSV files.
- Admin API: Importing emoji from a zip file - Admin API: Importing emoji from a zip file
- Pleroma API: Pagination for remote/local packs and emoji. - Pleroma API: Pagination for remote/local packs and emoji.
- Admin API: (`GET /api/pleroma/admin/users`) added filters user by `unconfirmed` status
- Admin API: (`GET /api/pleroma/admin/users`) added filters user by `actor_type`
</details> </details>
@ -53,6 +56,7 @@ switched to a new configuration mechanism, however it was not officially removed
- Allow sending out emails again. - Allow sending out emails again.
- Allow sending chat messages to yourself. - Allow sending chat messages to yourself.
- Fix remote users with a whitespace name. - Fix remote users with a whitespace name.
- OStatus / static FE endpoints: fixed inaccessibility for anonymous users on non-federating instances, switched to handling per `:restrict_unauthenticated` setting.
## Unreleased (Patch) ## Unreleased (Patch)

View File

@ -1757,28 +1757,37 @@
related_policy: "Pleroma.Web.ActivityPub.MRF.KeywordPolicy", related_policy: "Pleroma.Web.ActivityPub.MRF.KeywordPolicy",
label: "MRF Keyword", label: "MRF Keyword",
type: :group, type: :group,
description: "Reject or Word-Replace messages with a keyword or regex", description:
"Reject or Word-Replace messages matching a keyword or [Regex](https://hexdocs.pm/elixir/Regex.html).",
children: [ children: [
%{ %{
key: :reject, key: :reject,
type: {:list, :string}, type: {:list, :string},
description: description: """
"A list of patterns which result in message being rejected. Each pattern can be a string or a regular expression.", A list of patterns which result in message being rejected.
Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
""",
suggestions: ["foo", ~r/foo/iu] suggestions: ["foo", ~r/foo/iu]
}, },
%{ %{
key: :federated_timeline_removal, key: :federated_timeline_removal,
type: {:list, :string}, type: {:list, :string},
description: description: """
"A list of patterns which result in message being removed from federated timelines (a.k.a unlisted). Each pattern can be a string or a regular expression.", A list of patterns which result in message being removed from federated timelines (a.k.a unlisted).
Each pattern can be a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
""",
suggestions: ["foo", ~r/foo/iu] suggestions: ["foo", ~r/foo/iu]
}, },
%{ %{
key: :replace, key: :replace,
type: {:list, :tuple}, type: {:list, :tuple},
description: description: """
"A list of tuples containing {pattern, replacement}. Each pattern can be a string or a regular expression.", **Pattern**: a string or [Regex](https://hexdocs.pm/elixir/Regex.html) in the format of `~r/PATTERN/`.
suggestions: [{"foo", "bar"}, {~r/foo/iu, "bar"}]
**Replacement**: a string. Leaving the field empty is permitted.
"""
} }
] ]
}, },

View File

@ -20,12 +20,14 @@ Configuration options:
- `external`: only external users - `external`: only external users
- `active`: only active users - `active`: only active users
- `need_approval`: only unapproved users - `need_approval`: only unapproved users
- `unconfirmed`: only unconfirmed users
- `deactivated`: only deactivated users - `deactivated`: only deactivated users
- `is_admin`: users with admin role - `is_admin`: users with admin role
- `is_moderator`: users with moderator role - `is_moderator`: users with moderator role
- *optional* `page`: **integer** page number - *optional* `page`: **integer** page number
- *optional* `page_size`: **integer** number of users per page (default is `50`) - *optional* `page_size`: **integer** number of users per page (default is `50`)
- *optional* `tags`: **[string]** tags list - *optional* `tags`: **[string]** tags list
- *optional* `actor_types`: **[string]** actor type list (`Person`, `Service`, `Application`)
- *optional* `name`: **string** user display name - *optional* `name`: **string** user display name
- *optional* `email`: **string** user email - *optional* `email`: **string** user email
- Example: `https://mypleroma.org/api/pleroma/admin/users?query=john&filters=local,active&page=1&page_size=10&tags[]=some_tag&tags[]=another_tag&name=display_name&email=email@example.com` - Example: `https://mypleroma.org/api/pleroma/admin/users?query=john&filters=local,active&page=1&page_size=10&tags[]=some_tag&tags[]=another_tag&name=display_name&email=email@example.com`

View File

@ -1,12 +1,23 @@
# Managing frontends # Managing frontends
`mix pleroma.frontend install <frontend> [--ref <ref>] [--file <file>] [--build-url <build-url>] [--path <path>] [--build-dir <build-dir>]` === "OTP"
```sh
./bin/pleroma_ctl frontend install <frontend> [--ref <ref>] [--file <file>] [--build-url <build-url>] [--path <path>] [--build-dir <build-dir>]
```
=== "From Source"
```sh
mix pleroma.frontend install <frontend> [--ref <ref>] [--file <file>] [--build-url <build-url>] [--path <path>] [--build-dir <build-dir>]
```
Frontend can be installed either from local zip file, or automatically downloaded from the web. Frontend can be installed either from local zip file, or automatically downloaded from the web.
You can give all the options directly on the command like, but missing information will be filled out by looking at the data configured under `frontends.available` in the config files. You can give all the options directly on the command line, but missing information will be filled out by looking at the data configured under `frontends.available` in the config files.
Currently, known `<frontend>` values are:
Currently known `<frontend>` values are:
- [admin-fe](https://git.pleroma.social/pleroma/admin-fe) - [admin-fe](https://git.pleroma.social/pleroma/admin-fe)
- [kenoma](http://git.pleroma.social/lambadalambda/kenoma) - [kenoma](http://git.pleroma.social/lambadalambda/kenoma)
- [pleroma-fe](http://git.pleroma.social/pleroma/pleroma-fe) - [pleroma-fe](http://git.pleroma.social/pleroma/pleroma-fe)
@ -19,51 +30,67 @@ You can still install frontends that are not configured, see below.
For a frontend configured under the `available` key, it's enough to install it by name. For a frontend configured under the `available` key, it's enough to install it by name.
```sh tab="OTP" === "OTP"
./bin/pleroma_ctl frontend install pleroma
```
```sh tab="From Source" ```sh
mix pleroma.frontend install pleroma ./bin/pleroma_ctl frontend install pleroma
``` ```
This will download the latest build for the the pre-configured `ref` and install it. It can then be configured as the one of the served frontends in the config file (see `primary` or `admin`). === "From Source"
You can override any of the details. To install a pleroma build from a different url, you could do this: ```sh
mix pleroma.frontend install pleroma
```
```sh tab="OPT" This will download the latest build for the pre-configured `ref` and install it. It can then be configured as the one of the served frontends in the config file (see `primary` or `admin`).
./bin/pleroma_ctl frontend install pleroma --ref 2hu_edition --build-url https://example.org/raymoo.zip
```
```sh tab="From Source" You can override any of the details. To install a pleroma build from a different URL, you could do this:
mix pleroma.frontend install pleroma --ref 2hu_edition --build-url https://example.org/raymoo.zip
``` === "OTP"
```sh
./bin/pleroma_ctl frontend install pleroma --ref 2hu_edition --build-url https://example.org/raymoo.zip
```
=== "From Source"
```sh
mix pleroma.frontend install pleroma --ref 2hu_edition --build-url https://example.org/raymoo.zip
```
Similarly, you can also install from a local zip file. Similarly, you can also install from a local zip file.
```sh tab="OTP" === "OTP"
./bin/pleroma_ctl frontend install pleroma --ref mybuild --file ~/Downloads/doomfe.zip
```
```sh tab="From Source" ```sh
mix pleroma.frontend install pleroma --ref mybuild --file ~/Downloads/doomfe.zip ./bin/pleroma_ctl frontend install pleroma --ref mybuild --file ~/Downloads/doomfe.zip
``` ```
The resulting frontend will always be installed into a folder of this template: `${instance_static}/frontends/${name}/${ref}` === "From Source"
Careful: This folder will be completely replaced on installation ```sh
mix pleroma.frontend install pleroma --ref mybuild --file ~/Downloads/doomfe.zip
```
The resulting frontend will always be installed into a folder of this template: `${instance_static}/frontends/${name}/${ref}`.
Careful: This folder will be completely replaced on installation.
## Example installation for an unknown frontend ## Example installation for an unknown frontend
The installation process is the same, but you will have to give all the needed options on the commond line. For example: The installation process is the same, but you will have to give all the needed options on the command line. For example:
```sh tab="OTP" === "OTP"
./bin/pleroma_ctl frontend install gensokyo --ref master --build-url https://gensokyo.2hu/builds/marisa.zip
```
```sh tab="From Source" ```sh
mix pleroma.frontend install gensokyo --ref master --build-url https://gensokyo.2hu/builds/marisa.zip ./bin/pleroma_ctl frontend install gensokyo --ref master --build-url https://gensokyo.2hu/builds/marisa.zip
``` ```
If you don't have a zip file but just want to install a frontend from a local path, you can simply copy the files over a folder of this template: `${instance_static}/frontends/${name}/${ref}` === "From Source"
```sh
mix pleroma.frontend install gensokyo --ref master --build-url https://gensokyo.2hu/builds/marisa.zip
```
If you don't have a zip file but just want to install a frontend from a local path, you can simply copy the files over a folder of this template: `${instance_static}/frontends/${name}/${ref}`.

View File

@ -43,7 +43,7 @@ def get_for_ap_id(ap_id) do
def maybe_create_recipientships(participation, activity) do def maybe_create_recipientships(participation, activity) do
participation = Repo.preload(participation, :recipients) participation = Repo.preload(participation, :recipients)
if participation.recipients |> Enum.empty?() do if Enum.empty?(participation.recipients) do
recipients = User.get_all_by_ap_id(activity.recipients) recipients = User.get_all_by_ap_id(activity.recipients)
RecipientShip.create(recipients, participation) RecipientShip.create(recipients, participation)
end end
@ -69,10 +69,6 @@ def create_or_bump_for(activity, opts \\ []) do
Enum.map(users, fn user -> Enum.map(users, fn user ->
invisible_conversation = Enum.any?(users, &User.blocks?(user, &1)) invisible_conversation = Enum.any?(users, &User.blocks?(user, &1))
unless invisible_conversation do
User.increment_unread_conversation_count(conversation, user)
end
opts = Keyword.put(opts, :invisible_conversation, invisible_conversation) opts = Keyword.put(opts, :invisible_conversation, invisible_conversation)
{:ok, participation} = {:ok, participation} =

View File

@ -63,21 +63,10 @@ def mark_as_read(%User{} = user, %Conversation{} = conversation) do
end end
end end
def mark_as_read(participation) do def mark_as_read(%__MODULE__{} = participation) do
__MODULE__ participation
|> where(id: ^participation.id) |> change(read: true)
|> update(set: [read: true]) |> Repo.update()
|> select([p], p)
|> Repo.update_all([])
|> case do
{1, [participation]} ->
participation = Repo.preload(participation, :user)
User.set_unread_conversation_count(participation.user)
{:ok, participation}
error ->
error
end
end end
def mark_all_as_read(%User{local: true} = user, %User{} = target_user) do def mark_all_as_read(%User{local: true} = user, %User{} = target_user) do
@ -93,7 +82,6 @@ def mark_all_as_read(%User{local: true} = user, %User{} = target_user) do
|> update([p], set: [read: true]) |> update([p], set: [read: true])
|> Repo.update_all([]) |> Repo.update_all([])
{:ok, user} = User.set_unread_conversation_count(user)
{:ok, user, []} {:ok, user, []}
end end
@ -108,7 +96,6 @@ def mark_all_as_read(%User{} = user) do
|> select([p], p) |> select([p], p)
|> Repo.update_all([]) |> Repo.update_all([])
{:ok, user} = User.set_unread_conversation_count(user)
{:ok, user, participations} {:ok, user, participations}
end end
@ -220,6 +207,12 @@ def set_recipients(participation, user_ids) do
{:ok, Repo.preload(participation, :recipients, force: true)} {:ok, Repo.preload(participation, :recipients, force: true)}
end end
@spec unread_count(User.t()) :: integer()
def unread_count(%User{id: user_id}) do
from(q in __MODULE__, where: q.user_id == ^user_id and q.read == false)
|> Repo.aggregate(:count, :id)
end
def unread_conversation_count_for_user(user) do def unread_conversation_count_for_user(user) do
from(p in __MODULE__, from(p in __MODULE__,
where: p.user_id == ^user.id, where: p.user_id == ^user.id,

View File

@ -128,7 +128,6 @@ defmodule Pleroma.User do
field(:hide_followers, :boolean, default: false) field(:hide_followers, :boolean, default: false)
field(:hide_follows, :boolean, default: false) field(:hide_follows, :boolean, default: false)
field(:hide_favorites, :boolean, default: true) field(:hide_favorites, :boolean, default: true)
field(:unread_conversation_count, :integer, default: 0)
field(:pinned_activities, {:array, :string}, default: []) field(:pinned_activities, {:array, :string}, default: [])
field(:email_notifications, :map, default: %{"digest" => false}) field(:email_notifications, :map, default: %{"digest" => false})
field(:mascot, :map, default: nil) field(:mascot, :map, default: nil)
@ -1305,47 +1304,6 @@ def update_following_count(%User{local: true} = user) do
|> update_and_set_cache() |> update_and_set_cache()
end end
def set_unread_conversation_count(%User{local: true} = user) do
unread_query = Participation.unread_conversation_count_for_user(user)
User
|> join(:inner, [u], p in subquery(unread_query))
|> update([u, p],
set: [unread_conversation_count: p.count]
)
|> where([u], u.id == ^user.id)
|> select([u], u)
|> Repo.update_all([])
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
end
end
def set_unread_conversation_count(user), do: {:ok, user}
def increment_unread_conversation_count(conversation, %User{local: true} = user) do
unread_query =
Participation.unread_conversation_count_for_user(user)
|> where([p], p.conversation_id == ^conversation.id)
User
|> join(:inner, [u], p in subquery(unread_query))
|> update([u, p],
inc: [unread_conversation_count: 1]
)
|> where([u], u.id == ^user.id)
|> where([u, p], p.count == 0)
|> select([u], u)
|> Repo.update_all([])
|> case do
{1, [user]} -> set_cache(user)
_ -> {:error, user}
end
end
def increment_unread_conversation_count(_, user), do: {:ok, user}
@spec get_users_from_set([String.t()], keyword()) :: [User.t()] @spec get_users_from_set([String.t()], keyword()) :: [User.t()]
def get_users_from_set(ap_ids, opts \\ []) do def get_users_from_set(ap_ids, opts \\ []) do
local_only = Keyword.get(opts, :local_only, true) local_only = Keyword.get(opts, :local_only, true)

View File

@ -43,6 +43,7 @@ defmodule Pleroma.User.Query do
active: boolean(), active: boolean(),
deactivated: boolean(), deactivated: boolean(),
need_approval: boolean(), need_approval: boolean(),
unconfirmed: boolean(),
is_admin: boolean(), is_admin: boolean(),
is_moderator: boolean(), is_moderator: boolean(),
super_users: boolean(), super_users: boolean(),
@ -55,7 +56,8 @@ defmodule Pleroma.User.Query do
ap_id: [String.t()], ap_id: [String.t()],
order_by: term(), order_by: term(),
select: term(), select: term(),
limit: pos_integer() limit: pos_integer(),
actor_types: [String.t()]
} }
| map() | map()
@ -114,6 +116,10 @@ defp compose_query({:is_admin, bool}, query) do
where(query, [u], u.is_admin == ^bool) where(query, [u], u.is_admin == ^bool)
end end
defp compose_query({:actor_types, actor_types}, query) when is_list(actor_types) do
where(query, [u], u.actor_type in ^actor_types)
end
defp compose_query({:is_moderator, bool}, query) do defp compose_query({:is_moderator, bool}, query) do
where(query, [u], u.is_moderator == ^bool) where(query, [u], u.is_moderator == ^bool)
end end
@ -156,6 +162,10 @@ defp compose_query({:need_approval, _}, query) do
where(query, [u], u.approval_pending) where(query, [u], u.approval_pending)
end end
defp compose_query({:unconfirmed, _}, query) do
where(query, [u], u.confirmation_pending)
end
defp compose_query({:followers, %User{id: id}}, query) do defp compose_query({:followers, %User{id: id}}, query) do
query query
|> where([u], u.id != ^id) |> where([u], u.id != ^id)

View File

@ -44,29 +44,30 @@ def is_direct?(activity) do
def is_list?(%{data: %{"listMessage" => _}}), do: true def is_list?(%{data: %{"listMessage" => _}}), do: true
def is_list?(_), do: false def is_list?(_), do: false
@spec visible_for_user?(Activity.t(), User.t() | nil) :: boolean() @spec visible_for_user?(Activity.t() | nil, User.t() | nil) :: boolean()
def visible_for_user?(%{actor: ap_id}, %User{ap_id: ap_id}), do: true def visible_for_user?(%Activity{actor: ap_id}, %User{ap_id: ap_id}), do: true
def visible_for_user?(nil, _), do: false def visible_for_user?(nil, _), do: false
def visible_for_user?(%{data: %{"listMessage" => _}}, nil), do: false def visible_for_user?(%Activity{data: %{"listMessage" => _}}, nil), do: false
def visible_for_user?(%{data: %{"listMessage" => list_ap_id}} = activity, %User{} = user) do def visible_for_user?(
%Activity{data: %{"listMessage" => list_ap_id}} = activity,
%User{} = user
) do
user.ap_id in activity.data["to"] || user.ap_id in activity.data["to"] ||
list_ap_id list_ap_id
|> Pleroma.List.get_by_ap_id() |> Pleroma.List.get_by_ap_id()
|> Pleroma.List.member?(user) |> Pleroma.List.member?(user)
end end
def visible_for_user?(%{local: local} = activity, nil) do def visible_for_user?(%Activity{} = activity, nil) do
cfg_key = if local, do: :local, else: :remote if restrict_unauthenticated_access?(activity),
if Pleroma.Config.restrict_unauthenticated_access?(:activities, cfg_key),
do: false, do: false,
else: is_public?(activity) else: is_public?(activity)
end end
def visible_for_user?(activity, user) do def visible_for_user?(%Activity{} = activity, user) do
x = [user.ap_id | User.following(user)] x = [user.ap_id | User.following(user)]
y = [activity.actor] ++ activity.data["to"] ++ (activity.data["cc"] || []) y = [activity.actor] ++ activity.data["to"] ++ (activity.data["cc"] || [])
is_public?(activity) || Enum.any?(x, &(&1 in y)) is_public?(activity) || Enum.any?(x, &(&1 in y))
@ -82,6 +83,26 @@ def entire_thread_visible_for_user?(%Activity{} = activity, %User{} = user) do
result result
end end
def restrict_unauthenticated_access?(%Activity{local: local}) do
restrict_unauthenticated_access_to_activity?(local)
end
def restrict_unauthenticated_access?(%Object{} = object) do
object
|> Object.local?()
|> restrict_unauthenticated_access_to_activity?()
end
def restrict_unauthenticated_access?(%User{} = user) do
User.visible_for(user, _reading_user = nil)
end
defp restrict_unauthenticated_access_to_activity?(local?) when is_boolean(local?) do
cfg_key = if local?, do: :local, else: :remote
Pleroma.Config.restrict_unauthenticated_access?(:activities, cfg_key)
end
def get_visibility(object) do def get_visibility(object) do
to = object.data["to"] || [] to = object.data["to"] || []
cc = object.data["cc"] || [] cc = object.data["cc"] || []

View File

@ -5,7 +5,8 @@
defmodule Pleroma.Web.AdminAPI.AdminAPIController do defmodule Pleroma.Web.AdminAPI.AdminAPIController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper, only: [json_response: 3] import Pleroma.Web.ControllerHelper,
only: [json_response: 3, fetch_integer_param: 3]
alias Pleroma.Config alias Pleroma.Config
alias Pleroma.MFA alias Pleroma.MFA
@ -13,24 +14,19 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
alias Pleroma.Stats alias Pleroma.Stats
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.AdminAPI alias Pleroma.Web.AdminAPI
alias Pleroma.Web.AdminAPI.AccountView alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.AdminAPI.ModerationLogView alias Pleroma.Web.AdminAPI.ModerationLogView
alias Pleroma.Web.AdminAPI.Search
alias Pleroma.Web.Endpoint alias Pleroma.Web.Endpoint
alias Pleroma.Web.Plugs.OAuthScopesPlug alias Pleroma.Web.Plugs.OAuthScopesPlug
alias Pleroma.Web.Router alias Pleroma.Web.Router
require Logger
@users_page_size 50 @users_page_size 50
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["read:accounts"], admin: true} %{scopes: ["read:accounts"], admin: true}
when action in [:list_users, :user_show, :right_get, :show_user_credentials, :create_backup] when action in [:right_get, :show_user_credentials, :create_backup]
) )
plug( plug(
@ -39,12 +35,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
when action in [ when action in [
:get_password_reset, :get_password_reset,
:force_password_reset, :force_password_reset,
:user_delete,
:users_create,
:user_toggle_activation,
:user_activate,
:user_deactivate,
:user_approve,
:tag_users, :tag_users,
:untag_users, :untag_users,
:right_add, :right_add,
@ -56,12 +46,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
] ]
) )
plug(
OAuthScopesPlug,
%{scopes: ["write:follows"], admin: true}
when action in [:user_follow, :user_unfollow]
)
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["read:statuses"], admin: true} %{scopes: ["read:statuses"], admin: true}
@ -97,132 +81,6 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
action_fallback(AdminAPI.FallbackController) action_fallback(AdminAPI.FallbackController)
def user_delete(conn, %{"nickname" => nickname}) do
user_delete(conn, %{"nicknames" => [nickname]})
end
def user_delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users =
nicknames
|> Enum.map(&User.get_cached_by_nickname/1)
users
|> Enum.each(fn user ->
{:ok, delete_data, _} = Builder.delete(admin, user.ap_id)
Pipeline.common_pipeline(delete_data, local: true)
end)
ModerationLog.insert_log(%{
actor: admin,
subject: users,
action: "delete"
})
json(conn, nicknames)
end
def user_follow(%{assigns: %{user: admin}} = conn, %{
"follower" => follower_nick,
"followed" => followed_nick
}) do
with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
%User{} = followed <- User.get_cached_by_nickname(followed_nick) do
User.follow(follower, followed)
ModerationLog.insert_log(%{
actor: admin,
followed: followed,
follower: follower,
action: "follow"
})
end
json(conn, "ok")
end
def user_unfollow(%{assigns: %{user: admin}} = conn, %{
"follower" => follower_nick,
"followed" => followed_nick
}) do
with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
%User{} = followed <- User.get_cached_by_nickname(followed_nick) do
User.unfollow(follower, followed)
ModerationLog.insert_log(%{
actor: admin,
followed: followed,
follower: follower,
action: "unfollow"
})
end
json(conn, "ok")
end
def users_create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
changesets =
Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
user_data = %{
nickname: nickname,
name: nickname,
email: email,
password: password,
password_confirmation: password,
bio: "."
}
User.register_changeset(%User{}, user_data, need_confirmation: false)
end)
|> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
end)
case Pleroma.Repo.transaction(changesets) do
{:ok, users} ->
res =
users
|> Map.values()
|> Enum.map(fn user ->
{:ok, user} = User.post_register_action(user)
user
end)
|> Enum.map(&AccountView.render("created.json", %{user: &1}))
ModerationLog.insert_log(%{
actor: admin,
subjects: Map.values(users),
action: "create"
})
json(conn, res)
{:error, id, changeset, _} ->
res =
Enum.map(changesets.operations, fn
{current_id, {:changeset, _current_changeset, _}} when current_id == id ->
AccountView.render("create-error.json", %{changeset: changeset})
{_, {:changeset, current_changeset, _}} ->
AccountView.render("create-error.json", %{changeset: current_changeset})
end)
conn
|> put_status(:conflict)
|> json(res)
end
end
def user_show(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
conn
|> put_view(AccountView)
|> render("show.json", %{user: user})
else
_ -> {:error, :not_found}
end
end
def list_instance_statuses(conn, %{"instance" => instance} = params) do def list_instance_statuses(conn, %{"instance" => instance} = params) do
with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true with_reblogs = params["with_reblogs"] == "true" || params["with_reblogs"] == true
{page, page_size} = page_params(params) {page, page_size} = page_params(params)
@ -276,69 +134,6 @@ def list_user_chats(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}
end end
end end
def user_toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
user = User.get_cached_by_nickname(nickname)
{:ok, updated_user} = User.deactivate(user, !user.deactivated)
action = if user.deactivated, do: "activate", else: "deactivate"
ModerationLog.insert_log(%{
actor: admin,
subject: [user],
action: action
})
conn
|> put_view(AccountView)
|> render("show.json", %{user: updated_user})
end
def user_activate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
{:ok, updated_users} = User.deactivate(users, false)
ModerationLog.insert_log(%{
actor: admin,
subject: users,
action: "activate"
})
conn
|> put_view(AccountView)
|> render("index.json", %{users: Keyword.values(updated_users)})
end
def user_deactivate(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
{:ok, updated_users} = User.deactivate(users, true)
ModerationLog.insert_log(%{
actor: admin,
subject: users,
action: "deactivate"
})
conn
|> put_view(AccountView)
|> render("index.json", %{users: Keyword.values(updated_users)})
end
def user_approve(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
{:ok, updated_users} = User.approve(users)
ModerationLog.insert_log(%{
actor: admin,
subject: users,
action: "approve"
})
conn
|> put_view(AccountView)
|> render("index.json", %{users: 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(%{
@ -365,43 +160,6 @@ def untag_users(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames, "
end end
end end
def list_users(conn, params) do
{page, page_size} = page_params(params)
filters = maybe_parse_filters(params["filters"])
search_params = %{
query: params["query"],
page: page,
page_size: page_size,
tags: params["tags"],
name: params["name"],
email: params["email"]
}
with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)) do
json(
conn,
AccountView.render("index.json",
users: users,
count: count,
page_size: page_size
)
)
end
end
@filters ~w(local external active deactivated need_approval is_admin is_moderator)
@spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
defp maybe_parse_filters(filters) do
filters
|> String.split(",")
|> Enum.filter(&Enum.member?(@filters, &1))
|> Map.new(&{String.to_existing_atom(&1), true})
end
def right_add_multiple(%{assigns: %{user: admin}} = conn, %{ def right_add_multiple(%{assigns: %{user: admin}} = conn, %{
"permission_group" => permission_group, "permission_group" => permission_group,
"nicknames" => nicknames "nicknames" => nicknames
@ -693,24 +451,9 @@ def create_backup(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname})
end end
defp page_params(params) do defp page_params(params) do
{get_page(params["page"]), get_page_size(params["page_size"])} {
end fetch_integer_param(params, "page", 1),
fetch_integer_param(params, "page_size", @users_page_size)
defp get_page(page_string) when is_nil(page_string), do: 1 }
defp get_page(page_string) do
case Integer.parse(page_string) do
{page, _} -> page
:error -> 1
end
end
defp get_page_size(page_size_string) when is_nil(page_size_string), do: @users_page_size
defp get_page_size(page_size_string) do
case Integer.parse(page_size_string) do
{page_size, _} -> page_size
:error -> @users_page_size
end
end end
end end

View File

@ -0,0 +1,281 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.UserController do
use Pleroma.Web, :controller
import Pleroma.Web.ControllerHelper,
only: [fetch_integer_param: 3]
alias Pleroma.ModerationLog
alias Pleroma.User
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.AdminAPI
alias Pleroma.Web.AdminAPI.AccountView
alias Pleroma.Web.AdminAPI.Search
alias Pleroma.Web.Plugs.OAuthScopesPlug
@users_page_size 50
plug(
OAuthScopesPlug,
%{scopes: ["read:accounts"], admin: true}
when action in [:list, :show]
)
plug(
OAuthScopesPlug,
%{scopes: ["write:accounts"], admin: true}
when action in [
:delete,
:create,
:toggle_activation,
:activate,
:deactivate,
:approve
]
)
plug(
OAuthScopesPlug,
%{scopes: ["write:follows"], admin: true}
when action in [:follow, :unfollow]
)
action_fallback(AdminAPI.FallbackController)
def delete(conn, %{"nickname" => nickname}) do
delete(conn, %{"nicknames" => [nickname]})
end
def delete(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
Enum.each(users, fn user ->
{:ok, delete_data, _} = Builder.delete(admin, user.ap_id)
Pipeline.common_pipeline(delete_data, local: true)
end)
ModerationLog.insert_log(%{
actor: admin,
subject: users,
action: "delete"
})
json(conn, nicknames)
end
def follow(%{assigns: %{user: admin}} = conn, %{
"follower" => follower_nick,
"followed" => followed_nick
}) do
with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
%User{} = followed <- User.get_cached_by_nickname(followed_nick) do
User.follow(follower, followed)
ModerationLog.insert_log(%{
actor: admin,
followed: followed,
follower: follower,
action: "follow"
})
end
json(conn, "ok")
end
def unfollow(%{assigns: %{user: admin}} = conn, %{
"follower" => follower_nick,
"followed" => followed_nick
}) do
with %User{} = follower <- User.get_cached_by_nickname(follower_nick),
%User{} = followed <- User.get_cached_by_nickname(followed_nick) do
User.unfollow(follower, followed)
ModerationLog.insert_log(%{
actor: admin,
followed: followed,
follower: follower,
action: "unfollow"
})
end
json(conn, "ok")
end
def create(%{assigns: %{user: admin}} = conn, %{"users" => users}) do
changesets =
Enum.map(users, fn %{"nickname" => nickname, "email" => email, "password" => password} ->
user_data = %{
nickname: nickname,
name: nickname,
email: email,
password: password,
password_confirmation: password,
bio: "."
}
User.register_changeset(%User{}, user_data, need_confirmation: false)
end)
|> Enum.reduce(Ecto.Multi.new(), fn changeset, multi ->
Ecto.Multi.insert(multi, Ecto.UUID.generate(), changeset)
end)
case Pleroma.Repo.transaction(changesets) do
{:ok, users} ->
res =
users
|> Map.values()
|> Enum.map(fn user ->
{:ok, user} = User.post_register_action(user)
user
end)
|> Enum.map(&AccountView.render("created.json", %{user: &1}))
ModerationLog.insert_log(%{
actor: admin,
subjects: Map.values(users),
action: "create"
})
json(conn, res)
{:error, id, changeset, _} ->
res =
Enum.map(changesets.operations, fn
{current_id, {:changeset, _current_changeset, _}} when current_id == id ->
AccountView.render("create-error.json", %{changeset: changeset})
{_, {:changeset, current_changeset, _}} ->
AccountView.render("create-error.json", %{changeset: current_changeset})
end)
conn
|> put_status(:conflict)
|> json(res)
end
end
def show(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname, for: admin) do
conn
|> put_view(AccountView)
|> render("show.json", %{user: user})
else
_ -> {:error, :not_found}
end
end
def toggle_activation(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
user = User.get_cached_by_nickname(nickname)
{:ok, updated_user} = User.deactivate(user, !user.deactivated)
action = if user.deactivated, do: "activate", else: "deactivate"
ModerationLog.insert_log(%{
actor: admin,
subject: [user],
action: action
})
conn
|> put_view(AccountView)
|> render("show.json", %{user: updated_user})
end
def 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 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 approve(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users = Enum.map(nicknames, &User.get_cached_by_nickname/1)
{:ok, updated_users} = User.approve(users)
ModerationLog.insert_log(%{
actor: admin,
subject: users,
action: "approve"
})
conn
|> put_view(AccountView)
|> render("index.json", %{users: updated_users})
end
def list(conn, params) do
{page, page_size} = page_params(params)
filters = maybe_parse_filters(params["filters"])
search_params =
%{
query: params["query"],
page: page,
page_size: page_size,
tags: params["tags"],
name: params["name"],
email: params["email"],
actor_types: params["actor_types"]
}
|> Map.merge(filters)
with {:ok, users, count} <- Search.user(search_params) do
json(
conn,
AccountView.render("index.json",
users: users,
count: count,
page_size: page_size
)
)
end
end
@filters ~w(local external active deactivated need_approval unconfirmed is_admin is_moderator)
@spec maybe_parse_filters(String.t()) :: %{required(String.t()) => true} | %{}
defp maybe_parse_filters(filters) when is_nil(filters) or filters == "", do: %{}
defp maybe_parse_filters(filters) do
filters
|> String.split(",")
|> Enum.filter(&Enum.member?(@filters, &1))
|> Map.new(&{String.to_existing_atom(&1), true})
end
defp page_params(params) do
{
fetch_integer_param(params, "page", 1),
fetch_integer_param(params, "page_size", @users_page_size)
}
end
end

View File

@ -52,7 +52,7 @@ def render("credentials.json", %{user: user, for: for_user}) do
:skip_thread_containment, :skip_thread_containment,
:pleroma_settings_store, :pleroma_settings_store,
:raw_fields, :raw_fields,
:discoverable, :is_discoverable,
:actor_type :actor_type
]) ])
|> Map.merge(%{ |> Map.merge(%{

View File

@ -335,6 +335,7 @@ def mutes_operation do
operationId: "AccountController.mutes", operationId: "AccountController.mutes",
description: "Accounts the user has muted.", description: "Accounts the user has muted.",
security: [%{"oAuth" => ["follow", "read:mutes"]}], security: [%{"oAuth" => ["follow", "read:mutes"]}],
parameters: pagination_params(),
responses: %{ responses: %{
200 => Operation.response("Accounts", "application/json", array_of_accounts()) 200 => Operation.response("Accounts", "application/json", array_of_accounts())
} }
@ -348,6 +349,7 @@ def blocks_operation do
operationId: "AccountController.blocks", operationId: "AccountController.blocks",
description: "View your blocks. See also accounts/:id/{block,unblock}", description: "View your blocks. See also accounts/:id/{block,unblock}",
security: [%{"oAuth" => ["read:blocks"]}], security: [%{"oAuth" => ["read:blocks"]}],
parameters: pagination_params(),
responses: %{ responses: %{
200 => Operation.response("Accounts", "application/json", array_of_accounts()) 200 => Operation.response("Accounts", "application/json", array_of_accounts())
} }

View File

@ -10,14 +10,14 @@ defmodule Pleroma.Web.Feed.TagController do
alias Pleroma.Web.Feed.FeedView alias Pleroma.Web.Feed.FeedView
def feed(conn, params) do def feed(conn, params) do
unless Pleroma.Config.restrict_unauthenticated_access?(:activities, :local) do if Config.get!([:instance, :public]) do
render_feed(conn, params) render_feed(conn, params)
else else
render_error(conn, :not_found, "Not found") render_error(conn, :not_found, "Not found")
end end
end end
def render_feed(conn, %{"tag" => raw_tag} = params) do defp render_feed(conn, %{"tag" => raw_tag} = params) do
{format, tag} = parse_tag(raw_tag) {format, tag} = parse_tag(raw_tag)
activities = activities =
@ -36,12 +36,13 @@ def render_feed(conn, %{"tag" => raw_tag} = params) do
end end
@spec parse_tag(binary() | any()) :: {format :: String.t(), tag :: String.t()} @spec parse_tag(binary() | any()) :: {format :: String.t(), tag :: String.t()}
defp parse_tag(raw_tag) when is_binary(raw_tag) do defp parse_tag(raw_tag) do
case Enum.reverse(String.split(raw_tag, ".")) do case is_binary(raw_tag) && Enum.reverse(String.split(raw_tag, ".")) do
[format | tag] when format in ["atom", "rss"] -> {format, Enum.join(tag, ".")} [format | tag] when format in ["rss", "atom"] ->
_ -> {"rss", raw_tag} {format, Enum.join(tag, ".")}
_ ->
{"atom", raw_tag}
end end
end end
defp parse_tag(raw_tag), do: {"rss", raw_tag}
end end

View File

@ -5,6 +5,7 @@
defmodule Pleroma.Web.Feed.UserController do defmodule Pleroma.Web.Feed.UserController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
alias Pleroma.Config
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.ActivityPubController alias Pleroma.Web.ActivityPub.ActivityPubController
@ -22,12 +23,7 @@ def feed_redirect(%{assigns: %{format: "html"}} = conn, %{"nickname" => nickname
def feed_redirect(%{assigns: %{format: format}} = conn, _params) def feed_redirect(%{assigns: %{format: format}} = conn, _params)
when format in ["json", "activity+json"] do when format in ["json", "activity+json"] do
with %{halted: false} = conn <- ActivityPubController.call(conn, :user)
Pleroma.Web.Plugs.EnsureAuthenticatedPlug.call(conn,
unless_func: &Pleroma.Web.Plugs.FederatingPlug.federating?/1
) do
ActivityPubController.call(conn, :user)
end
end end
def feed_redirect(conn, %{"nickname" => nickname}) do def feed_redirect(conn, %{"nickname" => nickname}) do
@ -36,25 +32,18 @@ def feed_redirect(conn, %{"nickname" => nickname}) do
end end
end end
def feed(conn, params) do def feed(conn, %{"nickname" => nickname} = params) do
unless Pleroma.Config.restrict_unauthenticated_access?(:profiles, :local) do
render_feed(conn, params)
else
errors(conn, {:error, :not_found})
end
end
def render_feed(conn, %{"nickname" => nickname} = params) do
format = get_format(conn) format = get_format(conn)
format = format =
if format in ["rss", "atom"] do if format in ["atom", "rss"] do
format format
else else
"atom" "atom"
end end
with {_, %User{local: true} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)} do with {_, %User{local: true} = user} <- {:fetch_user, User.get_cached_by_nickname(nickname)},
{_, :visible} <- {:visibility, User.visible_for(user, _reading_user = nil)} do
activities = activities =
%{ %{
type: ["Create"], type: ["Create"],
@ -69,7 +58,7 @@ def render_feed(conn, %{"nickname" => nickname} = params) do
|> render("user.#{format}", |> render("user.#{format}",
user: user, user: user,
activities: activities, activities: activities,
feed_config: Pleroma.Config.get([:feed]) feed_config: Config.get([:feed])
) )
end end
end end
@ -81,6 +70,8 @@ def errors(conn, {:error, :not_found}) do
def errors(conn, {:fetch_user, %User{local: false}}), do: errors(conn, {:error, :not_found}) def errors(conn, {:fetch_user, %User{local: false}}), do: errors(conn, {:error, :not_found})
def errors(conn, {:fetch_user, nil}), do: errors(conn, {:error, :not_found}) def errors(conn, {:fetch_user, nil}), do: errors(conn, {:error, :not_found})
def errors(conn, {:visibility, _}), do: errors(conn, {:error, :not_found})
def errors(conn, _) do def errors(conn, _) do
render_error(conn, :internal_server_error, "Something went wrong") render_error(conn, :internal_server_error, "Something went wrong")
end end

View File

@ -442,15 +442,27 @@ def follow_by_uri(%{body_params: %{uri: uri}} = conn, _) do
end end
@doc "GET /api/v1/mutes" @doc "GET /api/v1/mutes"
def mutes(%{assigns: %{user: user}} = conn, _) do def mutes(%{assigns: %{user: user}} = conn, params) do
users = User.muted_users(user, _restrict_deactivated = true) users =
render(conn, "index.json", users: users, for: user, as: :user) user
|> User.muted_users_relation(_restrict_deactivated = true)
|> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
conn
|> add_link_headers(users)
|> render("index.json", users: users, for: user, as: :user)
end end
@doc "GET /api/v1/blocks" @doc "GET /api/v1/blocks"
def blocks(%{assigns: %{user: user}} = conn, _) do def blocks(%{assigns: %{user: user}} = conn, params) do
users = User.blocked_users(user, _restrict_deactivated = true) users =
render(conn, "index.json", users: users, for: user, as: :user) user
|> User.blocked_users_relation(_restrict_deactivated = true)
|> Pleroma.Pagination.fetch_paginated(Map.put(params, :skip_order, true))
conn
|> add_link_headers(users)
|> render("index.json", users: users, for: user, as: :user)
end end
@doc "GET /api/v1/endorsements" @doc "GET /api/v1/endorsements"

View File

@ -388,7 +388,7 @@ defp maybe_put_unread_conversation_count(data, %User{id: user_id} = user, %User{
data data
|> Kernel.put_in( |> Kernel.put_in(
[:pleroma, :unread_conversation_count], [:pleroma, :unread_conversation_count],
user.unread_conversation_count Pleroma.Conversation.Participation.unread_count(user)
) )
end end

View File

@ -16,10 +16,6 @@ defmodule Pleroma.Web.OStatus.OStatusController do
alias Pleroma.Web.Plugs.RateLimiter alias Pleroma.Web.Plugs.RateLimiter
alias Pleroma.Web.Router alias Pleroma.Web.Router
plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug,
unless_func: &Pleroma.Web.Plugs.FederatingPlug.federating?/1
)
plug( plug(
RateLimiter, RateLimiter,
[name: :ap_routes, params: ["uuid"]] when action in [:object, :activity] [name: :ap_routes, params: ["uuid"]] when action in [:object, :activity]
@ -37,14 +33,12 @@ def object(%{assigns: %{format: format}} = conn, _params)
ActivityPubController.call(conn, :object) ActivityPubController.call(conn, :object)
end end
def object(%{assigns: %{format: format}} = conn, _params) do def object(conn, _params) do
with id <- Endpoint.url() <> conn.request_path, with id <- Endpoint.url() <> conn.request_path,
{_, %Activity{} = activity} <- {_, %Activity{} = activity} <-
{:activity, Activity.get_create_by_object_ap_id_with_object(id)}, {:activity, Activity.get_create_by_object_ap_id_with_object(id)},
{_, true} <- {:public?, Visibility.is_public?(activity)} do {_, true} <- {:public?, Visibility.is_public?(activity)} do
case format do redirect(conn, to: "/notice/#{activity.id}")
_ -> redirect(conn, to: "/notice/#{activity.id}")
end
else else
reason when reason in [{:public?, false}, {:activity, nil}] -> reason when reason in [{:public?, false}, {:activity, nil}] ->
{:error, :not_found} {:error, :not_found}
@ -59,13 +53,11 @@ def activity(%{assigns: %{format: format}} = conn, _params)
ActivityPubController.call(conn, :activity) ActivityPubController.call(conn, :activity)
end end
def activity(%{assigns: %{format: format}} = conn, _params) do def activity(conn, _params) do
with id <- Endpoint.url() <> conn.request_path, with id <- Endpoint.url() <> conn.request_path,
{_, %Activity{} = activity} <- {:activity, Activity.normalize(id)}, {_, %Activity{} = activity} <- {:activity, Activity.normalize(id)},
{_, true} <- {:public?, Visibility.is_public?(activity)} do {_, true} <- {:public?, Visibility.is_public?(activity)} do
case format do redirect(conn, to: "/notice/#{activity.id}")
_ -> redirect(conn, to: "/notice/#{activity.id}")
end
else else
reason when reason in [{:public?, false}, {:activity, nil}] -> reason when reason in [{:public?, false}, {:activity, nil}] ->
{:error, :not_found} {:error, :not_found}
@ -119,6 +111,7 @@ def notice(%{assigns: %{format: format}} = conn, %{"id" => id}) do
def notice_player(conn, %{"id" => id}) do def notice_player(conn, %{"id" => id}) do
with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id_with_object(id), with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id_with_object(id),
true <- Visibility.is_public?(activity), true <- Visibility.is_public?(activity),
{_, true} <- {:visible?, Visibility.visible_for_user?(activity, _reading_user = nil)},
%Object{} = object <- Object.normalize(activity), %Object{} = object <- Object.normalize(activity),
%{data: %{"attachment" => [%{"url" => [url | _]} | _]}} <- object, %{data: %{"attachment" => [%{"url" => [url | _]} | _]}} <- object,
true <- String.starts_with?(url["mediaType"], ["audio", "video"]) do true <- String.starts_with?(url["mediaType"], ["audio", "video"]) do

View File

@ -34,22 +34,26 @@ def init(opts) do
end end
def call(conn, opts) do def call(conn, opts) do
frontend_type = Map.get(opts, :frontend_type, :primary) with false <- invalid_path?(conn.path_info),
path = file_path("", frontend_type) frontend_type <- Map.get(opts, :frontend_type, :primary),
path when not is_nil(path) <- file_path("", frontend_type) do
if path do call_static(conn, opts, path)
conn
|> call_static(opts, path)
else else
conn _ ->
conn
end end
end end
defp call_static(conn, opts, from) do defp invalid_path?(list) do
opts = invalid_path?(list, :binary.compile_pattern(["/", "\\", ":", "\0"]))
opts end
|> Map.put(:from, from)
defp invalid_path?([h | _], _match) when h in [".", "..", ""], do: true
defp invalid_path?([h | t], match), do: String.contains?(h, match) or invalid_path?(t)
defp invalid_path?([], _match), do: false
defp call_static(conn, opts, from) do
opts = Map.put(opts, :from, from)
Plug.Static.call(conn, opts) Plug.Static.call(conn, opts)
end end
end end

View File

@ -5,6 +5,26 @@
defmodule Pleroma.Web.Router do defmodule Pleroma.Web.Router do
use Pleroma.Web, :router use Pleroma.Web, :router
pipeline :accepts_html do
plug(:accepts, ["html"])
end
pipeline :accepts_html_xml do
plug(:accepts, ["html", "xml", "rss", "atom"])
end
pipeline :accepts_html_json do
plug(:accepts, ["html", "activity+json", "json"])
end
pipeline :accepts_html_xml_json do
plug(:accepts, ["html", "xml", "rss", "atom", "activity+json", "json"])
end
pipeline :accepts_xml_rss_atom do
plug(:accepts, ["xml", "rss", "atom"])
end
pipeline :browser do pipeline :browser do
plug(:accepts, ["html"]) plug(:accepts, ["html"])
plug(:fetch_session) plug(:fetch_session)
@ -128,19 +148,8 @@ defmodule Pleroma.Web.Router do
scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do scope "/api/pleroma/admin", Pleroma.Web.AdminAPI do
pipe_through(:admin_api) pipe_through(:admin_api)
post("/backups", AdminAPIController, :create_backup)
post("/users/follow", AdminAPIController, :user_follow)
post("/users/unfollow", AdminAPIController, :user_unfollow)
put("/users/disable_mfa", AdminAPIController, :disable_mfa) put("/users/disable_mfa", AdminAPIController, :disable_mfa)
delete("/users", AdminAPIController, :user_delete)
post("/users", AdminAPIController, :users_create)
patch("/users/:nickname/toggle_activation", AdminAPIController, :user_toggle_activation)
patch("/users/activate", AdminAPIController, :user_activate)
patch("/users/deactivate", AdminAPIController, :user_deactivate)
patch("/users/approve", AdminAPIController, :user_approve)
put("/users/tag", AdminAPIController, :tag_users) put("/users/tag", AdminAPIController, :tag_users)
delete("/users/tag", AdminAPIController, :untag_users) delete("/users/tag", AdminAPIController, :untag_users)
@ -163,6 +172,15 @@ defmodule Pleroma.Web.Router do
:right_delete_multiple :right_delete_multiple
) )
post("/users/follow", UserController, :follow)
post("/users/unfollow", UserController, :unfollow)
delete("/users", UserController, :delete)
post("/users", UserController, :create)
patch("/users/:nickname/toggle_activation", UserController, :toggle_activation)
patch("/users/activate", UserController, :activate)
patch("/users/deactivate", UserController, :deactivate)
patch("/users/approve", UserController, :approve)
get("/relay", RelayController, :index) get("/relay", RelayController, :index)
post("/relay", RelayController, :follow) post("/relay", RelayController, :follow)
delete("/relay", RelayController, :unfollow) delete("/relay", RelayController, :unfollow)
@ -177,8 +195,8 @@ defmodule Pleroma.Web.Router do
get("/users/:nickname/credentials", AdminAPIController, :show_user_credentials) get("/users/:nickname/credentials", AdminAPIController, :show_user_credentials)
patch("/users/:nickname/credentials", AdminAPIController, :update_user_credentials) patch("/users/:nickname/credentials", AdminAPIController, :update_user_credentials)
get("/users", AdminAPIController, :list_users) get("/users", UserController, :list)
get("/users/:nickname", AdminAPIController, :user_show) get("/users/:nickname", UserController, :show)
get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses) get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)
get("/users/:nickname/chats", AdminAPIController, :list_user_chats) get("/users/:nickname/chats", AdminAPIController, :list_user_chats)
@ -225,6 +243,8 @@ defmodule Pleroma.Web.Router do
get("/chats/:id", ChatController, :show) get("/chats/:id", ChatController, :show)
get("/chats/:id/messages", ChatController, :messages) get("/chats/:id/messages", ChatController, :messages)
delete("/chats/:id/messages/:message_id", ChatController, :delete_message) delete("/chats/:id/messages/:message_id", ChatController, :delete_message)
post("/backups", AdminAPIController, :create_backup)
end end
scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do scope "/api/pleroma/emoji", Pleroma.Web.PleromaAPI do
@ -571,30 +591,43 @@ defmodule Pleroma.Web.Router do
) )
end end
pipeline :ostatus do
plug(:accepts, ["html", "xml", "rss", "atom", "activity+json", "json"])
plug(Pleroma.Web.Plugs.StaticFEPlug)
end
pipeline :oembed do
plug(:accepts, ["json", "xml"])
end
scope "/", Pleroma.Web do scope "/", Pleroma.Web do
pipe_through([:ostatus, :http_signature]) # Note: html format is supported only if static FE is enabled
# Note: http signature is only considered for json requests (no auth for non-json requests)
pipe_through([:accepts_html_json, :http_signature, Pleroma.Web.Plugs.StaticFEPlug])
get("/objects/:uuid", OStatus.OStatusController, :object) get("/objects/:uuid", OStatus.OStatusController, :object)
get("/activities/:uuid", OStatus.OStatusController, :activity) get("/activities/:uuid", OStatus.OStatusController, :activity)
get("/notice/:id", OStatus.OStatusController, :notice) get("/notice/:id", OStatus.OStatusController, :notice)
get("/notice/:id/embed_player", OStatus.OStatusController, :notice_player)
# Mastodon compatibility routes # Mastodon compatibility routes
get("/users/:nickname/statuses/:id", OStatus.OStatusController, :object) get("/users/:nickname/statuses/:id", OStatus.OStatusController, :object)
get("/users/:nickname/statuses/:id/activity", OStatus.OStatusController, :activity) get("/users/:nickname/statuses/:id/activity", OStatus.OStatusController, :activity)
end
scope "/", Pleroma.Web do
# Note: html format is supported only if static FE is enabled
# Note: http signature is only considered for json requests (no auth for non-json requests)
pipe_through([:accepts_html_xml_json, :http_signature, Pleroma.Web.Plugs.StaticFEPlug])
# Note: returns user _profile_ for json requests, redirects to user _feed_ for non-json ones
get("/users/:nickname", Feed.UserController, :feed_redirect, as: :user_feed)
end
scope "/", Pleroma.Web do
# Note: html format is supported only if static FE is enabled
pipe_through([:accepts_html_xml, Pleroma.Web.Plugs.StaticFEPlug])
get("/users/:nickname/feed", Feed.UserController, :feed, as: :user_feed) get("/users/:nickname/feed", Feed.UserController, :feed, as: :user_feed)
get("/users/:nickname", Feed.UserController, :feed_redirect, as: :user_feed) end
scope "/", Pleroma.Web do
pipe_through(:accepts_html)
get("/notice/:id/embed_player", OStatus.OStatusController, :notice_player)
end
scope "/", Pleroma.Web do
pipe_through(:accepts_xml_rss_atom)
get("/tags/:tag", Feed.TagController, :feed, as: :tag_feed) get("/tags/:tag", Feed.TagController, :feed, as: :tag_feed)
end end

View File

@ -17,12 +17,96 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
plug(:put_view, Pleroma.Web.StaticFE.StaticFEView) plug(:put_view, Pleroma.Web.StaticFE.StaticFEView)
plug(:assign_id) plug(:assign_id)
plug(Pleroma.Web.Plugs.EnsureAuthenticatedPlug,
unless_func: &Pleroma.Web.Plugs.FederatingPlug.federating?/1
)
@page_keys ["max_id", "min_id", "limit", "since_id", "order"] @page_keys ["max_id", "min_id", "limit", "since_id", "order"]
@doc "Renders requested local public activity or public activities of requested user"
def show(%{assigns: %{notice_id: notice_id}} = conn, _params) do
with %Activity{local: true} = activity <-
Activity.get_by_id_with_object(notice_id),
true <- Visibility.is_public?(activity.object),
{_, true} <- {:visible?, Visibility.visible_for_user?(activity, _reading_user = nil)},
%User{} = user <- User.get_by_ap_id(activity.object.data["actor"]) do
meta = Metadata.build_tags(%{activity_id: notice_id, object: activity.object, user: user})
timeline =
activity.object.data["context"]
|> ActivityPub.fetch_activities_for_context(%{})
|> Enum.reverse()
|> Enum.map(&represent(&1, &1.object.id == activity.object.id))
render(conn, "conversation.html", %{activities: timeline, meta: meta})
else
%Activity{object: %Object{data: data}} ->
conn
|> put_status(:found)
|> redirect(external: data["url"] || data["external_url"] || data["id"])
_ ->
not_found(conn, "Post not found.")
end
end
def show(%{assigns: %{username_or_id: username_or_id}} = conn, params) do
with {_, %User{local: true} = user} <-
{:fetch_user, User.get_cached_by_nickname_or_id(username_or_id)},
{_, :visible} <- {:visibility, User.visible_for(user, _reading_user = nil)} do
meta = Metadata.build_tags(%{user: user})
params =
params
|> Map.take(@page_keys)
|> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
timeline =
user
|> ActivityPub.fetch_user_activities(_reading_user = nil, params)
|> Enum.map(&represent/1)
prev_page_id =
(params["min_id"] || params["max_id"]) &&
List.first(timeline) && List.first(timeline).id
next_page_id = List.last(timeline) && List.last(timeline).id
render(conn, "profile.html", %{
user: User.sanitize_html(user),
timeline: timeline,
prev_page_id: prev_page_id,
next_page_id: next_page_id,
meta: meta
})
else
_ ->
not_found(conn, "User not found.")
end
end
def show(%{assigns: %{object_id: _}} = conn, _params) do
url = Helpers.url(conn) <> conn.request_path
case Activity.get_create_by_object_ap_id_with_object(url) do
%Activity{} = activity ->
to = Helpers.o_status_path(Pleroma.Web.Endpoint, :notice, activity)
redirect(conn, to: to)
_ ->
not_found(conn, "Post not found.")
end
end
def show(%{assigns: %{activity_id: _}} = conn, _params) do
url = Helpers.url(conn) <> conn.request_path
case Activity.get_by_ap_id(url) do
%Activity{} = activity ->
to = Helpers.o_status_path(Pleroma.Web.Endpoint, :notice, activity)
redirect(conn, to: to)
_ ->
not_found(conn, "Post not found.")
end
end
defp get_title(%Object{data: %{"name" => name}}) when is_binary(name), defp get_title(%Object{data: %{"name" => name}}) when is_binary(name),
do: name do: name
@ -81,91 +165,6 @@ defp represent(%Activity{object: %Object{data: data}} = activity, selected) do
} }
end end
def show(%{assigns: %{notice_id: notice_id}} = conn, _params) do
with %Activity{local: true} = activity <-
Activity.get_by_id_with_object(notice_id),
true <- Visibility.is_public?(activity.object),
%User{} = user <- User.get_by_ap_id(activity.object.data["actor"]) do
meta = Metadata.build_tags(%{activity_id: notice_id, object: activity.object, user: user})
timeline =
activity.object.data["context"]
|> ActivityPub.fetch_activities_for_context(%{})
|> Enum.reverse()
|> Enum.map(&represent(&1, &1.object.id == activity.object.id))
render(conn, "conversation.html", %{activities: timeline, meta: meta})
else
%Activity{object: %Object{data: data}} ->
conn
|> put_status(:found)
|> redirect(external: data["url"] || data["external_url"] || data["id"])
_ ->
not_found(conn, "Post not found.")
end
end
def show(%{assigns: %{username_or_id: username_or_id}} = conn, params) do
case User.get_cached_by_nickname_or_id(username_or_id) do
%User{} = user ->
meta = Metadata.build_tags(%{user: user})
params =
params
|> Map.take(@page_keys)
|> Map.new(fn {k, v} -> {String.to_existing_atom(k), v} end)
timeline =
user
|> ActivityPub.fetch_user_activities(nil, params)
|> Enum.map(&represent/1)
prev_page_id =
(params["min_id"] || params["max_id"]) &&
List.first(timeline) && List.first(timeline).id
next_page_id = List.last(timeline) && List.last(timeline).id
render(conn, "profile.html", %{
user: User.sanitize_html(user),
timeline: timeline,
prev_page_id: prev_page_id,
next_page_id: next_page_id,
meta: meta
})
_ ->
not_found(conn, "User not found.")
end
end
def show(%{assigns: %{object_id: _}} = conn, _params) do
url = Helpers.url(conn) <> conn.request_path
case Activity.get_create_by_object_ap_id_with_object(url) do
%Activity{} = activity ->
to = Helpers.o_status_path(Pleroma.Web.Endpoint, :notice, activity)
redirect(conn, to: to)
_ ->
not_found(conn, "Post not found.")
end
end
def show(%{assigns: %{activity_id: _}} = conn, _params) do
url = Helpers.url(conn) <> conn.request_path
case Activity.get_by_ap_id(url) do
%Activity{} = activity ->
to = Helpers.o_status_path(Pleroma.Web.Endpoint, :notice, activity)
redirect(conn, to: to)
_ ->
not_found(conn, "Post not found.")
end
end
defp assign_id(%{path_info: ["notice", notice_id]} = conn, _opts), defp assign_id(%{path_info: ["notice", notice_id]} = conn, _opts),
do: assign(conn, :notice_id, notice_id) do: assign(conn, :notice_id, notice_id)

View File

@ -0,0 +1,38 @@
defmodule Pleroma.Repo.Migrations.RemoveUnreadConversationCountFromUser do
use Ecto.Migration
import Ecto.Query
alias Pleroma.Repo
def up do
alter table(:users) do
remove_if_exists(:unread_conversation_count, :integer)
end
end
def down do
alter table(:users) do
add_if_not_exists(:unread_conversation_count, :integer, default: 0)
end
flush()
recalc_unread_conversation_count()
end
defp recalc_unread_conversation_count do
participations_subquery =
from(
p in "conversation_participations",
where: p.read == false,
group_by: p.user_id,
select: %{user_id: p.user_id, unread_conversation_count: count(p.id)}
)
from(
u in "users",
join: p in subquery(participations_subquery),
on: p.user_id == u.id,
update: [set: [unread_conversation_count: p.unread_conversation_count]]
)
|> Repo.update_all([])
end
end

View File

@ -0,0 +1,12 @@
defmodule Pleroma.Repo.Migrations.AddUnreadIndexToConversationParticipation do
use Ecto.Migration
def change do
create(
index(:conversation_participations, [:user_id],
where: "read = false",
name: "unread_conversation_participation_count_index"
)
)
end
end

View File

@ -37,9 +37,8 @@ test "for a new conversation or a reply, it doesn't mark the author's participat
[%{read: true}] = Participation.for_user(user) [%{read: true}] = Participation.for_user(user)
[%{read: false} = participation] = Participation.for_user(other_user) [%{read: false} = participation] = Participation.for_user(other_user)
assert Participation.unread_count(user) == 0
assert User.get_cached_by_id(user.id).unread_conversation_count == 0 assert Participation.unread_count(other_user) == 1
assert User.get_cached_by_id(other_user.id).unread_conversation_count == 1
{:ok, _} = {:ok, _} =
CommonAPI.post(other_user, %{ CommonAPI.post(other_user, %{
@ -54,8 +53,8 @@ test "for a new conversation or a reply, it doesn't mark the author's participat
[%{read: false}] = Participation.for_user(user) [%{read: false}] = Participation.for_user(user)
[%{read: true}] = Participation.for_user(other_user) [%{read: true}] = Participation.for_user(other_user)
assert User.get_cached_by_id(user.id).unread_conversation_count == 1 assert Participation.unread_count(user) == 1
assert User.get_cached_by_id(other_user.id).unread_conversation_count == 0 assert Participation.unread_count(other_user) == 0
end 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
@ -264,7 +263,7 @@ test "when the user blocks a recipient, the existing conversations with them are
assert [%{read: false}, %{read: false}, %{read: false}, %{read: false}] = assert [%{read: false}, %{read: false}, %{read: false}, %{read: false}] =
Participation.for_user(blocker) Participation.for_user(blocker)
assert User.get_cached_by_id(blocker.id).unread_conversation_count == 4 assert Participation.unread_count(blocker) == 4
{:ok, _user_relationship} = User.block(blocker, blocked) {:ok, _user_relationship} = User.block(blocker, blocked)
@ -272,15 +271,15 @@ test "when the user blocks a recipient, the existing conversations with them are
assert [%{read: true}, %{read: true}, %{read: true}, %{read: false}] = assert [%{read: true}, %{read: true}, %{read: true}, %{read: false}] =
Participation.for_user(blocker) Participation.for_user(blocker)
assert User.get_cached_by_id(blocker.id).unread_conversation_count == 1 assert Participation.unread_count(blocker) == 1
# The conversation is not marked as read for the blocked user # The conversation is not marked as read for the blocked user
assert [_, _, %{read: false}] = Participation.for_user(blocked) assert [_, _, %{read: false}] = Participation.for_user(blocked)
assert User.get_cached_by_id(blocked.id).unread_conversation_count == 1 assert Participation.unread_count(blocker) == 1
# The conversation is not marked as read for the third user # The conversation is not marked as read for the third user
assert [%{read: false}, _, _] = Participation.for_user(third_user) assert [%{read: false}, _, _] = Participation.for_user(third_user)
assert User.get_cached_by_id(third_user.id).unread_conversation_count == 1 assert Participation.unread_count(third_user) == 1
end end
test "the new conversation with the blocked user is not marked as unread " do test "the new conversation with the blocked user is not marked as unread " do
@ -298,7 +297,7 @@ test "the new conversation with the blocked user is not marked as unread " do
}) })
assert [%{read: true}] = Participation.for_user(blocker) assert [%{read: true}] = Participation.for_user(blocker)
assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0 assert Participation.unread_count(blocker) == 0
# When the blocked user is a recipient # When the blocked user is a recipient
{:ok, _direct2} = {:ok, _direct2} =
@ -308,10 +307,10 @@ test "the new conversation with the blocked user is not marked as unread " do
}) })
assert [%{read: true}, %{read: true}] = Participation.for_user(blocker) assert [%{read: true}, %{read: true}] = Participation.for_user(blocker)
assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0 assert Participation.unread_count(blocker) == 0
assert [%{read: false}, _] = Participation.for_user(blocked) assert [%{read: false}, _] = Participation.for_user(blocked)
assert User.get_cached_by_id(blocked.id).unread_conversation_count == 1 assert Participation.unread_count(blocked) == 1
end end
test "the conversation with the blocked user is not marked as unread on a reply" do test "the conversation with the blocked user is not marked as unread on a reply" do
@ -327,8 +326,8 @@ test "the conversation with the blocked user is not marked as unread on a reply"
{:ok, _user_relationship} = User.block(blocker, blocked) {:ok, _user_relationship} = User.block(blocker, blocked)
assert [%{read: true}] = Participation.for_user(blocker) assert [%{read: true}] = Participation.for_user(blocker)
assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0
assert Participation.unread_count(blocker) == 0
assert [blocked_participation] = Participation.for_user(blocked) assert [blocked_participation] = Participation.for_user(blocked)
# When it's a reply from the blocked user # When it's a reply from the blocked user
@ -340,8 +339,8 @@ test "the conversation with the blocked user is not marked as unread on a reply"
}) })
assert [%{read: true}] = Participation.for_user(blocker) assert [%{read: true}] = Participation.for_user(blocker)
assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0
assert Participation.unread_count(blocker) == 0
assert [third_user_participation] = Participation.for_user(third_user) assert [third_user_participation] = Participation.for_user(third_user)
# When it's a reply from the third user # When it's a reply from the third user
@ -353,11 +352,12 @@ test "the conversation with the blocked user is not marked as unread on a reply"
}) })
assert [%{read: true}] = Participation.for_user(blocker) assert [%{read: true}] = Participation.for_user(blocker)
assert User.get_cached_by_id(blocker.id).unread_conversation_count == 0 assert Participation.unread_count(blocker) == 0
# Marked as unread for the blocked user # Marked as unread for the blocked user
assert [%{read: false}] = Participation.for_user(blocked) assert [%{read: false}] = Participation.for_user(blocked)
assert User.get_cached_by_id(blocked.id).unread_conversation_count == 1
assert Participation.unread_count(blocked) == 1
end end
end end
end end

View File

@ -156,21 +156,6 @@ test "it returns error when user is not found", %{conn: conn} do
assert response == "Not found" assert response == "Not found"
end end
test "it requires authentication if instance is NOT federating", %{
conn: conn
} do
user = insert(:user)
conn =
put_req_header(
conn,
"accept",
"application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\""
)
ensure_federating_or_authenticated(conn, "/users/#{user.nickname}.json", user)
end
end end
describe "mastodon compatibility routes" do describe "mastodon compatibility routes" do
@ -338,18 +323,6 @@ test "cached purged after object deletion", %{conn: conn} do
assert "Not found" == json_response(conn2, :not_found) assert "Not found" == json_response(conn2, :not_found)
end end
test "it requires authentication if instance is NOT federating", %{
conn: conn
} do
user = insert(:user)
note = insert(:note)
uuid = String.split(note.data["id"], "/") |> List.last()
conn = put_req_header(conn, "accept", "application/activity+json")
ensure_federating_or_authenticated(conn, "/objects/#{uuid}", user)
end
end end
describe "/activities/:uuid" do describe "/activities/:uuid" do
@ -421,18 +394,6 @@ test "cached purged after activity deletion", %{conn: conn} do
assert "Not found" == json_response(conn2, :not_found) assert "Not found" == json_response(conn2, :not_found)
end end
test "it requires authentication if instance is NOT federating", %{
conn: conn
} do
user = insert(:user)
activity = insert(:note_activity)
uuid = String.split(activity.data["id"], "/") |> List.last()
conn = put_req_header(conn, "accept", "application/activity+json")
ensure_federating_or_authenticated(conn, "/activities/#{uuid}", user)
end
end end
describe "/inbox" do describe "/inbox" do
@ -893,15 +854,6 @@ test "it returns an announce activity in a collection", %{conn: conn} do
assert response(conn, 200) =~ announce_activity.data["object"] assert response(conn, 200) =~ announce_activity.data["object"]
end end
test "it requires authentication if instance is NOT federating", %{
conn: conn
} do
user = insert(:user)
conn = put_req_header(conn, "accept", "application/activity+json")
ensure_federating_or_authenticated(conn, "/users/#{user.nickname}/outbox", user)
end
end end
describe "POST /users/:nickname/outbox (C2S)" do describe "POST /users/:nickname/outbox (C2S)" do

View File

@ -0,0 +1,971 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.AdminAPI.UserControllerTest do
use Pleroma.Web.ConnCase
use Oban.Testing, repo: Pleroma.Repo
import Mock
import Pleroma.Factory
alias Pleroma.Config
alias Pleroma.HTML
alias Pleroma.ModerationLog
alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web
alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MediaProxy
setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end
setup do
admin = insert(:user, is_admin: true)
token = insert(:oauth_admin_token, user: admin)
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, token)
{:ok, %{admin: admin, token: token, conn: conn}}
end
test "with valid `admin_token` query parameter, skips OAuth scopes check" do
clear_config([:admin_token], "password123")
user = insert(:user)
conn = get(build_conn(), "/api/pleroma/admin/users/#{user.nickname}?admin_token=password123")
assert json_response(conn, 200)
end
describe "with [:auth, :enforce_oauth_admin_scope_usage]," do
setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], true)
test "GET /api/pleroma/admin/users/:nickname requires admin:read:accounts or broader scope",
%{admin: admin} do
user = insert(:user)
url = "/api/pleroma/admin/users/#{user.nickname}"
good_token1 = insert(:oauth_token, user: admin, scopes: ["admin"])
good_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read"])
good_token3 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts"])
bad_token1 = insert(:oauth_token, user: admin, scopes: ["read:accounts"])
bad_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts:partial"])
bad_token3 = nil
for good_token <- [good_token1, good_token2, good_token3] do
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, good_token)
|> get(url)
assert json_response(conn, 200)
end
for good_token <- [good_token1, good_token2, good_token3] do
conn =
build_conn()
|> assign(:user, nil)
|> assign(:token, good_token)
|> get(url)
assert json_response(conn, :forbidden)
end
for bad_token <- [bad_token1, bad_token2, bad_token3] do
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, bad_token)
|> get(url)
assert json_response(conn, :forbidden)
end
end
end
describe "unless [:auth, :enforce_oauth_admin_scope_usage]," do
setup do: clear_config([:auth, :enforce_oauth_admin_scope_usage], false)
test "GET /api/pleroma/admin/users/:nickname requires " <>
"read:accounts or admin:read:accounts or broader scope",
%{admin: admin} do
user = insert(:user)
url = "/api/pleroma/admin/users/#{user.nickname}"
good_token1 = insert(:oauth_token, user: admin, scopes: ["admin"])
good_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read"])
good_token3 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts"])
good_token4 = insert(:oauth_token, user: admin, scopes: ["read:accounts"])
good_token5 = insert(:oauth_token, user: admin, scopes: ["read"])
good_tokens = [good_token1, good_token2, good_token3, good_token4, good_token5]
bad_token1 = insert(:oauth_token, user: admin, scopes: ["read:accounts:partial"])
bad_token2 = insert(:oauth_token, user: admin, scopes: ["admin:read:accounts:partial"])
bad_token3 = nil
for good_token <- good_tokens do
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, good_token)
|> get(url)
assert json_response(conn, 200)
end
for good_token <- good_tokens do
conn =
build_conn()
|> assign(:user, nil)
|> assign(:token, good_token)
|> get(url)
assert json_response(conn, :forbidden)
end
for bad_token <- [bad_token1, bad_token2, bad_token3] do
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, bad_token)
|> get(url)
assert json_response(conn, :forbidden)
end
end
end
describe "DELETE /api/pleroma/admin/users" do
test "single user", %{admin: admin, conn: conn} do
clear_config([:instance, :federating], true)
user =
insert(:user,
avatar: %{"url" => [%{"href" => "https://someurl"}]},
banner: %{"url" => [%{"href" => "https://somebanner"}]},
bio: "Hello world!",
name: "A guy"
)
# Create some activities to check they got deleted later
follower = insert(:user)
{:ok, _} = CommonAPI.post(user, %{status: "test"})
{:ok, _, _, _} = CommonAPI.follow(user, follower)
{:ok, _, _, _} = CommonAPI.follow(follower, user)
user = Repo.get(User, user.id)
assert user.note_count == 1
assert user.follower_count == 1
assert user.following_count == 1
refute user.deactivated
with_mock Pleroma.Web.Federator,
publish: fn _ -> nil end,
perform: fn _, _ -> nil end do
conn =
conn
|> put_req_header("accept", "application/json")
|> delete("/api/pleroma/admin/users?nickname=#{user.nickname}")
ObanHelpers.perform_all()
assert User.get_by_nickname(user.nickname).deactivated
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} deleted users: @#{user.nickname}"
assert json_response(conn, 200) == [user.nickname]
user = Repo.get(User, user.id)
assert user.deactivated
assert user.avatar == %{}
assert user.banner == %{}
assert user.note_count == 0
assert user.follower_count == 0
assert user.following_count == 0
assert user.bio == ""
assert user.name == nil
assert called(Pleroma.Web.Federator.publish(:_))
end
end
test "multiple users", %{admin: admin, conn: conn} do
user_one = insert(:user)
user_two = insert(:user)
conn =
conn
|> 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", %{conn: conn} do
conn =
conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users", %{
"users" => [
%{
"nickname" => "lain",
"email" => "lain@example.org",
"password" => "test"
},
%{
"nickname" => "lain2",
"email" => "lain2@example.org",
"password" => "test"
}
]
})
response = json_response(conn, 200) |> Enum.map(&Map.get(&1, "type"))
assert response == ["success", "success"]
log_entry = Repo.one(ModerationLog)
assert ["lain", "lain2"] -- Enum.map(log_entry.data["subjects"], & &1["nickname"]) == []
end
test "Cannot create user with existing email", %{conn: conn} do
user = insert(:user)
conn =
conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users", %{
"users" => [
%{
"nickname" => "lain",
"email" => user.email,
"password" => "test"
}
]
})
assert json_response(conn, 409) == [
%{
"code" => 409,
"data" => %{
"email" => user.email,
"nickname" => "lain"
},
"error" => "email has already been taken",
"type" => "error"
}
]
end
test "Cannot create user with existing nickname", %{conn: conn} do
user = insert(:user)
conn =
conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users", %{
"users" => [
%{
"nickname" => user.nickname,
"email" => "someuser@plerama.social",
"password" => "test"
}
]
})
assert json_response(conn, 409) == [
%{
"code" => 409,
"data" => %{
"email" => "someuser@plerama.social",
"nickname" => user.nickname
},
"error" => "nickname has already been taken",
"type" => "error"
}
]
end
test "Multiple user creation works in transaction", %{conn: conn} do
user = insert(:user)
conn =
conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users", %{
"users" => [
%{
"nickname" => "newuser",
"email" => "newuser@pleroma.social",
"password" => "test"
},
%{
"nickname" => "lain",
"email" => user.email,
"password" => "test"
}
]
})
assert json_response(conn, 409) == [
%{
"code" => 409,
"data" => %{
"email" => user.email,
"nickname" => "lain"
},
"error" => "email has already been taken",
"type" => "error"
},
%{
"code" => 409,
"data" => %{
"email" => "newuser@pleroma.social",
"nickname" => "newuser"
},
"error" => "",
"type" => "error"
}
]
assert User.get_by_nickname("newuser") === nil
end
end
describe "/api/pleroma/admin/users/:nickname" do
test "Show", %{conn: conn} do
user = insert(:user)
conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}")
assert user_response(user) == json_response(conn, 200)
end
test "when the user doesn't exist", %{conn: conn} do
user = build(:user)
conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}")
assert %{"error" => "Not found"} == json_response(conn, 404)
end
end
describe "/api/pleroma/admin/users/follow" do
test "allows to force-follow another user", %{admin: admin, conn: conn} do
user = insert(:user)
follower = insert(:user)
conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users/follow", %{
"follower" => follower.nickname,
"followed" => user.nickname
})
user = User.get_cached_by_id(user.id)
follower = User.get_cached_by_id(follower.id)
assert User.following?(follower, user)
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} made @#{follower.nickname} follow @#{user.nickname}"
end
end
describe "/api/pleroma/admin/users/unfollow" do
test "allows to force-unfollow another user", %{admin: admin, conn: conn} do
user = insert(:user)
follower = insert(:user)
User.follow(follower, user)
conn
|> put_req_header("accept", "application/json")
|> post("/api/pleroma/admin/users/unfollow", %{
"follower" => follower.nickname,
"followed" => user.nickname
})
user = User.get_cached_by_id(user.id)
follower = User.get_cached_by_id(follower.id)
refute User.following?(follower, user)
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} made @#{follower.nickname} unfollow @#{user.nickname}"
end
end
describe "GET /api/pleroma/admin/users" do
test "renders users array for the first page", %{conn: conn, admin: admin} do
user = insert(:user, local: false, tags: ["foo", "bar"])
user2 = insert(:user, approval_pending: true, registration_reason: "I'm a chill dude")
conn = get(conn, "/api/pleroma/admin/users?page=1")
users =
[
user_response(
admin,
%{"roles" => %{"admin" => true, "moderator" => false}}
),
user_response(user, %{"local" => false, "tags" => ["foo", "bar"]}),
user_response(
user2,
%{
"local" => true,
"approval_pending" => true,
"registration_reason" => "I'm a chill dude",
"actor_type" => "Person"
}
)
]
|> Enum.sort_by(& &1["nickname"])
assert json_response(conn, 200) == %{
"count" => 3,
"page_size" => 50,
"users" => users
}
end
test "pagination works correctly with service users", %{conn: conn} do
service1 = User.get_or_create_service_actor_by_ap_id(Web.base_url() <> "/meido", "meido")
insert_list(25, :user)
assert %{"count" => 26, "page_size" => 10, "users" => users1} =
conn
|> get("/api/pleroma/admin/users?page=1&filters=", %{page_size: "10"})
|> json_response(200)
assert Enum.count(users1) == 10
assert service1 not in users1
assert %{"count" => 26, "page_size" => 10, "users" => users2} =
conn
|> get("/api/pleroma/admin/users?page=2&filters=", %{page_size: "10"})
|> json_response(200)
assert Enum.count(users2) == 10
assert service1 not in users2
assert %{"count" => 26, "page_size" => 10, "users" => users3} =
conn
|> get("/api/pleroma/admin/users?page=3&filters=", %{page_size: "10"})
|> json_response(200)
assert Enum.count(users3) == 6
assert service1 not in users3
end
test "renders empty array for the second page", %{conn: conn} do
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?page=2")
assert json_response(conn, 200) == %{
"count" => 2,
"page_size" => 50,
"users" => []
}
end
test "regular search", %{conn: conn} do
user = insert(:user, nickname: "bob")
conn = get(conn, "/api/pleroma/admin/users?query=bo")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [user_response(user, %{"local" => true})]
}
end
test "search by domain", %{conn: conn} do
user = insert(:user, nickname: "nickname@domain.com")
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?query=domain.com")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [user_response(user)]
}
end
test "search by full nickname", %{conn: conn} do
user = insert(:user, nickname: "nickname@domain.com")
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?query=nickname@domain.com")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [user_response(user)]
}
end
test "search by display name", %{conn: conn} do
user = insert(:user, name: "Display name")
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?name=display")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [user_response(user)]
}
end
test "search by email", %{conn: conn} do
user = insert(:user, email: "email@example.com")
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?email=email@example.com")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [user_response(user)]
}
end
test "regular search with page size", %{conn: conn} do
user = insert(:user, nickname: "aalice")
user2 = insert(:user, nickname: "alice")
conn1 = get(conn, "/api/pleroma/admin/users?query=a&page_size=1&page=1")
assert json_response(conn1, 200) == %{
"count" => 2,
"page_size" => 1,
"users" => [user_response(user)]
}
conn2 = get(conn, "/api/pleroma/admin/users?query=a&page_size=1&page=2")
assert json_response(conn2, 200) == %{
"count" => 2,
"page_size" => 1,
"users" => [user_response(user2)]
}
end
test "only local users" do
admin = insert(:user, is_admin: true, nickname: "john")
token = insert(:oauth_admin_token, user: admin)
user = insert(:user, nickname: "bob")
insert(:user, nickname: "bobb", local: false)
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, token)
|> get("/api/pleroma/admin/users?query=bo&filters=local")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [user_response(user)]
}
end
test "only local users with no query", %{conn: conn, admin: old_admin} do
admin = insert(:user, is_admin: true, nickname: "john")
user = insert(:user, nickname: "bob")
insert(:user, nickname: "bobb", local: false)
conn = get(conn, "/api/pleroma/admin/users?filters=local")
users =
[
user_response(user),
user_response(admin, %{
"roles" => %{"admin" => true, "moderator" => false}
}),
user_response(old_admin, %{
"deactivated" => false,
"roles" => %{"admin" => true, "moderator" => false}
})
]
|> Enum.sort_by(& &1["nickname"])
assert json_response(conn, 200) == %{
"count" => 3,
"page_size" => 50,
"users" => users
}
end
test "only unconfirmed users", %{conn: conn} do
sad_user = insert(:user, nickname: "sadboy", confirmation_pending: true)
old_user = insert(:user, nickname: "oldboy", confirmation_pending: true)
insert(:user, nickname: "happyboy", approval_pending: false)
insert(:user, confirmation_pending: false)
result =
conn
|> get("/api/pleroma/admin/users?filters=unconfirmed")
|> json_response(200)
users =
Enum.map([old_user, sad_user], fn user ->
user_response(user, %{
"confirmation_pending" => true,
"approval_pending" => false
})
end)
|> Enum.sort_by(& &1["nickname"])
assert result == %{"count" => 2, "page_size" => 50, "users" => users}
end
test "only unapproved users", %{conn: conn} do
user =
insert(:user,
nickname: "sadboy",
approval_pending: true,
registration_reason: "Plz let me in!"
)
insert(:user, nickname: "happyboy", approval_pending: false)
conn = get(conn, "/api/pleroma/admin/users?filters=need_approval")
users = [
user_response(
user,
%{"approval_pending" => true, "registration_reason" => "Plz let me in!"}
)
]
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => users
}
end
test "load only admins", %{conn: conn, admin: admin} do
second_admin = insert(:user, is_admin: true)
insert(:user)
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?filters=is_admin")
users =
[
user_response(admin, %{
"deactivated" => false,
"roles" => %{"admin" => true, "moderator" => false}
}),
user_response(second_admin, %{
"deactivated" => false,
"roles" => %{"admin" => true, "moderator" => false}
})
]
|> Enum.sort_by(& &1["nickname"])
assert json_response(conn, 200) == %{
"count" => 2,
"page_size" => 50,
"users" => users
}
end
test "load only moderators", %{conn: conn} do
moderator = insert(:user, is_moderator: true)
insert(:user)
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?filters=is_moderator")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
user_response(moderator, %{
"deactivated" => false,
"roles" => %{"admin" => false, "moderator" => true}
})
]
}
end
test "load users with actor_type is Person", %{admin: admin, conn: conn} do
insert(:user, actor_type: "Service")
insert(:user, actor_type: "Application")
user1 = insert(:user)
user2 = insert(:user)
response =
conn
|> get(user_path(conn, :list), %{actor_types: ["Person"]})
|> json_response(200)
users =
[
user_response(admin, %{"roles" => %{"admin" => true, "moderator" => false}}),
user_response(user1),
user_response(user2)
]
|> Enum.sort_by(& &1["nickname"])
assert response == %{"count" => 3, "page_size" => 50, "users" => users}
end
test "load users with actor_type is Person and Service", %{admin: admin, conn: conn} do
user_service = insert(:user, actor_type: "Service")
insert(:user, actor_type: "Application")
user1 = insert(:user)
user2 = insert(:user)
response =
conn
|> get(user_path(conn, :list), %{actor_types: ["Person", "Service"]})
|> json_response(200)
users =
[
user_response(admin, %{"roles" => %{"admin" => true, "moderator" => false}}),
user_response(user1),
user_response(user2),
user_response(user_service, %{"actor_type" => "Service"})
]
|> Enum.sort_by(& &1["nickname"])
assert response == %{"count" => 4, "page_size" => 50, "users" => users}
end
test "load users with actor_type is Service", %{conn: conn} do
user_service = insert(:user, actor_type: "Service")
insert(:user, actor_type: "Application")
insert(:user)
insert(:user)
response =
conn
|> get(user_path(conn, :list), %{actor_types: ["Service"]})
|> json_response(200)
users = [user_response(user_service, %{"actor_type" => "Service"})]
assert response == %{"count" => 1, "page_size" => 50, "users" => users}
end
test "load users with tags list", %{conn: conn} do
user1 = insert(:user, tags: ["first"])
user2 = insert(:user, tags: ["second"])
insert(:user)
insert(:user)
conn = get(conn, "/api/pleroma/admin/users?tags[]=first&tags[]=second")
users =
[
user_response(user1, %{"tags" => ["first"]}),
user_response(user2, %{"tags" => ["second"]})
]
|> Enum.sort_by(& &1["nickname"])
assert json_response(conn, 200) == %{
"count" => 2,
"page_size" => 50,
"users" => users
}
end
test "`active` filters out users pending approval", %{token: token} do
insert(:user, approval_pending: true)
%{id: user_id} = insert(:user, approval_pending: false)
%{id: admin_id} = token.user
conn =
build_conn()
|> assign(:user, token.user)
|> assign(:token, token)
|> get("/api/pleroma/admin/users?filters=active")
assert %{
"count" => 2,
"page_size" => 50,
"users" => [
%{"id" => ^admin_id},
%{"id" => ^user_id}
]
} = json_response(conn, 200)
end
test "it works with multiple filters" do
admin = insert(:user, nickname: "john", is_admin: true)
token = insert(:oauth_admin_token, user: admin)
user = insert(:user, nickname: "bob", local: false, deactivated: true)
insert(:user, nickname: "ken", local: true, deactivated: true)
insert(:user, nickname: "bobb", local: false, deactivated: false)
conn =
build_conn()
|> assign(:user, admin)
|> assign(:token, token)
|> get("/api/pleroma/admin/users?filters=deactivated,external")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [user_response(user)]
}
end
test "it omits relay user", %{admin: admin, conn: conn} do
assert %User{} = Relay.get_actor()
conn = get(conn, "/api/pleroma/admin/users")
assert json_response(conn, 200) == %{
"count" => 1,
"page_size" => 50,
"users" => [
user_response(admin, %{"roles" => %{"admin" => true, "moderator" => false}})
]
}
end
end
test "PATCH /api/pleroma/admin/users/activate", %{admin: admin, conn: conn} do
user_one = insert(:user, deactivated: true)
user_two = insert(:user, deactivated: true)
conn =
patch(
conn,
"/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", %{admin: admin, conn: conn} do
user_one = insert(:user, deactivated: false)
user_two = insert(:user, deactivated: false)
conn =
patch(
conn,
"/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/approve", %{admin: admin, conn: conn} do
user_one = insert(:user, approval_pending: true)
user_two = insert(:user, approval_pending: true)
conn =
patch(
conn,
"/api/pleroma/admin/users/approve",
%{nicknames: [user_one.nickname, user_two.nickname]}
)
response = json_response(conn, 200)
assert Enum.map(response["users"], & &1["approval_pending"]) == [false, false]
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} approved users: @#{user_one.nickname}, @#{user_two.nickname}"
end
test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation", %{admin: admin, conn: conn} do
user = insert(:user)
conn = patch(conn, "/api/pleroma/admin/users/#{user.nickname}/toggle_activation")
assert json_response(conn, 200) ==
user_response(
user,
%{"deactivated" => !user.deactivated}
)
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} deactivated users: @#{user.nickname}"
end
defp user_response(user, attrs \\ %{}) do
%{
"deactivated" => user.deactivated,
"id" => user.id,
"nickname" => user.nickname,
"roles" => %{"admin" => false, "moderator" => false},
"local" => user.local,
"tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false,
"approval_pending" => false,
"url" => user.ap_id,
"registration_reason" => nil,
"actor_type" => "Person"
}
|> Map.merge(attrs)
end
end

View File

@ -143,6 +143,20 @@ test "it returns users with tags" do
assert user2 in users assert user2 in users
end end
test "it returns users by actor_types" do
user_service = insert(:user, actor_type: "Service")
user_application = insert(:user, actor_type: "Application")
user1 = insert(:user)
user2 = insert(:user)
{:ok, [^user_service], 1} = Search.user(%{actor_types: ["Service"]})
{:ok, [^user_application], 1} = Search.user(%{actor_types: ["Application"]})
{:ok, [^user1, ^user2], 2} = Search.user(%{actor_types: ["Person"]})
{:ok, [^user_service, ^user1, ^user2], 3} =
Search.user(%{actor_types: ["Person", "Service"]})
end
test "it returns user by display name" do test "it returns user by display name" do
user = insert(:user, name: "Display name") user = insert(:user, name: "Display name")
insert(:user) insert(:user)
@ -178,6 +192,17 @@ test "it returns unapproved user" do
assert count == 1 assert count == 1
end end
test "it returns unconfirmed user" do
unconfirmed = insert(:user, confirmation_pending: true)
insert(:user)
insert(:user)
{:ok, _results, total} = Search.user()
{:ok, [^unconfirmed], count} = Search.user(%{unconfirmed: true})
assert total == 3
assert count == 1
end
test "it returns non-discoverable users" do test "it returns non-discoverable users" do
insert(:user) insert(:user)
insert(:user, is_discoverable: false) insert(:user, is_discoverable: false)

View File

@ -8,6 +8,7 @@ defmodule Pleroma.Web.Feed.TagControllerTest do
import Pleroma.Factory import Pleroma.Factory
import SweetXml import SweetXml
alias Pleroma.Config
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Feed.FeedView alias Pleroma.Web.Feed.FeedView
@ -15,7 +16,7 @@ defmodule Pleroma.Web.Feed.TagControllerTest do
setup do: clear_config([:feed]) setup do: clear_config([:feed])
test "gets a feed (ATOM)", %{conn: conn} do test "gets a feed (ATOM)", %{conn: conn} do
Pleroma.Config.put( Config.put(
[:feed, :post_title], [:feed, :post_title],
%{max_length: 25, omission: "..."} %{max_length: 25, omission: "..."}
) )
@ -82,7 +83,7 @@ test "gets a feed (ATOM)", %{conn: conn} do
end end
test "gets a feed (RSS)", %{conn: conn} do test "gets a feed (RSS)", %{conn: conn} do
Pleroma.Config.put( Config.put(
[:feed, :post_title], [:feed, :post_title],
%{max_length: 25, omission: "..."} %{max_length: 25, omission: "..."}
) )
@ -157,7 +158,7 @@ test "gets a feed (RSS)", %{conn: conn} do
response = response =
conn conn
|> put_req_header("accept", "application/rss+xml") |> put_req_header("accept", "application/rss+xml")
|> get(tag_feed_path(conn, :feed, "pleromaart")) |> get(tag_feed_path(conn, :feed, "pleromaart.rss"))
|> response(200) |> response(200)
xml = parse(response) xml = parse(response)
@ -183,14 +184,12 @@ test "gets a feed (RSS)", %{conn: conn} do
end end
describe "private instance" do describe "private instance" do
setup do: clear_config([:instance, :public]) setup do: clear_config([:instance, :public], false)
test "returns 404 for tags feed", %{conn: conn} do test "returns 404 for tags feed", %{conn: conn} do
Config.put([:instance, :public], false)
conn conn
|> put_req_header("accept", "application/rss+xml") |> put_req_header("accept", "application/rss+xml")
|> get(tag_feed_path(conn, :feed, "pleromaart")) |> get(tag_feed_path(conn, :feed, "pleromaart.rss"))
|> response(404) |> response(404)
end end
end end

View File

@ -13,7 +13,7 @@ defmodule Pleroma.Web.Feed.UserControllerTest do
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
setup do: clear_config([:instance, :federating], true) setup do: clear_config([:static_fe, :enabled], false)
describe "feed" do describe "feed" do
setup do: clear_config([:feed]) setup do: clear_config([:feed])
@ -192,6 +192,16 @@ test "returns 404 when the user is remote", %{conn: conn} do
|> get(user_feed_path(conn, :feed, user.nickname)) |> get(user_feed_path(conn, :feed, user.nickname))
|> response(404) |> response(404)
end end
test "does not require authentication on non-federating instances", %{conn: conn} do
clear_config([:instance, :federating], false)
user = insert(:user)
conn
|> put_req_header("accept", "application/rss+xml")
|> get("/users/#{user.nickname}/feed.rss")
|> response(200)
end
end end
# Note: see ActivityPubControllerTest for JSON format tests # Note: see ActivityPubControllerTest for JSON format tests

View File

@ -1509,28 +1509,103 @@ test "returns an empty list on a bad request", %{conn: conn} do
test "getting a list of mutes" do test "getting a list of mutes" do
%{user: user, conn: conn} = oauth_access(["read:mutes"]) %{user: user, conn: conn} = oauth_access(["read:mutes"])
other_user = insert(:user) %{id: id1} = other_user1 = insert(:user)
%{id: id2} = other_user2 = insert(:user)
%{id: id3} = other_user3 = insert(:user)
{:ok, _user_relationships} = User.mute(user, other_user) {:ok, _user_relationships} = User.mute(user, other_user1)
{:ok, _user_relationships} = User.mute(user, other_user2)
{:ok, _user_relationships} = User.mute(user, other_user3)
conn = get(conn, "/api/v1/mutes") result =
conn
|> assign(:user, user)
|> get("/api/v1/mutes")
|> json_response_and_validate_schema(200)
other_user_id = to_string(other_user.id) assert [id1, id2, id3] == Enum.map(result, & &1["id"])
assert [%{"id" => ^other_user_id}] = json_response_and_validate_schema(conn, 200)
result =
conn
|> assign(:user, user)
|> get("/api/v1/mutes?limit=1")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^id1}] = result
result =
conn
|> assign(:user, user)
|> get("/api/v1/mutes?since_id=#{id1}")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^id2}, %{"id" => ^id3}] = result
result =
conn
|> assign(:user, user)
|> get("/api/v1/mutes?since_id=#{id1}&max_id=#{id3}")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^id2}] = result
result =
conn
|> assign(:user, user)
|> get("/api/v1/mutes?since_id=#{id1}&limit=1")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^id2}] = result
end end
test "getting a list of blocks" do test "getting a list of blocks" do
%{user: user, conn: conn} = oauth_access(["read:blocks"]) %{user: user, conn: conn} = oauth_access(["read:blocks"])
other_user = insert(:user) %{id: id1} = other_user1 = insert(:user)
%{id: id2} = other_user2 = insert(:user)
%{id: id3} = other_user3 = insert(:user)
{:ok, _user_relationship} = User.block(user, other_user) {:ok, _user_relationship} = User.block(user, other_user1)
{:ok, _user_relationship} = User.block(user, other_user3)
{:ok, _user_relationship} = User.block(user, other_user2)
conn = result =
conn conn
|> assign(:user, user) |> assign(:user, user)
|> get("/api/v1/blocks") |> get("/api/v1/blocks")
|> json_response_and_validate_schema(200)
other_user_id = to_string(other_user.id) assert [id1, id2, id3] == Enum.map(result, & &1["id"])
assert [%{"id" => ^other_user_id}] = json_response_and_validate_schema(conn, 200)
result =
conn
|> assign(:user, user)
|> get("/api/v1/blocks?limit=1")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^id1}] = result
result =
conn
|> assign(:user, user)
|> get("/api/v1/blocks?since_id=#{id1}")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^id2}, %{"id" => ^id3}] = result
result =
conn
|> assign(:user, user)
|> get("/api/v1/blocks?since_id=#{id1}&max_id=#{id3}")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^id2}] = result
result =
conn
|> assign(:user, user)
|> get("/api/v1/blocks?since_id=#{id1}&limit=1")
|> json_response_and_validate_schema(200)
assert [%{"id" => ^id2}] = result
end end
end end

View File

@ -5,6 +5,7 @@
defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do defmodule Pleroma.Web.MastodonAPI.ConversationControllerTest do
use Pleroma.Web.ConnCase use Pleroma.Web.ConnCase
alias Pleroma.Conversation.Participation
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
@ -28,10 +29,10 @@ test "returns correct conversations", %{
user_three: user_three, user_three: user_three,
conn: conn conn: conn
} do } do
assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 assert Participation.unread_count(user_two) == 0
{:ok, direct} = create_direct_message(user_one, [user_two, user_three]) {:ok, direct} = create_direct_message(user_one, [user_two, user_three])
assert User.get_cached_by_id(user_two.id).unread_conversation_count == 1 assert Participation.unread_count(user_two) == 1
{:ok, _follower_only} = {:ok, _follower_only} =
CommonAPI.post(user_one, %{ CommonAPI.post(user_one, %{
@ -59,7 +60,7 @@ test "returns correct conversations", %{
assert is_binary(res_id) assert is_binary(res_id)
assert unread == false 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).unread_conversation_count == 0 assert Participation.unread_count(user_one) == 0
end end
test "observes limit params", %{ test "observes limit params", %{
@ -134,8 +135,8 @@ test "the user marks a conversation as read", %{user: user_one, conn: conn} do
user_two = insert(:user) user_two = insert(:user)
{:ok, direct} = create_direct_message(user_one, [user_two]) {:ok, direct} = create_direct_message(user_one, [user_two])
assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0 assert Participation.unread_count(user_one) == 0
assert User.get_cached_by_id(user_two.id).unread_conversation_count == 1 assert Participation.unread_count(user_two) == 1
user_two_conn = user_two_conn =
build_conn() build_conn()
@ -155,8 +156,8 @@ test "the user marks a conversation as read", %{user: user_one, conn: conn} do
|> post("/api/v1/conversations/#{direct_conversation_id}/read") |> post("/api/v1/conversations/#{direct_conversation_id}/read")
|> json_response_and_validate_schema(200) |> json_response_and_validate_schema(200)
assert User.get_cached_by_id(user_one.id).unread_conversation_count == 0 assert Participation.unread_count(user_one) == 0
assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 assert Participation.unread_count(user_two) == 0
# The conversation is marked as unread on reply # The conversation is marked as unread on reply
{:ok, _} = {:ok, _} =
@ -171,8 +172,8 @@ test "the user marks a conversation as read", %{user: user_one, conn: conn} do
|> get("/api/v1/conversations") |> get("/api/v1/conversations")
|> json_response_and_validate_schema(200) |> json_response_and_validate_schema(200)
assert User.get_cached_by_id(user_one.id).unread_conversation_count == 1 assert Participation.unread_count(user_one) == 1
assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 assert Participation.unread_count(user_two) == 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, _} =
@ -182,8 +183,8 @@ test "the user marks a conversation as read", %{user: user_one, conn: conn} do
in_reply_to_status_id: direct.id in_reply_to_status_id: direct.id
}) })
assert User.get_cached_by_id(user_one.id).unread_conversation_count == 1 assert Participation.unread_count(user_one) == 1
assert User.get_cached_by_id(user_two.id).unread_conversation_count == 0 assert Participation.unread_count(user_two) == 0
end end
test "(vanilla) Mastodon frontend behaviour", %{user: user_one, conn: conn} do test "(vanilla) Mastodon frontend behaviour", %{user: user_one, conn: conn} do

View File

@ -7,7 +7,6 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do
import Pleroma.Factory import Pleroma.Factory
alias Pleroma.Config
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
@ -21,7 +20,7 @@ defmodule Pleroma.Web.OStatus.OStatusControllerTest do
:ok :ok
end end
setup do: clear_config([:instance, :federating], true) setup do: clear_config([:static_fe, :enabled], false)
describe "Mastodon compatibility routes" do describe "Mastodon compatibility routes" do
setup %{conn: conn} do setup %{conn: conn} do
@ -215,15 +214,16 @@ test "404s a non-existing notice", %{conn: conn} do
assert response(conn, 404) assert response(conn, 404)
end end
test "it requires authentication if instance is NOT federating", %{ test "does not require authentication on non-federating instances", %{
conn: conn conn: conn
} do } do
user = insert(:user) clear_config([:instance, :federating], false)
note_activity = insert(:note_activity) note_activity = insert(:note_activity)
conn = put_req_header(conn, "accept", "text/html") conn
|> put_req_header("accept", "text/html")
ensure_federating_or_authenticated(conn, "/notice/#{note_activity.id}", user) |> get("/notice/#{note_activity.id}")
|> response(200)
end end
end end
@ -325,14 +325,16 @@ test "404s when attachment isn't audio or video", %{conn: conn} do
|> response(404) |> response(404)
end end
test "it requires authentication if instance is NOT federating", %{ test "does not require authentication on non-federating instances", %{
conn: conn, conn: conn,
note_activity: note_activity note_activity: note_activity
} do } do
user = insert(:user) clear_config([:instance, :federating], false)
conn = put_req_header(conn, "accept", "text/html")
ensure_federating_or_authenticated(conn, "/notice/#{note_activity.id}/embed_player", user) conn
|> put_req_header("accept", "text/html")
|> get("/notice/#{note_activity.id}/embed_player")
|> response(200)
end end
end end
end end

View File

@ -121,7 +121,7 @@ test "POST /api/v1/pleroma/conversations/read" do
[participation2, participation1] = Participation.for_user(other_user) [participation2, participation1] = Participation.for_user(other_user)
assert Participation.get(participation2.id).read == false assert Participation.get(participation2.id).read == false
assert Participation.get(participation1.id).read == false assert Participation.get(participation1.id).read == false
assert User.get_cached_by_id(other_user.id).unread_conversation_count == 2 assert Participation.unread_count(other_user) == 2
[%{"unread" => false}, %{"unread" => false}] = [%{"unread" => false}, %{"unread" => false}] =
conn conn
@ -131,6 +131,6 @@ test "POST /api/v1/pleroma/conversations/read" do
[participation2, participation1] = Participation.for_user(other_user) [participation2, participation1] = Participation.for_user(other_user)
assert Participation.get(participation2.id).read == true assert Participation.get(participation2.id).read == true
assert Participation.get(participation1.id).read == true assert Participation.get(participation1.id).read == true
assert User.get_cached_by_id(other_user.id).unread_conversation_count == 0 assert Participation.unread_count(other_user) == 0
end end
end end

View File

@ -4,6 +4,7 @@
defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do defmodule Pleroma.Web.Plugs.FrontendStaticPlugTest do
use Pleroma.Web.ConnCase use Pleroma.Web.ConnCase
import Mock
@dir "test/tmp/instance_static" @dir "test/tmp/instance_static"
@ -53,4 +54,24 @@ test "overrides existing static files for the `pleroma/admin` path", %{conn: con
index = get(conn, "/pleroma/admin/") index = get(conn, "/pleroma/admin/")
assert html_response(index, 200) == "from frontend plug" assert html_response(index, 200) == "from frontend plug"
end end
test "exclude invalid path", %{conn: conn} do
name = "pleroma-fe"
ref = "dist"
clear_config([:media_proxy, :enabled], true)
clear_config([Pleroma.Web.Endpoint, :secret_key_base], "00000000000")
clear_config([:frontends, :primary], %{"name" => name, "ref" => ref})
path = "#{@dir}/frontends/#{name}/#{ref}"
File.mkdir_p!("#{path}/proxy/rr/ss")
File.write!("#{path}/proxy/rr/ss/Ek7w8WPVcAApOvN.jpg:large", "FB image")
url =
Pleroma.Web.MediaProxy.encode_url("https://pbs.twimg.com/media/Ek7w8WPVcAApOvN.jpg:large")
with_mock Pleroma.ReverseProxy,
call: fn _conn, _url, _opts -> %Plug.Conn{status: :success} end do
assert %Plug.Conn{status: :success} = get(conn, url)
end
end
end end

View File

@ -6,14 +6,12 @@ defmodule Pleroma.Web.StaticFE.StaticFEControllerTest do
use Pleroma.Web.ConnCase use Pleroma.Web.ConnCase
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Config
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
import Pleroma.Factory import Pleroma.Factory
setup_all do: clear_config([:static_fe, :enabled], true) setup_all do: clear_config([:static_fe, :enabled], true)
setup do: clear_config([:instance, :federating], true)
setup %{conn: conn} do setup %{conn: conn} do
conn = put_req_header(conn, "accept", "text/html") conn = put_req_header(conn, "accept", "text/html")
@ -74,8 +72,27 @@ test "pagination, page 2", %{conn: conn, user: user} do
refute html =~ ">test29<" refute html =~ ">test29<"
end end
test "it requires authentication if instance is NOT federating", %{conn: conn, user: user} do test "does not require authentication on non-federating instances", %{
ensure_federating_or_authenticated(conn, "/users/#{user.nickname}", user) conn: conn,
user: user
} do
clear_config([:instance, :federating], false)
conn = get(conn, "/users/#{user.nickname}")
assert html_response(conn, 200) =~ user.nickname
end
test "returns 404 for local user with `restrict_unauthenticated/profiles/local` setting", %{
conn: conn
} do
clear_config([:restrict_unauthenticated, :profiles, :local], true)
local_user = insert(:user, local: true)
conn
|> get("/users/#{local_user.nickname}")
|> html_response(404)
end end
end end
@ -187,10 +204,28 @@ test "302 for remote cached status", %{conn: conn, user: user} do
assert html_response(conn, 302) =~ "redirected" assert html_response(conn, 302) =~ "redirected"
end end
test "it requires authentication if instance is NOT federating", %{conn: conn, user: user} do test "does not require authentication on non-federating instances", %{
conn: conn,
user: user
} do
clear_config([:instance, :federating], false)
{:ok, activity} = CommonAPI.post(user, %{status: "testing a thing!"}) {:ok, activity} = CommonAPI.post(user, %{status: "testing a thing!"})
ensure_federating_or_authenticated(conn, "/notice/#{activity.id}", user) conn = get(conn, "/notice/#{activity.id}")
assert html_response(conn, 200) =~ "testing a thing!"
end
test "returns 404 for local public activity with `restrict_unauthenticated/activities/local` setting",
%{conn: conn, user: user} do
clear_config([:restrict_unauthenticated, :activities, :local], true)
{:ok, activity} = CommonAPI.post(user, %{status: "testing a thing!"})
conn
|> get("/notice/#{activity.id}")
|> html_response(404)
end end
end end
end end

View File

@ -112,28 +112,6 @@ defp json_response_and_validate_schema(
defp json_response_and_validate_schema(conn, _status) do defp json_response_and_validate_schema(conn, _status) do
flunk("Response schema not found for #{conn.method} #{conn.request_path} #{conn.status}") flunk("Response schema not found for #{conn.method} #{conn.request_path} #{conn.status}")
end end
defp ensure_federating_or_authenticated(conn, url, user) do
initial_setting = Config.get([:instance, :federating])
on_exit(fn -> Config.put([:instance, :federating], initial_setting) end)
Config.put([:instance, :federating], false)
conn
|> get(url)
|> response(403)
conn
|> assign(:user, user)
|> get(url)
|> response(200)
Config.put([:instance, :federating], true)
conn
|> get(url)
|> response(200)
end
end end
end end