Merge branch 'develop' into feature/admin-api-render-whole-status

This commit is contained in:
Maxim Filippov 2019-11-26 00:25:43 +09:00
commit b62be2564c
37 changed files with 588 additions and 198 deletions

View File

@ -43,6 +43,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Authentication: Added rate limit for password-authorized actions / login existence checks - Authentication: Added rate limit for password-authorized actions / login existence checks
- Static Frontend: Add the ability to render user profiles and notices server-side without requiring JS app. - Static Frontend: Add the ability to render user profiles and notices server-side without requiring JS app.
- Mix task to re-count statuses for all users (`mix pleroma.count_statuses`) - Mix task to re-count statuses for all users (`mix pleroma.count_statuses`)
- Mix task to list all users (`mix pleroma.user list`)
- Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache). - Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache).
- MRF: New module which handles incoming posts based on their age. By default, all incoming posts that are older than 2 days will be unlisted and not shown to their followers. - MRF: New module which handles incoming posts based on their age. By default, all incoming posts that are older than 2 days will be unlisted and not shown to their followers.
<details> <details>
@ -70,6 +71,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: Add the `recipients` parameter to `GET /api/v1/conversations` - Mastodon API: Add the `recipients` parameter to `GET /api/v1/conversations`
- Configuration: `feed` option for user atom feed. - Configuration: `feed` option for user atom feed.
- Pleroma API: Add Emoji reactions - Pleroma API: Add Emoji reactions
- Admin API: Add `/api/pleroma/admin/instances/:instance/statuses` - lists all statuses from a given instance
- Admin API: `PATCH /api/pleroma/users/confirm_email` to confirm email for multiple users, `PATCH /api/pleroma/users/resend_confirmation_email` to resend confirmation email for multiple users
</details> </details>
### Fixed ### Fixed
@ -81,6 +84,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`) - Mastodon API: Fix private and direct statuses not being filtered out from the public timeline for an authenticated user (`GET /api/v1/timelines/public`)
- Mastodon API: Inability to get some local users by nickname in `/api/v1/accounts/:id_or_nickname` - Mastodon API: Inability to get some local users by nickname in `/api/v1/accounts/:id_or_nickname`
- AdminAPI: If some status received reports both in the "new" format and "old" format it was considered reports on two different statuses (in the context of grouped reports) - AdminAPI: If some status received reports both in the "new" format and "old" format it was considered reports on two different statuses (in the context of grouped reports)
- Admin API: Error when trying to update reports in the "old" format
</details> </details>
## [1.1.6] - 2019-11-19 ## [1.1.6] - 2019-11-19

View File

@ -180,7 +180,8 @@
# Configures Elixir's Logger # Configures Elixir's Logger
config :logger, :console, config :logger, :console,
format: "$time $metadata[$level] $message\n", level: :debug,
format: "\n$time $metadata[$level] $message\n",
metadata: [:request_id] metadata: [:request_id]
config :logger, :ex_syslogger, config :logger, :ex_syslogger,
@ -208,6 +209,7 @@
config :pleroma, :http, config :pleroma, :http,
proxy_url: nil, proxy_url: nil,
send_user_agent: true, send_user_agent: true,
user_agent: :default,
adapter: [ adapter: [
ssl_options: [ ssl_options: [
# Workaround for remote server certificate chain issues # Workaround for remote server certificate chain issues

View File

@ -20,7 +20,7 @@
config :phoenix, serve_endpoints: true config :phoenix, serve_endpoints: true
# Do not print debug messages in production # Do not print debug messages in production
config :logger, level: :warn config :logger, :console, level: :warn
# ## SSL Support # ## SSL Support
# #

View File

@ -1,6 +1,6 @@
import Config import Config
config :pleroma, :instance, static: "/var/lib/pleroma/static" config :pleroma, :instance, static_dir: "/var/lib/pleroma/static"
config :pleroma, Pleroma.Uploaders.Local, uploads: "/var/lib/pleroma/uploads" config :pleroma, Pleroma.Uploaders.Local, uploads: "/var/lib/pleroma/uploads"
config_path = System.get_env("PLEROMA_CONFIG_PATH") || "/etc/pleroma/config.exs" config_path = System.get_env("PLEROMA_CONFIG_PATH") || "/etc/pleroma/config.exs"

View File

@ -15,7 +15,9 @@
method: Pleroma.Captcha.Mock method: Pleroma.Captcha.Mock
# Print only warnings and errors during test # Print only warnings and errors during test
config :logger, level: :warn config :logger, :console,
level: :warn,
format: "\n[$level] $message\n"
config :pleroma, :auth, oauth_consumer_strategies: [] config :pleroma, :auth, oauth_consumer_strategies: []

View File

@ -870,3 +870,19 @@ Compile time settings (need instance reboot):
- Authentication: required - Authentication: required
- Params: None - Params: None
- Response: JSON, "ok" and 200 status - Response: JSON, "ok" and 200 status
## `PATCH /api/pleroma/admin/users/confirm_email`
### Confirm users' emails
- Params:
- `nicknames`
- Response: Array of user nicknames
## `PATCH /api/pleroma/admin/users/resend_confirmation_email`
### Resend confirmation email
- Params:
- `nicknames`
- Response: Array of user nicknames

View File

@ -15,6 +15,11 @@ $PREFIX new <nickname> <email> [<options>]
- `--admin`/`--no-admin` - whether the user should be an admin - `--admin`/`--no-admin` - whether the user should be an admin
- `-y`, `--assume-yes`/`--no-assume-yes` - whether to assume yes to all questions - `-y`, `--assume-yes`/`--no-assume-yes` - whether to assume yes to all questions
## List local users
```sh
$PREFIX list
```
## Generate an invite link ## Generate an invite link
```sh ```sh
$PREFIX invite [<options>] $PREFIX invite [<options>]

View File

@ -348,7 +348,17 @@ Available caches:
* `:activity_pub` - activity pub routes (except question activities). Defaults to `nil` (no expiration). * `:activity_pub` - activity pub routes (except question activities). Defaults to `nil` (no expiration).
* `:activity_pub_question` - activity pub routes (question activities). Defaults to `30_000` (30 seconds). * `:activity_pub_question` - activity pub routes (question activities). Defaults to `30_000` (30 seconds).
## :hackney_pools ## HTTP client
### :http
* `proxy_url`: an upstream proxy to fetch posts and/or media with, (default: `nil`)
* `send_user_agent`: should we include a user agent with HTTP requests? (default: `true`)
* `user_agent`: what user agent should we use? (default: `:default`), must be string or `:default`
* `adapter`: array of hackney options
### :hackney_pools
Advanced. Tweaks Hackney (http client) connections pools. Advanced. Tweaks Hackney (http client) connections pools.

View File

@ -6,6 +6,11 @@ defmodule Mix.Pleroma do
@doc "Common functions to be reused in mix tasks" @doc "Common functions to be reused in mix tasks"
def start_pleroma do def start_pleroma do
Application.put_env(:phoenix, :serve_endpoints, false, persistent: true) Application.put_env(:phoenix, :serve_endpoints, false, persistent: true)
if Pleroma.Config.get(:env) != :test do
Application.put_env(:logger, :console, level: :debug)
end
{:ok, _} = Application.ensure_all_started(:pleroma) {:ok, _} = Application.ensure_all_started(:pleroma)
end end

View File

@ -364,6 +364,24 @@ def run(["sign_out", nickname]) do
end end
end end
def run(["list"]) do
start_pleroma()
Pleroma.User.Query.build(%{local: true})
|> Pleroma.RepoStreamer.chunk_stream(500)
|> Stream.each(fn users ->
users
|> Enum.each(fn user ->
shell_info(
"#{user.nickname} moderator: #{user.info.is_moderator}, admin: #{user.info.is_admin}, locked: #{
user.info.locked
}, deactivated: #{user.info.deactivated}"
)
end)
end)
|> Stream.run()
end
defp set_moderator(user, value) do defp set_moderator(user, value) do
{:ok, user} = {:ok, user} =
user user

View File

@ -17,8 +17,14 @@ def named_version, do: @name <> " " <> @version
def repository, do: @repository def repository, do: @repository
def user_agent do def user_agent do
case Pleroma.Config.get([:http, :user_agent], :default) do
:default ->
info = "#{Pleroma.Web.base_url()} <#{Pleroma.Config.get([:instance, :email], "")}>" info = "#{Pleroma.Web.base_url()} <#{Pleroma.Config.get([:instance, :email], "")}>"
named_version() <> "; " <> info named_version() <> "; " <> info
custom ->
custom
end
end end
# See http://elixir-lang.org/docs/stable/elixir/Application.html # See http://elixir-lang.org/docs/stable/elixir/Application.html

View File

@ -101,7 +101,7 @@ def following(%User{} = user) do
|> select([r, u], u.follower_address) |> select([r, u], u.follower_address)
|> Repo.all() |> Repo.all()
if not user.local or user.nickname in [nil, "internal.fetch"] do if not user.local or user.invisible do
following following
else else
[user.follower_address | following] [user.follower_address | following]

View File

@ -624,7 +624,31 @@ def get_log_entry_message(%ModerationLog{
"subject" => subjects "subject" => subjects
} }
}) do }) do
"@#{actor_nickname} force password reset for users: #{users_to_nicknames_string(subjects)}" "@#{actor_nickname} forced password reset for users: #{users_to_nicknames_string(subjects)}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "confirm_email",
"subject" => subjects
}
}) do
"@#{actor_nickname} confirmed email for users: #{users_to_nicknames_string(subjects)}"
end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "resend_confirmation_email",
"subject" => subjects
}
}) do
"@#{actor_nickname} re-sent confirmation email for users: #{
users_to_nicknames_string(subjects)
}"
end end
defp nicknames_to_string(nicknames) do defp nicknames_to_string(nicknames) do

View File

@ -63,7 +63,7 @@ def get_by_ap_id(ap_id) do
end end
defp warn_on_no_object_preloaded(ap_id) do defp warn_on_no_object_preloaded(ap_id) do
"Object.normalize() called without preloaded object (#{ap_id}). Consider preloading the object" "Object.normalize() called without preloaded object (#{inspect(ap_id)}). Consider preloading the object"
|> Logger.debug() |> Logger.debug()
Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}") Logger.debug("Backtrace: #{inspect(Process.info(:erlang.self(), :current_stacktrace))}")
@ -255,4 +255,8 @@ def update_data(%Object{data: data} = object, attrs \\ %{}) do
|> Object.change(%{data: Map.merge(data || %{}, attrs)}) |> Object.change(%{data: Map.merge(data || %{}, attrs)})
|> Repo.update() |> Repo.update()
end end
def local?(%Object{data: %{"id" => id}}) do
String.starts_with?(id, Pleroma.Web.base_url() <> "/")
end
end end

View File

@ -49,7 +49,7 @@ defp reinject_object(struct, data) do
end end
def refetch_object(%Object{data: %{"id" => id}} = object) do def refetch_object(%Object{data: %{"id" => id}} = object) do
with {:local, false} <- {:local, String.starts_with?(id, Pleroma.Web.base_url() <> "/")}, with {:local, false} <- {:local, Object.local?(object)},
{:ok, data} <- fetch_and_contain_remote_object_from_id(id), {:ok, data} <- fetch_and_contain_remote_object_from_id(id),
{:ok, object} <- reinject_object(object, data) do {:ok, object} <- reinject_object(object, data) do
{:ok, object} {:ok, object}

View File

@ -67,8 +67,7 @@ defmodule Pleroma.User do
field(:source_data, :map, default: %{}) field(:source_data, :map, default: %{})
field(:note_count, :integer, default: 0) field(:note_count, :integer, default: 0)
field(:follower_count, :integer, default: 0) field(:follower_count, :integer, default: 0)
# Should be filled in only for remote users field(:following_count, :integer, default: 0)
field(:following_count, :integer, default: nil)
field(:locked, :boolean, default: false) field(:locked, :boolean, default: false)
field(:confirmation_pending, :boolean, default: false) field(:confirmation_pending, :boolean, default: false)
field(:password_reset_pending, :boolean, default: false) field(:password_reset_pending, :boolean, default: false)
@ -134,6 +133,8 @@ def auth_active?(%User{}), do: true
def visible_for?(user, for_user \\ nil) def visible_for?(user, for_user \\ nil)
def visible_for?(%User{invisible: true}, _), do: false
def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true def visible_for?(%User{id: user_id}, %User{id: for_id}) when user_id == for_id, do: true
def visible_for?(%User{} = user, for_user) do def visible_for?(%User{} = user, for_user) do
@ -177,19 +178,17 @@ def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
def ap_following(%User{} = user), do: "#{ap_id(user)}/following" def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
def user_info(%User{} = user, args \\ %{}) do def user_info(%User{} = user, args \\ %{}) do
following_count = following_count = Map.get(args, :following_count, user.following_count)
Map.get(args, :following_count, user.following_count || following_count(user))
follower_count = Map.get(args, :follower_count, user.follower_count) follower_count = Map.get(args, :follower_count, user.follower_count)
%{ %{
note_count: user.note_count, note_count: user.note_count,
locked: user.locked, locked: user.locked,
confirmation_pending: user.confirmation_pending, confirmation_pending: user.confirmation_pending,
default_scope: user.default_scope default_scope: user.default_scope,
follower_count: follower_count,
following_count: following_count
} }
|> Map.put(:following_count, following_count)
|> Map.put(:follower_count, follower_count)
end end
def follow_state(%User{} = user, %User{} = target) do def follow_state(%User{} = user, %User{} = target) do
@ -492,6 +491,10 @@ def try_send_confirmation_email(%User{} = user) do
end end
end end
def try_send_confirmation_email(users) do
Enum.each(users, &try_send_confirmation_email/1)
end
def needs_update?(%User{local: true}), do: false def needs_update?(%User{local: true}), do: false
def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true def needs_update?(%User{local: false, last_refreshed_at: nil}), do: true
@ -522,14 +525,9 @@ def maybe_direct_follow(%User{} = follower, %User{} = followed) do
@doc "A mass follow for local users. Respects blocks in both directions but does not create activities." @doc "A mass follow for local users. Respects blocks in both directions but does not create activities."
@spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()} @spec follow_all(User.t(), list(User.t())) :: {atom(), User.t()}
def follow_all(follower, followeds) do def follow_all(follower, followeds) do
followeds = followeds
Enum.reject(followeds, fn followed -> |> Enum.reject(fn followed -> blocks?(follower, followed) || blocks?(followed, follower) end)
blocks?(follower, followed) || blocks?(followed, follower) |> Enum.each(&follow(follower, &1, "accept"))
end)
Enum.each(followeds, &follow(follower, &1, "accept"))
Enum.each(followeds, &update_follower_count/1)
set_cache(follower) set_cache(follower)
end end
@ -549,11 +547,11 @@ def follow(%User{} = follower, %User{} = followed, state \\ "accept") do
true -> true ->
FollowingRelationship.follow(follower, followed, state) FollowingRelationship.follow(follower, followed, state)
follower = maybe_update_following_count(follower)
{:ok, _} = update_follower_count(followed) {:ok, _} = update_follower_count(followed)
set_cache(follower) follower
|> update_following_count()
|> set_cache()
end end
end end
@ -561,11 +559,12 @@ def unfollow(%User{} = follower, %User{} = followed) do
if following?(follower, followed) and follower.ap_id != followed.ap_id do if following?(follower, followed) and follower.ap_id != followed.ap_id do
FollowingRelationship.unfollow(follower, followed) FollowingRelationship.unfollow(follower, followed)
follower = maybe_update_following_count(follower)
{:ok, followed} = update_follower_count(followed) {:ok, followed} = update_follower_count(followed)
set_cache(follower) {:ok, follower} =
follower
|> update_following_count()
|> set_cache()
{:ok, follower, Utils.fetch_latest_follow(follower, followed)} {:ok, follower, Utils.fetch_latest_follow(follower, followed)}
else else
@ -895,8 +894,8 @@ def update_follower_count(%User{} = user) do
end end
end end
@spec maybe_update_following_count(User.t()) :: User.t() @spec update_following_count(User.t()) :: User.t()
def maybe_update_following_count(%User{local: false} = user) do def update_following_count(%User{local: false} = user) do
if Pleroma.Config.get([:instance, :external_user_synchronization]) do if Pleroma.Config.get([:instance, :external_user_synchronization]) do
maybe_fetch_follow_information(user) maybe_fetch_follow_information(user)
else else
@ -904,7 +903,13 @@ def maybe_update_following_count(%User{local: false} = user) do
end end
end end
def maybe_update_following_count(user), do: user def update_following_count(%User{local: true} = user) do
following_count = FollowingRelationship.following_count(user)
user
|> follow_information_changeset(%{following_count: following_count})
|> Repo.update!()
end
def set_unread_conversation_count(%User{local: true} = user) do def set_unread_conversation_count(%User{local: true} = user) do
unread_query = Participation.unread_conversation_count_for_user(user) unread_query = Participation.unread_conversation_count_for_user(user)
@ -1097,7 +1102,12 @@ def deactivate(users, status) when is_list(users) do
def deactivate(%User{} = user, status) do def deactivate(%User{} = user, status) do
with {:ok, user} <- set_activation_status(user, status) do with {:ok, user} <- set_activation_status(user, status) do
Enum.each(get_followers(user), &invalidate_cache/1) user
|> get_followers()
|> Enum.filter(& &1.local)
|> Enum.each(fn follower ->
follower |> update_following_count() |> set_cache()
end)
# Only update local user counts, remote will be update during the next pull. # Only update local user counts, remote will be update during the next pull.
user user
@ -1317,19 +1327,20 @@ def get_or_fetch_by_ap_id(ap_id) do
end end
end end
@doc "Creates an internal service actor by URI if missing. Optionally takes nickname for addressing." @doc """
Creates an internal service actor by URI if missing.
Optionally takes nickname for addressing.
"""
def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do def get_or_create_service_actor_by_ap_id(uri, nickname \\ nil) do
with %User{} = user <- get_cached_by_ap_id(uri) do with user when is_nil(user) <- get_cached_by_ap_id(uri) do
user
else
_ ->
{:ok, user} = {:ok, user} =
%User{} %User{
|> cast(%{}, [:ap_id, :nickname, :local]) invisible: true,
|> put_change(:ap_id, uri) local: true,
|> put_change(:nickname, nickname) ap_id: uri,
|> put_change(:local, true) nickname: nickname,
|> put_change(:follower_address, uri <> "/followers") follower_address: uri <> "/followers"
}
|> Repo.insert() |> Repo.insert()
user user
@ -1575,6 +1586,11 @@ def toggle_confirmation(%User{} = user) do
|> update_and_set_cache() |> update_and_set_cache()
end end
@spec toggle_confirmation([User.t()]) :: [{:ok, User.t()} | {:error, Changeset.t()}]
def toggle_confirmation(users) do
Enum.map(users, &toggle_confirmation/1)
end
def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do def get_mascot(%{mascot: %{} = mascot}) when not is_nil(mascot) do
mascot mascot
end end

View File

@ -45,6 +45,7 @@ defp search_query(query_string, for_user, following) do
for_user for_user
|> base_query(following) |> base_query(following)
|> filter_blocked_user(for_user) |> filter_blocked_user(for_user)
|> filter_invisible_users()
|> filter_blocked_domains(for_user) |> filter_blocked_domains(for_user)
|> fts_search(query_string) |> fts_search(query_string)
|> trigram_rank(query_string) |> trigram_rank(query_string)
@ -98,6 +99,10 @@ defp trigram_rank(query, query_string) do
defp base_query(_user, false), do: User defp base_query(_user, false), do: User
defp base_query(user, true), do: User.get_followers_query(user) defp base_query(user, true), do: User.get_followers_query(user)
defp filter_invisible_users(query) do
from(q in query, where: q.invisible == false)
end
defp filter_blocked_user(query, %User{blocks: blocks}) defp filter_blocked_user(query, %User{blocks: blocks})
when length(blocks) > 0 do when length(blocks) > 0 do
from(q in query, where: not (q.ap_id in ^blocks)) from(q in query, where: not (q.ap_id in ^blocks))

View File

@ -734,6 +734,17 @@ def fetch_user_activities(user, reading_user, params \\ %{}) do
|> Enum.reverse() |> Enum.reverse()
end end
def fetch_instance_activities(params) do
params =
params
|> Map.put("type", ["Create", "Announce"])
|> Map.put("instance", params["instance"])
|> Map.put("whole_db", true)
fetch_activities([Pleroma.Constants.as_public()], params, :offset)
|> Enum.reverse()
end
defp user_activities_recipients(%{"godmode" => true}) do defp user_activities_recipients(%{"godmode" => true}) do
[] []
end end
@ -961,6 +972,20 @@ defp restrict_muted_reblogs(query, %{"muting_user" => %User{} = user}) do
defp restrict_muted_reblogs(query, _), do: query defp restrict_muted_reblogs(query, _), do: query
defp restrict_instance(query, %{"instance" => instance}) do
users =
from(
u in User,
select: u.ap_id,
where: fragment("? LIKE ?", u.nickname, ^"%@#{instance}")
)
|> Repo.all()
from(activity in query, where: activity.actor in ^users)
end
defp restrict_instance(query, _), do: query
defp exclude_poll_votes(query, %{"include_poll_votes" => true}), do: query defp exclude_poll_votes(query, %{"include_poll_votes" => true}), do: query
defp exclude_poll_votes(query, _) do defp exclude_poll_votes(query, _) do
@ -1041,6 +1066,7 @@ def fetch_activities_query(recipients, opts \\ %{}) do
|> restrict_reblogs(opts) |> restrict_reblogs(opts)
|> restrict_pinned(opts) |> restrict_pinned(opts)
|> restrict_muted_reblogs(opts) |> restrict_muted_reblogs(opts)
|> restrict_instance(opts)
|> Activity.restrict_deactivated_users() |> Activity.restrict_deactivated_users()
|> exclude_poll_votes(opts) |> exclude_poll_votes(opts)
|> exclude_visibility(opts) |> exclude_visibility(opts)

View File

@ -45,7 +45,7 @@ def relay_active?(conn, _) do
end end
def user(conn, %{"nickname" => nickname}) do def user(conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname(nickname), with %User{local: true} = user <- User.get_cached_by_nickname(nickname),
{:ok, user} <- User.ensure_keys_present(user) do {:ok, user} <- User.ensure_keys_present(user) do
conn conn
|> put_resp_content_type("application/activity+json") |> put_resp_content_type("application/activity+json")
@ -53,6 +53,7 @@ def user(conn, %{"nickname" => nickname}) do
|> render("user.json", %{user: user}) |> render("user.json", %{user: user})
else else
nil -> {:error, :not_found} nil -> {:error, :not_found}
%{local: false} -> {:error, :not_found}
end end
end end

View File

@ -14,7 +14,6 @@ def get_actor do
relay_ap_id() relay_ap_id()
|> User.get_or_create_service_actor_by_ap_id() |> User.get_or_create_service_actor_by_ap_id()
{:ok, actor} = User.set_invisible(actor, true)
actor actor
end end

View File

@ -919,7 +919,13 @@ def update_report_state(_, _), do: {:error, "Unsupported state"}
def strip_report_status_data(activity) do def strip_report_status_data(activity) do
[actor | reported_activities] = activity.data["object"] [actor | reported_activities] = activity.data["object"]
stripped_activities = Enum.map(reported_activities, & &1["id"])
stripped_activities =
Enum.map(reported_activities, fn
act when is_map(act) -> act["id"]
act when is_binary(act) -> act
end)
new_data = put_in(activity.data, ["object"], [actor | stripped_activities]) new_data = put_in(activity.data, ["object"], [actor | stripped_activities])
{:ok, %{activity | data: new_data}} {:ok, %{activity | data: new_data}}

View File

@ -227,6 +227,21 @@ def user_show(conn, %{"nickname" => nickname}) do
end end
end end
def list_instance_statuses(conn, %{"instance" => instance} = params) do
{page, page_size} = page_params(params)
activities =
ActivityPub.fetch_instance_activities(%{
"instance" => instance,
"limit" => page_size,
"offset" => (page - 1) * page_size
})
conn
|> put_view(StatusView)
|> render("index.json", %{activities: activities, as: :activity})
end
def list_user_statuses(conn, %{"nickname" => nickname} = params) do def list_user_statuses(conn, %{"nickname" => nickname} = params) do
godmode = params["godmode"] == "true" || params["godmode"] == true godmode = params["godmode"] == "true" || params["godmode"] == true
@ -335,7 +350,7 @@ def list_users(conn, params) do
} }
with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)), with {:ok, users, count} <- Search.user(Map.merge(search_params, filters)),
{:ok, users, count} <- filter_relay_user(users, count), {:ok, users, count} <- filter_service_users(users, count),
do: do:
conn conn
|> json( |> json(
@ -347,15 +362,16 @@ def list_users(conn, params) do
) )
end end
defp filter_relay_user(users, count) do defp filter_service_users(users, count) do
filtered_users = Enum.reject(users, &relay_user?/1) filtered_users = Enum.reject(users, &service_user?/1)
count = if Enum.any?(users, &relay_user?/1), do: length(filtered_users), else: count count = if Enum.any?(users, &service_user?/1), do: length(filtered_users), else: count
{:ok, filtered_users, count} {:ok, filtered_users, count}
end end
defp relay_user?(user) do defp service_user?(user) do
user.ap_id == Relay.relay_ap_id() String.match?(user.ap_id, ~r/.*\/relay$/) or
String.match?(user.ap_id, ~r/.*\/internal\/fetch$/)
end end
@filters ~w(local external active deactivated is_admin is_moderator) @filters ~w(local external active deactivated is_admin is_moderator)
@ -799,6 +815,34 @@ def reload_emoji(conn, _params) do
conn |> json("ok") conn |> json("ok")
end end
def confirm_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
User.toggle_confirmation(users)
ModerationLog.insert_log(%{
actor: admin,
subject: users,
action: "confirm_email"
})
conn |> json("")
end
def resend_confirmation_email(%{assigns: %{user: admin}} = conn, %{"nicknames" => nicknames}) do
users = nicknames |> Enum.map(&User.get_cached_by_nickname/1)
User.try_send_confirmation_email(users)
ModerationLog.insert_log(%{
actor: admin,
subject: users,
action: "resend_confirmation_email"
})
conn |> json("")
end
def errors(conn, {:error, :not_found}) do def errors(conn, {:error, :not_found}) do
conn conn
|> put_status(:not_found) |> put_status(:not_found)

View File

@ -36,7 +36,8 @@ def render("show.json", %{user: user}) do
"deactivated" => user.deactivated, "deactivated" => user.deactivated,
"local" => user.local, "local" => user.local,
"roles" => User.roles(user), "roles" => User.roles(user),
"tags" => user.tags || [] "tags" => user.tags || [],
"confirmation_pending" => user.confirmation_pending
} }
end end

View File

@ -238,7 +238,7 @@ def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
@doc "GET /api/v1/accounts/:id" @doc "GET /api/v1/accounts/:id"
def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do def show(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user), with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do true <- User.visible_for?(user, for_user) do
render(conn, "show.json", user: user, for: for_user) render(conn, "show.json", user: user, for: for_user)
else else
_e -> render_error(conn, :not_found, "Can't find user") _e -> render_error(conn, :not_found, "Can't find user")

View File

@ -11,7 +11,6 @@ defmodule Pleroma.Web.OStatus.OStatusController do
alias Pleroma.Plugs.RateLimiter alias Pleroma.Plugs.RateLimiter
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPubController alias Pleroma.Web.ActivityPub.ActivityPubController
alias Pleroma.Web.ActivityPub.ObjectView
alias Pleroma.Web.ActivityPub.Visibility alias Pleroma.Web.ActivityPub.Visibility
alias Pleroma.Web.Endpoint alias Pleroma.Web.Endpoint
alias Pleroma.Web.Metadata.PlayerView alias Pleroma.Web.Metadata.PlayerView
@ -38,11 +37,9 @@ def object(%{assigns: %{format: format}} = conn, %{"uuid" => uuid}) do
with id <- o_status_url(conn, :object, uuid), with id <- o_status_url(conn, :object, uuid),
{_, %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)}, {_, true} <- {:public?, Visibility.is_public?(activity)} do
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
case format do case format do
"html" -> redirect(conn, to: "/notice/#{activity.id}") _ -> redirect(conn, to: "/notice/#{activity.id}")
_ -> represent_activity(conn, nil, activity, user)
end end
else else
reason when reason in [{:public?, false}, {:activity, nil}] -> reason when reason in [{:public?, false}, {:activity, nil}] ->
@ -61,11 +58,9 @@ def activity(%{assigns: %{format: format}} = conn, %{"uuid" => _uuid})
def activity(%{assigns: %{format: format}} = conn, %{"uuid" => uuid}) do def activity(%{assigns: %{format: format}} = conn, %{"uuid" => uuid}) do
with id <- o_status_url(conn, :activity, uuid), with id <- o_status_url(conn, :activity, uuid),
{_, %Activity{} = activity} <- {:activity, Activity.normalize(id)}, {_, %Activity{} = activity} <- {:activity, Activity.normalize(id)},
{_, true} <- {:public?, Visibility.is_public?(activity)}, {_, true} <- {:public?, Visibility.is_public?(activity)} do
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
case format do case format do
"html" -> redirect(conn, to: "/notice/#{activity.id}") _ -> redirect(conn, to: "/notice/#{activity.id}")
_ -> represent_activity(conn, format, activity, user)
end end
else else
reason when reason in [{:public?, false}, {:activity, nil}] -> reason when reason in [{:public?, false}, {:activity, nil}] ->
@ -81,7 +76,15 @@ def notice(%{assigns: %{format: format}} = conn, %{"id" => id}) do
{_, true} <- {:public?, Visibility.is_public?(activity)}, {_, true} <- {:public?, Visibility.is_public?(activity)},
%User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do %User{} = user <- User.get_cached_by_ap_id(activity.data["actor"]) do
cond do cond do
format == "html" && activity.data["type"] == "Create" -> format in ["json", "activity+json"] ->
if activity.local do
%{data: %{"id" => redirect_url}} = Object.normalize(activity)
redirect(conn, external: redirect_url)
else
{:error, :not_found}
end
activity.data["type"] == "Create" ->
%Object{} = object = Object.normalize(activity) %Object{} = object = Object.normalize(activity)
RedirectController.redirector_with_meta( RedirectController.redirector_with_meta(
@ -94,11 +97,8 @@ def notice(%{assigns: %{format: format}} = conn, %{"id" => id}) do
} }
) )
format == "html" ->
RedirectController.redirector(conn, nil)
true -> true ->
represent_activity(conn, format, activity, user) RedirectController.redirector(conn, nil)
end end
else else
reason when reason in [{:public?, false}, {:activity, nil}] -> reason when reason in [{:public?, false}, {:activity, nil}] ->
@ -135,24 +135,6 @@ def notice_player(conn, %{"id" => id}) do
end end
end end
defp represent_activity(
conn,
"activity+json",
%Activity{data: %{"type" => "Create"}} = activity,
_user
) do
object = Object.normalize(activity)
conn
|> put_resp_header("content-type", "application/activity+json")
|> put_view(ObjectView)
|> render("object.json", %{object: object})
end
defp represent_activity(_conn, _, _, _) do
{:error, :not_found}
end
def errors(conn, {:error, :not_found}) do def errors(conn, {:error, :not_found}) do
render_error(conn, :not_found, "Not found") render_error(conn, :not_found, "Not found")
end end

View File

@ -178,6 +178,11 @@ defmodule Pleroma.Web.Router do
get("/users/:nickname", AdminAPIController, :user_show) get("/users/:nickname", AdminAPIController, :user_show)
get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses) get("/users/:nickname/statuses", AdminAPIController, :list_user_statuses)
get("/instances/:instance/statuses", AdminAPIController, :list_instance_statuses)
patch("/users/confirm_email", AdminAPIController, :confirm_email)
patch("/users/resend_confirmation_email", AdminAPIController, :resend_confirmation_email)
get("/reports", AdminAPIController, :list_reports) get("/reports", AdminAPIController, :list_reports)
get("/grouped_reports", AdminAPIController, :list_grouped_reports) get("/grouped_reports", AdminAPIController, :list_grouped_reports)
get("/reports/:id", AdminAPIController, :report_show) get("/reports/:id", AdminAPIController, :report_show)

22
mix.exs
View File

@ -194,25 +194,19 @@ defp version(version) do
identifier_filter = ~r/[^0-9a-z\-]+/i identifier_filter = ~r/[^0-9a-z\-]+/i
# Pre-release version, denoted from patch version with a hyphen # Pre-release version, denoted from patch version with a hyphen
{git_tag, git_pre_release} = git_pre_release =
with {tag, 0} <- with {tag, 0} <-
System.cmd("git", ["describe", "--tags", "--abbrev=0"], stderr_to_stdout: true), System.cmd("git", ["describe", "--tags", "--abbrev=0"], stderr_to_stdout: true),
tag = String.trim(tag), {describe, 0} <- System.cmd("git", ["describe", "--tags", "--abbrev=8"]) do
{describe, 0} <- System.cmd("git", ["describe", "--tags", "--abbrev=8"]), describe
describe = String.trim(describe), |> String.trim()
ahead <- String.replace(describe, tag, ""), |> String.replace(String.trim(tag), "")
ahead <- String.trim_leading(ahead, "-") do |> String.trim_leading("-")
{String.replace_prefix(tag, "v", ""), if(ahead != "", do: String.trim(ahead))} |> String.trim()
else else
_ -> _ ->
{commit_hash, 0} = System.cmd("git", ["rev-parse", "--short", "HEAD"]) {commit_hash, 0} = System.cmd("git", ["rev-parse", "--short", "HEAD"])
{nil, "0-g" <> String.trim(commit_hash)} "0-g" <> String.trim(commit_hash)
end
if git_tag && version != git_tag do
Mix.shell().error(
"Application version #{inspect(version)} does not match git tag #{inspect(git_tag)}"
)
end end
# Branch name as pre-release version component, denoted with a dot # Branch name as pre-release version component, denoted with a dot

View File

@ -0,0 +1,22 @@
defmodule Pleroma.Repo.Migrations.SetVisibleServiceActors do
use Ecto.Migration
import Ecto.Query
alias Pleroma.Repo
def up do
user_nicknames = ["relay", "internal.fetch"]
from(
u in "users",
where: u.nickname in ^user_nicknames,
update: [
set: [invisible: true]
]
)
|> Repo.update_all([])
end
def down do
:ok
end
end

View File

@ -8,5 +8,5 @@
# fi # fi
# Set the release to work across nodes # Set the release to work across nodes
export RELEASE_DISTRIBUTION=name export RELEASE_DISTRIBUTION="${RELEASE_DISTRIBUTION:-name}"
export RELEASE_NODE=<%= @release.name %>@127.0.0.1 export RELEASE_NODE="${RELEASE_NODE:-<%= @release.name %>@127.0.0.1}"

View File

@ -0,0 +1,47 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.FollowingRelationshipTest do
use Pleroma.DataCase
alias Pleroma.FollowingRelationship
alias Pleroma.Web.ActivityPub.InternalFetchActor
alias Pleroma.Web.ActivityPub.Relay
import Pleroma.Factory
describe "following/1" do
test "returns following addresses without internal.fetch" do
user = insert(:user)
fetch_actor = InternalFetchActor.get_actor()
FollowingRelationship.follow(fetch_actor, user, "accept")
assert FollowingRelationship.following(fetch_actor) == [user.follower_address]
end
test "returns following addresses without relay" do
user = insert(:user)
relay_actor = Relay.get_actor()
FollowingRelationship.follow(relay_actor, user, "accept")
assert FollowingRelationship.following(relay_actor) == [user.follower_address]
end
test "returns following addresses without remote user" do
user = insert(:user)
actor = insert(:user, local: false)
FollowingRelationship.follow(actor, user, "accept")
assert FollowingRelationship.following(actor) == [user.follower_address]
end
test "returns following addresses with local user" do
user = insert(:user)
actor = insert(:user, local: true)
FollowingRelationship.follow(actor, user, "accept")
assert FollowingRelationship.following(actor) == [
actor.follower_address,
user.follower_address
]
end
end
end

View File

@ -16,11 +16,21 @@ test "don't send pleroma user agent" do
test "send pleroma user agent" do test "send pleroma user agent" do
Pleroma.Config.put([:http, :send_user_agent], true) Pleroma.Config.put([:http, :send_user_agent], true)
Pleroma.Config.put([:http, :user_agent], :default)
assert RequestBuilder.headers(%{}, []) == %{ assert RequestBuilder.headers(%{}, []) == %{
headers: [{"User-Agent", Pleroma.Application.user_agent()}] headers: [{"User-Agent", Pleroma.Application.user_agent()}]
} }
end end
test "send custom user agent" do
Pleroma.Config.put([:http, :send_user_agent], true)
Pleroma.Config.put([:http, :user_agent], "totally-not-pleroma")
assert RequestBuilder.headers(%{}, []) == %{
headers: [{"User-Agent", "totally-not-pleroma"}]
}
end
end end
describe "add_optional_params/3" do describe "add_optional_params/3" do

View File

@ -15,6 +15,14 @@ defmodule Pleroma.UserSearchTest do
end end
describe "User.search" do describe "User.search" do
test "excluded invisible users from results" do
user = insert(:user, %{nickname: "john t1000"})
insert(:user, %{invisible: true, nickname: "john t800"})
[found_user] = User.search("john")
assert found_user.id == user.id
end
test "accepts limit parameter" do test "accepts limit parameter" do
Enum.each(0..4, &insert(:user, %{nickname: "john#{&1}"})) Enum.each(0..4, &insert(:user, %{nickname: "john#{&1}"}))
assert length(User.search("john", limit: 3)) == 3 assert length(User.search("john", limit: 3)) == 3

View File

@ -25,6 +25,25 @@ defmodule Pleroma.UserTest do
clear_config([:instance, :account_activation_required]) clear_config([:instance, :account_activation_required])
describe "service actors" do
test "returns invisible actor" do
uri = "#{Pleroma.Web.Endpoint.url()}/internal/fetch-test"
followers_uri = "#{uri}/followers"
user = User.get_or_create_service_actor_by_ap_id(uri, "internal.fetch-test")
assert %User{
nickname: "internal.fetch-test",
invisible: true,
local: true,
ap_id: ^uri,
follower_address: ^followers_uri
} = user
user2 = User.get_or_create_service_actor_by_ap_id(uri, "internal.fetch-test")
assert user.id == user2.id
end
end
describe "when tags are nil" do describe "when tags are nil" do
test "tagging a user" do test "tagging a user" do
user = insert(:user, %{tags: nil}) user = insert(:user, %{tags: nil})
@ -148,9 +167,10 @@ test "follow takes a user and another user" do
{:ok, user} = User.follow(user, followed) {:ok, user} = User.follow(user, followed)
user = User.get_cached_by_id(user.id) user = User.get_cached_by_id(user.id)
followed = User.get_cached_by_ap_id(followed.ap_id) followed = User.get_cached_by_ap_id(followed.ap_id)
assert followed.follower_count == 1 assert followed.follower_count == 1
assert user.following_count == 1
assert User.ap_followers(followed) in User.following(user) assert User.ap_followers(followed) in User.following(user)
end end
@ -952,12 +972,14 @@ test "hide a user from friends" do
user2 = insert(:user) user2 = insert(:user)
{:ok, user2} = User.follow(user2, user) {:ok, user2} = User.follow(user2, user)
assert user2.following_count == 1
assert User.following_count(user2) == 1 assert User.following_count(user2) == 1
{:ok, _user} = User.deactivate(user) {:ok, _user} = User.deactivate(user)
info = User.get_cached_user_info(user2) info = User.get_cached_user_info(user2)
assert refresh_record(user2).following_count == 0
assert info.following_count == 0 assert info.following_count == 0
assert User.following_count(user2) == 0 assert User.following_count(user2) == 0
assert [] = User.get_friends(user2) assert [] = User.get_friends(user2)

View File

@ -110,6 +110,19 @@ test "it returns a json representation of the user with accept application/ld+js
assert json_response(conn, 200) == UserView.render("user.json", %{user: user}) assert json_response(conn, 200) == UserView.render("user.json", %{user: user})
end end
test "it returns 404 for remote users", %{
conn: conn
} do
user = insert(:user, local: false, nickname: "remoteuser@example.com")
conn =
conn
|> put_req_header("accept", "application/json")
|> get("/users/#{user.nickname}.json")
assert json_response(conn, 404)
end
end end
describe "/object/:uuid" do describe "/object/:uuid" do

View File

@ -226,7 +226,8 @@ test "Show", %{conn: conn} do
"roles" => %{"admin" => false, "moderator" => false}, "roles" => %{"admin" => false, "moderator" => false},
"tags" => [], "tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(), "avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname) "display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
} }
assert expected == json_response(conn, 200) assert expected == json_response(conn, 200)
@ -635,7 +636,8 @@ test "renders users array for the first page", %{conn: conn, admin: admin} do
"local" => true, "local" => true,
"tags" => [], "tags" => [],
"avatar" => User.avatar_url(admin) |> MediaProxy.url(), "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(admin.name || admin.nickname) "display_name" => HTML.strip_tags(admin.name || admin.nickname),
"confirmation_pending" => false
}, },
%{ %{
"deactivated" => user.deactivated, "deactivated" => user.deactivated,
@ -645,7 +647,8 @@ test "renders users array for the first page", %{conn: conn, admin: admin} do
"local" => false, "local" => false,
"tags" => ["foo", "bar"], "tags" => ["foo", "bar"],
"avatar" => User.avatar_url(user) |> MediaProxy.url(), "avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname) "display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
} }
] ]
|> Enum.sort_by(& &1["nickname"]) |> Enum.sort_by(& &1["nickname"])
@ -686,7 +689,8 @@ test "regular search", %{conn: conn} do
"local" => true, "local" => true,
"tags" => [], "tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(), "avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname) "display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
} }
] ]
} }
@ -710,7 +714,8 @@ test "search by domain", %{conn: conn} do
"local" => true, "local" => true,
"tags" => [], "tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(), "avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname) "display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
} }
] ]
} }
@ -734,7 +739,8 @@ test "search by full nickname", %{conn: conn} do
"local" => true, "local" => true,
"tags" => [], "tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(), "avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname) "display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
} }
] ]
} }
@ -758,7 +764,8 @@ test "search by display name", %{conn: conn} do
"local" => true, "local" => true,
"tags" => [], "tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(), "avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname) "display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
} }
] ]
} }
@ -782,7 +789,8 @@ test "search by email", %{conn: conn} do
"local" => true, "local" => true,
"tags" => [], "tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(), "avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname) "display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
} }
] ]
} }
@ -806,7 +814,8 @@ test "regular search with page size", %{conn: conn} do
"local" => true, "local" => true,
"tags" => [], "tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(), "avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname) "display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
} }
] ]
} }
@ -825,7 +834,8 @@ test "regular search with page size", %{conn: conn} do
"local" => true, "local" => true,
"tags" => [], "tags" => [],
"avatar" => User.avatar_url(user2) |> MediaProxy.url(), "avatar" => User.avatar_url(user2) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user2.name || user2.nickname) "display_name" => HTML.strip_tags(user2.name || user2.nickname),
"confirmation_pending" => false
} }
] ]
} }
@ -854,7 +864,8 @@ test "only local users" do
"local" => true, "local" => true,
"tags" => [], "tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(), "avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname) "display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
} }
] ]
} }
@ -881,7 +892,8 @@ test "only local users with no query", %{admin: old_admin} do
"local" => true, "local" => true,
"tags" => [], "tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(), "avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname) "display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
}, },
%{ %{
"deactivated" => admin.deactivated, "deactivated" => admin.deactivated,
@ -891,7 +903,8 @@ test "only local users with no query", %{admin: old_admin} do
"local" => true, "local" => true,
"tags" => [], "tags" => [],
"avatar" => User.avatar_url(admin) |> MediaProxy.url(), "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(admin.name || admin.nickname) "display_name" => HTML.strip_tags(admin.name || admin.nickname),
"confirmation_pending" => false
}, },
%{ %{
"deactivated" => false, "deactivated" => false,
@ -901,7 +914,8 @@ test "only local users with no query", %{admin: old_admin} do
"roles" => %{"admin" => true, "moderator" => false}, "roles" => %{"admin" => true, "moderator" => false},
"tags" => [], "tags" => [],
"avatar" => User.avatar_url(old_admin) |> MediaProxy.url(), "avatar" => User.avatar_url(old_admin) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(old_admin.name || old_admin.nickname) "display_name" => HTML.strip_tags(old_admin.name || old_admin.nickname),
"confirmation_pending" => false
} }
] ]
|> Enum.sort_by(& &1["nickname"]) |> Enum.sort_by(& &1["nickname"])
@ -930,7 +944,8 @@ test "load only admins", %{conn: conn, admin: admin} do
"local" => admin.local, "local" => admin.local,
"tags" => [], "tags" => [],
"avatar" => User.avatar_url(admin) |> MediaProxy.url(), "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(admin.name || admin.nickname) "display_name" => HTML.strip_tags(admin.name || admin.nickname),
"confirmation_pending" => false
}, },
%{ %{
"deactivated" => false, "deactivated" => false,
@ -940,7 +955,8 @@ test "load only admins", %{conn: conn, admin: admin} do
"local" => second_admin.local, "local" => second_admin.local,
"tags" => [], "tags" => [],
"avatar" => User.avatar_url(second_admin) |> MediaProxy.url(), "avatar" => User.avatar_url(second_admin) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(second_admin.name || second_admin.nickname) "display_name" => HTML.strip_tags(second_admin.name || second_admin.nickname),
"confirmation_pending" => false
} }
] ]
|> Enum.sort_by(& &1["nickname"]) |> Enum.sort_by(& &1["nickname"])
@ -971,7 +987,8 @@ test "load only moderators", %{conn: conn} do
"local" => moderator.local, "local" => moderator.local,
"tags" => [], "tags" => [],
"avatar" => User.avatar_url(moderator) |> MediaProxy.url(), "avatar" => User.avatar_url(moderator) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(moderator.name || moderator.nickname) "display_name" => HTML.strip_tags(moderator.name || moderator.nickname),
"confirmation_pending" => false
} }
] ]
} }
@ -995,7 +1012,8 @@ test "load users with tags list", %{conn: conn} do
"local" => user1.local, "local" => user1.local,
"tags" => ["first"], "tags" => ["first"],
"avatar" => User.avatar_url(user1) |> MediaProxy.url(), "avatar" => User.avatar_url(user1) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user1.name || user1.nickname) "display_name" => HTML.strip_tags(user1.name || user1.nickname),
"confirmation_pending" => false
}, },
%{ %{
"deactivated" => false, "deactivated" => false,
@ -1005,7 +1023,8 @@ test "load users with tags list", %{conn: conn} do
"local" => user2.local, "local" => user2.local,
"tags" => ["second"], "tags" => ["second"],
"avatar" => User.avatar_url(user2) |> MediaProxy.url(), "avatar" => User.avatar_url(user2) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user2.name || user2.nickname) "display_name" => HTML.strip_tags(user2.name || user2.nickname),
"confirmation_pending" => false
} }
] ]
|> Enum.sort_by(& &1["nickname"]) |> Enum.sort_by(& &1["nickname"])
@ -1041,7 +1060,8 @@ test "it works with multiple filters" do
"local" => user.local, "local" => user.local,
"tags" => [], "tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(), "avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname) "display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
} }
] ]
} }
@ -1067,7 +1087,8 @@ test "it omits relay user", %{admin: admin} do
"local" => true, "local" => true,
"tags" => [], "tags" => [],
"avatar" => User.avatar_url(admin) |> MediaProxy.url(), "avatar" => User.avatar_url(admin) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(admin.name || admin.nickname) "display_name" => HTML.strip_tags(admin.name || admin.nickname),
"confirmation_pending" => false
} }
] ]
} }
@ -1136,7 +1157,8 @@ test "PATCH /api/pleroma/admin/users/:nickname/toggle_activation" do
"local" => true, "local" => true,
"tags" => [], "tags" => [],
"avatar" => User.avatar_url(user) |> MediaProxy.url(), "avatar" => User.avatar_url(user) |> MediaProxy.url(),
"display_name" => HTML.strip_tags(user.name || user.nickname) "display_name" => HTML.strip_tags(user.name || user.nickname),
"confirmation_pending" => false
} }
log_entry = Repo.one(ModerationLog) log_entry = Repo.one(ModerationLog)
@ -2833,6 +2855,105 @@ test "DELETE /relay", %{admin: admin} do
"@#{admin.nickname} unfollowed relay: http://mastodon.example.org/users/admin" "@#{admin.nickname} unfollowed relay: http://mastodon.example.org/users/admin"
end end
end end
describe "instances" do
test "GET /instances/:instance/statuses" do
admin = insert(:user, is_admin: true)
user = insert(:user, local: false, nickname: "archaeme@archae.me")
user2 = insert(:user, local: false, nickname: "test@test.com")
insert_pair(:note_activity, user: user)
insert(:note_activity, user: user2)
conn =
build_conn()
|> assign(:user, admin)
|> get("/api/pleroma/admin/instances/archae.me/statuses")
response = json_response(conn, 200)
assert length(response) == 2
conn =
build_conn()
|> assign(:user, admin)
|> get("/api/pleroma/admin/instances/test.com/statuses")
response = json_response(conn, 200)
assert length(response) == 1
conn =
build_conn()
|> assign(:user, admin)
|> get("/api/pleroma/admin/instances/nonexistent.com/statuses")
response = json_response(conn, 200)
assert length(response) == 0
end
end
describe "PATCH /confirm_email" do
setup %{conn: conn} do
admin = insert(:user, is_admin: true)
%{conn: assign(conn, :user, admin), admin: admin}
end
test "it confirms emails of two users", %{admin: admin} do
[first_user, second_user] = insert_pair(:user, confirmation_pending: true)
assert first_user.confirmation_pending == true
assert second_user.confirmation_pending == true
build_conn()
|> assign(:user, admin)
|> patch("/api/pleroma/admin/users/confirm_email", %{
nicknames: [
first_user.nickname,
second_user.nickname
]
})
assert first_user.confirmation_pending == true
assert second_user.confirmation_pending == true
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} confirmed email for users: @#{first_user.nickname}, @#{
second_user.nickname
}"
end
end
describe "PATCH /resend_confirmation_email" do
setup %{conn: conn} do
admin = insert(:user, is_admin: true)
%{conn: assign(conn, :user, admin), admin: admin}
end
test "it resend emails for two users", %{admin: admin} do
[first_user, second_user] = insert_pair(:user, confirmation_pending: true)
build_conn()
|> assign(:user, admin)
|> patch("/api/pleroma/admin/users/resend_confirmation_email", %{
nicknames: [
first_user.nickname,
second_user.nickname
]
})
log_entry = Repo.one(ModerationLog)
assert ModerationLog.get_log_entry_message(log_entry) ==
"@#{admin.nickname} re-sent confirmation email for users: @#{first_user.nickname}, @#{
second_user.nickname
}"
end
end
end end
# Needed for testing # Needed for testing

View File

@ -8,6 +8,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.InternalFetchActor
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.OAuth.Token alias Pleroma.Web.OAuth.Token
@ -118,6 +119,28 @@ test "accounts fetches correct account for nicknames beginning with numbers", %{
refute acc_one == acc_two refute acc_one == acc_two
assert acc_two == acc_three assert acc_two == acc_three
end end
test "returns 404 when user is invisible", %{conn: conn} do
user = insert(:user, %{invisible: true})
resp =
conn
|> get("/api/v1/accounts/#{user.nickname}")
|> json_response(404)
assert %{"error" => "Can't find user"} = resp
end
test "returns 404 for internal.fetch actor", %{conn: conn} do
%User{nickname: "internal.fetch"} = InternalFetchActor.get_actor()
resp =
conn
|> get("/api/v1/accounts/internal.fetch")
|> json_response(404)
assert %{"error" => "Can't find user"} = resp
end
end end
describe "user timelines" do describe "user timelines" do

View File

@ -35,23 +35,6 @@ test "redirects to /notice/id for html format", %{conn: conn} do
assert redirected_to(conn) == "/notice/#{note_activity.id}" assert redirected_to(conn) == "/notice/#{note_activity.id}"
end end
test "500s when user not found", %{conn: conn} do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = User.get_cached_by_ap_id(note_activity.data["actor"])
User.invalidate_cache(user)
Pleroma.Repo.delete(user)
[_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, object.data["id"]))
url = "/objects/#{uuid}"
conn =
conn
|> put_req_header("accept", "application/xml")
|> get(url)
assert response(conn, 500) == ~S({"error":"Something went wrong"})
end
test "404s on private objects", %{conn: conn} do test "404s on private objects", %{conn: conn} do
note_activity = insert(:direct_note_activity) note_activity = insert(:direct_note_activity)
object = Object.normalize(note_activity) object = Object.normalize(note_activity)
@ -82,21 +65,6 @@ test "redirects to /notice/id for html format", %{conn: conn} do
assert redirected_to(conn) == "/notice/#{note_activity.id}" assert redirected_to(conn) == "/notice/#{note_activity.id}"
end end
test "505s when user not found", %{conn: conn} do
note_activity = insert(:note_activity)
[_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
user = User.get_cached_by_ap_id(note_activity.data["actor"])
User.invalidate_cache(user)
Pleroma.Repo.delete(user)
conn =
conn
|> put_req_header("accept", "text/html")
|> get("/activities/#{uuid}")
assert response(conn, 500) == ~S({"error":"Something went wrong"})
end
test "404s on private activities", %{conn: conn} do test "404s on private activities", %{conn: conn} do
note_activity = insert(:direct_note_activity) note_activity = insert(:direct_note_activity)
[_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"])) [_, uuid] = hd(Regex.scan(~r/.+\/([\w-]+)$/, note_activity.data["id"]))
@ -127,21 +95,28 @@ test "gets an activity in AS2 format", %{conn: conn} do
end end
describe "GET notice/2" do describe "GET notice/2" do
test "gets a notice in xml format", %{conn: conn} do test "redirects to a proper object URL when json requested and the object is local", %{
conn: conn
} do
note_activity = insert(:note_activity) note_activity = insert(:note_activity)
expected_redirect_url = Object.normalize(note_activity).data["id"]
redirect_url =
conn conn
|> put_req_header("accept", "application/activity+json")
|> get("/notice/#{note_activity.id}") |> get("/notice/#{note_activity.id}")
|> response(200) |> redirected_to()
assert redirect_url == expected_redirect_url
end end
test "gets a notice in AS2 format", %{conn: conn} do test "returns a 404 on remote notice when json requested", %{conn: conn} do
note_activity = insert(:note_activity) note_activity = insert(:note_activity, local: false)
conn conn
|> put_req_header("accept", "application/activity+json") |> put_req_header("accept", "application/activity+json")
|> get("/notice/#{note_activity.id}") |> get("/notice/#{note_activity.id}")
|> json_response(200) |> response(404)
end end
test "500s when actor not found", %{conn: conn} do test "500s when actor not found", %{conn: conn} do
@ -157,32 +132,6 @@ test "500s when actor not found", %{conn: conn} do
assert response(conn, 500) == ~S({"error":"Something went wrong"}) assert response(conn, 500) == ~S({"error":"Something went wrong"})
end end
test "only gets a notice in AS2 format for Create messages", %{conn: conn} do
note_activity = insert(:note_activity)
url = "/notice/#{note_activity.id}"
conn =
conn
|> put_req_header("accept", "application/activity+json")
|> get(url)
assert json_response(conn, 200)
user = insert(:user)
{:ok, like_activity, _} = CommonAPI.favorite(note_activity.id, user)
url = "/notice/#{like_activity.id}"
assert like_activity.data["type"] == "Like"
conn =
build_conn()
|> put_req_header("accept", "application/activity+json")
|> get(url)
assert response(conn, 404)
end
test "render html for redirect for html format", %{conn: conn} do test "render html for redirect for html format", %{conn: conn} do
note_activity = insert(:note_activity) note_activity = insert(:note_activity)