Merge branch 'develop' into issue/1411

This commit is contained in:
Maksim Pechnikov 2019-12-01 16:18:16 +03:00
commit 88f7cf51d4
77 changed files with 1233 additions and 389 deletions

View file

@ -5,7 +5,6 @@ CC-BY-SA-4.0
COPYING COPYING
*file *file
elixir_buildpack.config elixir_buildpack.config
docs/
test/ test/
# Required to get version # Required to get version

View file

@ -31,6 +31,7 @@ build:
benchmark: benchmark:
stage: benchmark stage: benchmark
when: manual
variables: variables:
MIX_ENV: benchmark MIX_ENV: benchmark
services: services:
@ -55,6 +56,19 @@ unit-testing:
- mix ecto.migrate - mix ecto.migrate
- mix coveralls --preload-modules - mix coveralls --preload-modules
federated-testing:
stage: test
services:
- name: minibikini/postgres-with-rum:12
alias: postgres
command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"]
script:
- mix deps.get
- mix ecto.create
- mix ecto.migrate
- epmd -daemon
- mix test --trace --only federated
unit-testing-rum: unit-testing-rum:
stage: test stage: test
services: services:

View file

@ -30,6 +30,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Admin API: Return `total` when querying for reports - Admin API: Return `total` when querying for reports
- Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`) - Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`)
- Admin API: Return link alongside with token on password reset - Admin API: Return link alongside with token on password reset
- Admin API: Support authentication via `x-admin-token` HTTP header
- Mastodon API: Add `pleroma.direct_conversation_id` to the status endpoint (`GET /api/v1/statuses/:id`) - Mastodon API: Add `pleroma.direct_conversation_id` to the status endpoint (`GET /api/v1/statuses/:id`)
- Mastodon API: `pleroma.thread_muted` to the Status entity - Mastodon API: `pleroma.thread_muted` to the Status entity
- Mastodon API: Mark the direct conversation as read for the author when they send a new direct message - Mastodon API: Mark the direct conversation as read for the author when they send a new direct message
@ -41,6 +42,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>
@ -68,6 +70,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
@ -78,8 +82,25 @@ 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`
- Admin API: Error when trying to update reports in the "old" format
</details> </details>
## [1.1.6] - 2019-11-19
### Fixed
- Not being able to log into to third party apps when the browser is logged into mastofe
- Email confirmation not being required even when enabled
- Mastodon API: conversations API crashing when one status is malformed
### Bundled Pleroma-FE Changes
#### Added
- About page
- Meme arrows
#### Fixed
- Image modal not closing unless clicked outside of image
- Attachment upload spinner not being centered
- Showing follow counters being 0 when they are actually hidden
## [1.1.5] - 2019-11-09 ## [1.1.5] - 2019-11-09
### Fixed ### Fixed
- Polls having different numbers in timelines/notifications/poll api endpoints due to cache desyncronization - Polls having different numbers in timelines/notifications/poll api endpoints due to cache desyncronization

View file

@ -95,7 +95,36 @@ def query_timelines(user) do
for: user, for: user,
as: :activity as: :activity
}) })
end end,
"Rendering favorites timeline" => fn ->
conn = Phoenix.ConnTest.build_conn(:get, "http://localhost:4001/api/v1/favourites", nil)
Pleroma.Web.MastodonAPI.StatusController.favourites(
%Plug.Conn{conn |
assigns: %{user: user},
query_params: %{"limit" => "0"},
body_params: %{},
cookies: %{},
params: %{},
path_params: %{},
private: %{
Pleroma.Web.Router => {[], %{}},
phoenix_router: Pleroma.Web.Router,
phoenix_action: :favourites,
phoenix_controller: Pleroma.Web.MastodonAPI.StatusController,
phoenix_endpoint: Pleroma.Web.Endpoint,
phoenix_format: "json",
phoenix_layout: {Pleroma.Web.LayoutView, "app.html"},
phoenix_recycled: true,
phoenix_view: Pleroma.Web.MastodonAPI.StatusView,
plug_session: %{"user_id" => user.id},
plug_session_fetch: :done,
plug_session_info: :write,
plug_skip_csrf_protection: true
}
},
%{})
end,
}) })
end end

View file

@ -2,6 +2,24 @@ defmodule Pleroma.LoadTesting.Generator do
use Pleroma.LoadTesting.Helper use Pleroma.LoadTesting.Helper
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
def generate_like_activities(user, posts) do
count_likes = Kernel.trunc(length(posts) / 4)
IO.puts("Starting generating #{count_likes} like activities...")
{time, _} =
:timer.tc(fn ->
Task.async_stream(
Enum.take_random(posts, count_likes),
fn post -> {:ok, _, _} = CommonAPI.favorite(post.id, user) end,
max_concurrency: 10,
timeout: 30_000
)
|> Stream.run()
end)
IO.puts("Inserting like activities take #{to_sec(time)} sec.\n")
end
def generate_users(opts) do def generate_users(opts) do
IO.puts("Starting generating #{opts[:users_max]} users...") IO.puts("Starting generating #{opts[:users_max]} users...")
{time, _} = :timer.tc(fn -> do_generate_users(opts) end) {time, _} = :timer.tc(fn -> do_generate_users(opts) end)
@ -31,7 +49,6 @@ defp generate_user_data(i) do
password_hash: password_hash:
"$pbkdf2-sha512$160000$bU.OSFI7H/yqWb5DPEqyjw$uKp/2rmXw12QqnRRTqTtuk2DTwZfF8VR4MYW2xMeIlqPR/UX1nT1CEKVUx2CowFMZ5JON8aDvURrZpJjSgqXrg", "$pbkdf2-sha512$160000$bU.OSFI7H/yqWb5DPEqyjw$uKp/2rmXw12QqnRRTqTtuk2DTwZfF8VR4MYW2xMeIlqPR/UX1nT1CEKVUx2CowFMZ5JON8aDvURrZpJjSgqXrg",
bio: "Tester Number #{i}", bio: "Tester Number #{i}",
info: %{},
local: remote local: remote
} }

View file

@ -100,6 +100,10 @@ def run(args) do
generate_remote_activities(user, remote_users) generate_remote_activities(user, remote_users)
generate_like_activities(
user, Pleroma.Repo.all(Pleroma.Activity.Queries.by_type("Create"))
)
generate_dms(user, users, opts) generate_dms(user, users, opts)
{:ok, activity} = generate_long_thread(user, users, opts) {:ok, activity} = generate_long_thread(user, users, opts)

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,8 @@
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
config :logger, :ex_syslogger, 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

@ -235,14 +235,6 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
} }
``` ```
## DEPRECATED `PATCH /api/pleroma/admin/users/:nickname/activation_status`
### Active or deactivate a user
- Params:
- `nickname`
- `status` BOOLEAN field, false value means deactivation.
## `GET /api/pleroma/admin/users/:nickname_or_id` ## `GET /api/pleroma/admin/users/:nickname_or_id`
### Retrive the details of a user ### Retrive the details of a user
@ -878,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.
@ -656,7 +666,7 @@ Feel free to adjust the priv_dir and port number. Then you will have to create t
### :admin_token ### :admin_token
Allows to set a token that can be used to authenticate with the admin api without using an actual user by giving it as the 'admin_token' parameter. Example: Allows to set a token that can be used to authenticate with the admin api without using an actual user by giving it as the `admin_token` parameter or `x-admin-token` HTTP header. Example:
```elixir ```elixir
config :pleroma, :admin_token, "somerandomtoken" config :pleroma, :admin_token, "somerandomtoken"
@ -664,8 +674,14 @@ config :pleroma, :admin_token, "somerandomtoken"
You can then do You can then do
```sh ```shell
curl "http://localhost:4000/api/pleroma/admin/invite_token?admin_token=somerandomtoken" curl "http://localhost:4000/api/pleroma/admin/users/invites?admin_token=somerandomtoken"
```
or
```shell
curl -H "X-Admin-Token: somerandomtoken" "http://localhost:4000/api/pleroma/admin/users/invites"
``` ```
### :auth ### :auth

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

@ -303,4 +303,17 @@ def restrict_deactivated_users(query) do
end end
defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search defdelegate search(user, query, options \\ []), to: Pleroma.Activity.Search
def direct_conversation_id(activity, for_user) do
alias Pleroma.Conversation.Participation
with %{data: %{"context" => context}} when is_binary(context) <- activity,
%Pleroma.Conversation{} = conversation <- Pleroma.Conversation.get_for_ap_id(context),
%Participation{id: participation_id} <-
Participation.for_user_and_conversation(for_user, conversation) do
participation_id
else
_ -> nil
end
end
end end

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
info = "#{Pleroma.Web.base_url()} <#{Pleroma.Config.get([:instance, :email], "")}>" case Pleroma.Config.get([:http, :user_agent], :default) do
named_version() <> "; " <> info :default ->
info = "#{Pleroma.Web.base_url()} <#{Pleroma.Config.get([:instance, :email], "")}>"
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

@ -91,6 +91,7 @@ def extract_first_external_url(object, content) do
Cachex.fetch!(:scrubber_cache, key, fn _key -> Cachex.fetch!(:scrubber_cache, key, fn _key ->
result = result =
content content
|> HtmlEntities.decode()
|> Floki.filter_out("a.mention,a.hashtag,a[rel~=\"tag\"]") |> Floki.filter_out("a.mention,a.hashtag,a[rel~=\"tag\"]")
|> Floki.attribute("a", "href") |> Floki.attribute("a", "href")
|> Enum.at(0) |> Enum.at(0)

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

@ -80,7 +80,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))}")
@ -272,4 +272,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

@ -16,14 +16,28 @@ def secret_token do
def call(%{assigns: %{user: %User{}}} = conn, _), do: conn def call(%{assigns: %{user: %User{}}} = conn, _), do: conn
def call(%{params: %{"admin_token" => admin_token}} = conn, _) do def call(conn, _) do
if secret_token() && admin_token == secret_token() do if secret_token() do
conn authenticate(conn)
|> assign(:user, %User{is_admin: true})
else else
conn conn
end end
end end
def call(conn, _), do: conn def authenticate(%{params: %{"admin_token" => admin_token}} = conn) do
if admin_token == secret_token() do
assign(conn, :user, %User{is_admin: true})
else
conn
end
end
def authenticate(conn) do
token = secret_token()
case get_req_header(conn, "x-admin-token") do
[^token] -> assign(conn, :user, %User{is_admin: true})
_ -> conn
end
end
end end

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)
@ -119,8 +118,6 @@ defmodule Pleroma.User do
has_many(:registrations, Registration) has_many(:registrations, Registration)
has_many(:deliveries, Delivery) has_many(:deliveries, Delivery)
field(:info, :map, default: %{})
timestamps() timestamps()
end end
@ -134,6 +131,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
@ -176,22 +175,6 @@ def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa 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
following_count =
Map.get(args, :following_count, user.following_count || following_count(user))
follower_count = Map.get(args, :follower_count, user.follower_count)
%{
note_count: user.note_count,
locked: user.locked,
confirmation_pending: user.confirmation_pending,
default_scope: user.default_scope
}
|> Map.put(:following_count, following_count)
|> Map.put(:follower_count, follower_count)
end
def follow_state(%User{} = user, %User{} = target) do def follow_state(%User{} = user, %User{} = target) do
case Utils.fetch_latest_follow(user, target) do case Utils.fetch_latest_follow(user, target) do
%{data: %{"state" => state}} -> state %{data: %{"state" => state}} -> state
@ -210,10 +193,6 @@ def set_follow_state_cache(user_ap_id, target_ap_id, state) do
Cachex.put(:user_cache, "follow_state:#{user_ap_id}|#{target_ap_id}", state) Cachex.put(:user_cache, "follow_state:#{user_ap_id}|#{target_ap_id}", state)
end end
def set_info_cache(user, args) do
Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user, args))
end
@spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t() @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
def restrict_deactivated(query) do def restrict_deactivated(query) do
from(u in query, where: u.deactivated != ^true) from(u in query, where: u.deactivated != ^true)
@ -244,7 +223,6 @@ def remote_user_creation(params) do
params = params =
params params
|> Map.put(:info, params[:info] || %{})
|> truncate_if_exists(:name, name_limit) |> truncate_if_exists(:name, name_limit)
|> truncate_if_exists(:bio, bio_limit) |> truncate_if_exists(:bio, bio_limit)
|> truncate_fields_param() |> truncate_fields_param()
@ -492,6 +470,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 +504,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 +526,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 +538,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
@ -615,7 +593,6 @@ def set_cache({:error, err}), do: {:error, err}
def set_cache(%User{} = user) do def set_cache(%User{} = user) do
Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user) Cachex.put(:user_cache, "ap_id:#{user.ap_id}", user)
Cachex.put(:user_cache, "nickname:#{user.nickname}", user) Cachex.put(:user_cache, "nickname:#{user.nickname}", user)
Cachex.put(:user_cache, "user_info:#{user.id}", user_info(user))
{:ok, user} {:ok, user}
end end
@ -634,7 +611,6 @@ def update_and_set_cache(changeset) do
def invalidate_cache(user) do def invalidate_cache(user) do
Cachex.del(:user_cache, "ap_id:#{user.ap_id}") Cachex.del(:user_cache, "ap_id:#{user.ap_id}")
Cachex.del(:user_cache, "nickname:#{user.nickname}") Cachex.del(:user_cache, "nickname:#{user.nickname}")
Cachex.del(:user_cache, "user_info:#{user.id}")
end end
def get_cached_by_ap_id(ap_id) do def get_cached_by_ap_id(ap_id) do
@ -702,11 +678,6 @@ def get_by_nickname_or_email(nickname_or_email) do
get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email) get_by_nickname(nickname_or_email) || get_by_email(nickname_or_email)
end end
def get_cached_user_info(user) do
key = "user_info:#{user.id}"
Cachex.fetch!(:user_cache, key, fn -> user_info(user) end)
end
def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname) def fetch_by_nickname(nickname), do: ActivityPub.make_user_from_nickname(nickname)
def get_or_fetch_by_nickname(nickname) do def get_or_fetch_by_nickname(nickname) do
@ -895,8 +866,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 +875,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 +1074,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
@ -1226,7 +1208,7 @@ def external_users_query do
def external_users(opts \\ []) do def external_users(opts \\ []) do
query = query =
external_users_query() external_users_query()
|> select([u], struct(u, [:id, :ap_id, :info])) |> select([u], struct(u, [:id, :ap_id]))
query = query =
if opts[:max_id], if opts[:max_id],
@ -1317,22 +1299,23 @@ 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 {:ok, user} =
else %User{
_ -> invisible: true,
{:ok, user} = local: true,
%User{} ap_id: uri,
|> cast(%{}, [:ap_id, :nickname, :local]) nickname: nickname,
|> put_change(:ap_id, uri) follower_address: uri <> "/followers"
|> put_change(:nickname, nickname) }
|> put_change(:local, true) |> Repo.insert()
|> put_change(:follower_address, uri <> "/followers")
|> Repo.insert()
user user
end end
end end
@ -1575,6 +1558,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)
@ -1226,13 +1252,13 @@ defp maybe_update_follow_information(data) do
end end
end end
defp collection_private(data) do defp collection_private(%{"first" => first}) do
if is_map(data["first"]) and if is_map(first) and
data["first"]["type"] in ["CollectionPage", "OrderedCollectionPage"] do first["type"] in ["CollectionPage", "OrderedCollectionPage"] do
{:ok, false} {:ok, false}
else else
with {:ok, %{"type" => type}} when type in ["CollectionPage", "OrderedCollectionPage"] <- with {:ok, %{"type" => type}} when type in ["CollectionPage", "OrderedCollectionPage"] <-
Fetcher.fetch_and_contain_remote_object_from_id(data["first"]) do Fetcher.fetch_and_contain_remote_object_from_id(first) do
{:ok, false} {:ok, false}
else else
{:error, {:ok, %{status: code}}} when code in [401, 403] -> {:error, {:ok, %{status: code}}} when code in [401, 403] ->
@ -1247,6 +1273,8 @@ defp collection_private(data) do
end end
end end
defp collection_private(_data), do: {:ok, true}
def user_data_from_user_object(data) do def user_data_from_user_object(data) do
with {:ok, data} <- MRF.filter(data), with {:ok, data} <- MRF.filter(data),
{:ok, data} <- object_to_user_data(data) do {:ok, data} <- object_to_user_data(data) do

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

@ -903,7 +903,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

@ -71,18 +71,17 @@ defp do_render("show.json", %{user: user} = opts) do
image = User.avatar_url(user) |> MediaProxy.url() image = User.avatar_url(user) |> MediaProxy.url()
header = User.banner_url(user) |> MediaProxy.url() header = User.banner_url(user) |> MediaProxy.url()
user_info = User.get_cached_user_info(user)
following_count = following_count =
if !user.hide_follows_count or !user.hide_follows or opts[:for] == user do if !user.hide_follows_count or !user.hide_follows or opts[:for] == user do
user_info.following_count user.following_count || 0
else else
0 0
end end
followers_count = followers_count =
if !user.hide_followers_count or !user.hide_followers or opts[:for] == user do if !user.hide_followers_count or !user.hide_followers or opts[:for] == user do
user_info.follower_count user.follower_count || 0
else else
0 0
end end
@ -144,7 +143,7 @@ defp do_render("show.json", %{user: user} = opts) do
# Pleroma extension # Pleroma extension
pleroma: %{ pleroma: %{
confirmation_pending: user_info.confirmation_pending, confirmation_pending: user.confirmation_pending,
tags: user.tags, tags: user.tags,
hide_followers_count: user.hide_followers_count, hide_followers_count: user.hide_followers_count,
hide_follows_count: user.hide_follows_count, hide_follows_count: user.hide_follows_count,
@ -157,7 +156,7 @@ defp do_render("show.json", %{user: user} = opts) do
} }
} }
|> maybe_put_role(user, opts[:for]) |> maybe_put_role(user, opts[:for])
|> maybe_put_settings(user, opts[:for], user_info) |> maybe_put_settings(user, opts[:for], opts)
|> maybe_put_notification_settings(user, opts[:for]) |> maybe_put_notification_settings(user, opts[:for])
|> maybe_put_settings_store(user, opts[:for], opts) |> maybe_put_settings_store(user, opts[:for], opts)
|> maybe_put_chat_token(user, opts[:for], opts) |> maybe_put_chat_token(user, opts[:for], opts)
@ -191,7 +190,7 @@ defp maybe_put_settings(
data, data,
%User{id: user_id} = user, %User{id: user_id} = user,
%User{id: user_id}, %User{id: user_id},
_user_info _opts
) do ) do
data data
|> Kernel.put_in([:source, :privacy], user.default_scope) |> Kernel.put_in([:source, :privacy], user.default_scope)

View file

@ -9,8 +9,6 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.ActivityExpiration alias Pleroma.ActivityExpiration
alias Pleroma.Conversation
alias Pleroma.Conversation.Participation
alias Pleroma.HTML alias Pleroma.HTML
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
@ -245,12 +243,8 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
direct_conversation_id = direct_conversation_id =
with {_, nil} <- {:direct_conversation_id, opts[:direct_conversation_id]}, with {_, nil} <- {:direct_conversation_id, opts[:direct_conversation_id]},
{_, true} <- {:include_id, opts[:with_direct_conversation_id]}, {_, true} <- {:include_id, opts[:with_direct_conversation_id]},
{_, %User{} = for_user} <- {:for_user, opts[:for]}, {_, %User{} = for_user} <- {:for_user, opts[:for]} do
%{data: %{"context" => context}} when is_binary(context) <- activity, Activity.direct_conversation_id(activity, for_user)
%Conversation{} = conversation <- Conversation.get_for_ap_id(context),
%Participation{id: participation_id} <-
Participation.for_user_and_conversation(for_user, conversation) do
participation_id
else else
{:direct_conversation_id, participation_id} when is_integer(participation_id) -> {:direct_conversation_id, participation_id} when is_integer(participation_id) ->
participation_id participation_id

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

@ -33,6 +33,8 @@ def perform(
gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key) gcm_api_key = Application.get_env(:web_push_encryption, :gcm_api_key)
avatar_url = User.avatar_url(actor) avatar_url = User.avatar_url(actor)
object = Object.normalize(activity) object = Object.normalize(activity)
user = User.get_cached_by_id(user_id)
direct_conversation_id = Activity.direct_conversation_id(activity, user)
for subscription <- fetch_subsriptions(user_id), for subscription <- fetch_subsriptions(user_id),
get_in(subscription.data, ["alerts", type]) do get_in(subscription.data, ["alerts", type]) do
@ -45,7 +47,8 @@ def perform(
icon: avatar_url, icon: avatar_url,
preferred_locale: "en", preferred_locale: "en",
pleroma: %{ pleroma: %{
activity_id: activity_id activity_id: activity_id,
direct_conversation_id: direct_conversation_id
} }
} }
|> Jason.encode!() |> Jason.encode!()

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)

24
mix.exs
View file

@ -102,7 +102,7 @@ defp deps do
{:phoenix_ecto, "~> 4.0"}, {:phoenix_ecto, "~> 4.0"},
{:ecto_sql, "~> 3.2"}, {:ecto_sql, "~> 3.2"},
{:postgrex, ">= 0.13.5"}, {:postgrex, ">= 0.13.5"},
{:oban, "~> 0.8.1"}, {:oban, "~> 0.12.0"},
{:quantum, "~> 2.3"}, {:quantum, "~> 2.3"},
{:gettext, "~> 0.15"}, {:gettext, "~> 0.15"},
{:comeonin, "~> 4.1.1"}, {:comeonin, "~> 4.1.1"},
@ -194,27 +194,21 @@ 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 end
if git_tag && version != git_tag do
Mix.shell().error(
"Application version #{inspect(version)} does not match git tag #{inspect(git_tag)}"
)
end
# Branch name as pre-release version component, denoted with a dot # Branch name as pre-release version component, denoted with a dot
branch_name = branch_name =
with {branch_name, 0} <- System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]), with {branch_name, 0} <- System.cmd("git", ["rev-parse", "--abbrev-ref", "HEAD"]),

View file

@ -23,8 +23,8 @@
"decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [:mix], [], "hexpm"}, "decimal": {:hex, :decimal, "1.8.0", "ca462e0d885f09a1c5a342dbd7c1dcf27ea63548c65a65e67334f4b61803822e", [:mix], [], "hexpm"},
"deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm"},
"earmark": {:hex, :earmark, "1.4.2", "3aa0bd23bc4c61cf2f1e5d752d1bb470560a6f8539974f767a38923bb20e1d7f", [:mix], [], "hexpm"}, "earmark": {:hex, :earmark, "1.4.2", "3aa0bd23bc4c61cf2f1e5d752d1bb470560a6f8539974f767a38923bb20e1d7f", [:mix], [], "hexpm"},
"ecto": {:hex, :ecto, "3.2.3", "51274df79862845b388733fddcf6f107d0c8c86e27abe7131fa98f8d30761bda", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"}, "ecto": {:hex, :ecto, "3.2.5", "76c864b77948a479e18e69cc1d0f0f4ee7cced1148ffe6a093ff91eba644f0b5", [:mix], [{:decimal, "~> 1.6", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
"ecto_sql": {:hex, :ecto_sql, "3.2.0", "751cea597e8deb616084894dd75cbabfdbe7255ff01e8c058ca13f0353a3921b", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.2.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.2.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, "ecto_sql": {:hex, :ecto_sql, "3.2.2", "d10845bc147b9f61ef485cbf0973c0a337237199bd9bd30dd9542db00aadc26b", [:mix], [{:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.2.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.2.0 or ~> 0.3.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.15.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
"esshd": {:hex, :esshd, "0.1.0", "6f93a2062adb43637edad0ea7357db2702a4b80dd9683482fe00f5134e97f4c1", [:mix], [], "hexpm"}, "esshd": {:hex, :esshd, "0.1.0", "6f93a2062adb43637edad0ea7357db2702a4b80dd9683482fe00f5134e97f4c1", [:mix], [], "hexpm"},
"eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm"}, "eternal": {:hex, :eternal, "1.2.1", "d5b6b2499ba876c57be2581b5b999ee9bdf861c647401066d3eeed111d096bc4", [:mix], [], "hexpm"},
"ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [:mix], [], "hexpm"}, "ex2ms": {:hex, :ex2ms, "1.5.0", "19e27f9212be9a96093fed8cdfbef0a2b56c21237196d26760f11dfcfae58e97", [:mix], [], "hexpm"},
@ -67,7 +67,7 @@
"myhtmlex": {:git, "https://git.pleroma.social/pleroma/myhtmlex.git", "ad0097e2f61d4953bfef20fb6abddf23b87111e6", [ref: "ad0097e2f61d4953bfef20fb6abddf23b87111e6", submodules: true]}, "myhtmlex": {:git, "https://git.pleroma.social/pleroma/myhtmlex.git", "ad0097e2f61d4953bfef20fb6abddf23b87111e6", [ref: "ad0097e2f61d4953bfef20fb6abddf23b87111e6", submodules: true]},
"nimble_parsec": {:hex, :nimble_parsec, "0.5.1", "c90796ecee0289dbb5ad16d3ad06f957b0cd1199769641c961cfe0b97db190e0", [:mix], [], "hexpm"}, "nimble_parsec": {:hex, :nimble_parsec, "0.5.1", "c90796ecee0289dbb5ad16d3ad06f957b0cd1199769641c961cfe0b97db190e0", [:mix], [], "hexpm"},
"nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]}, "nodex": {:git, "https://git.pleroma.social/pleroma/nodex", "cb6730f943cfc6aad674c92161be23a8411f15d1", [ref: "cb6730f943cfc6aad674c92161be23a8411f15d1"]},
"oban": {:hex, :oban, "0.8.1", "4bbf62eb1829f856d69aeb5069ac7036afe07db8221a17de2a9169cc7a58a318", [:mix], [{:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, "oban": {:hex, :oban, "0.12.0", "5477d5ab4a5a201c0b6c89764040ebfc5d2c71c488a36f378016ce5990838f0f", [:mix], [{:ecto_sql, "~> 3.1", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.14", [hex: :postgrex, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
"parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"}, "parse_trans": {:hex, :parse_trans, "3.3.0", "09765507a3c7590a784615cfd421d101aec25098d50b89d7aa1d66646bc571c1", [:rebar3], [], "hexpm"},
"pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.4", "8dd29ed783f2e12195d7e0a4640effc0a7c37e6537da491f1db01839eee6d053", [:mix], [], "hexpm"}, "pbkdf2_elixir": {:hex, :pbkdf2_elixir, "0.12.4", "8dd29ed783f2e12195d7e0a4640effc0a7c37e6537da491f1db01839eee6d053", [:mix], [], "hexpm"},
"phoenix": {:hex, :phoenix, "1.4.10", "619e4a545505f562cd294df52294372d012823f4fd9d34a6657a8b242898c255", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"}, "phoenix": {:hex, :phoenix, "1.4.10", "619e4a545505f562cd294df52294372d012823f4fd9d34a6657a8b242898c255", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 1.1", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:plug, "~> 1.8.1 or ~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 1.0 or ~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm"},
@ -97,7 +97,7 @@
"sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm"}, "sweet_xml": {:hex, :sweet_xml, "0.6.6", "fc3e91ec5dd7c787b6195757fbcf0abc670cee1e4172687b45183032221b66b8", [:mix], [], "hexpm"},
"swoosh": {:hex, :swoosh, "0.23.5", "bfd9404bbf5069b1be2ffd317923ce57e58b332e25dbca2a35dedd7820dfee5a", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"}, "swoosh": {:hex, :swoosh, "0.23.5", "bfd9404bbf5069b1be2ffd317923ce57e58b332e25dbca2a35dedd7820dfee5a", [:mix], [{:cowboy, "~> 1.0.1 or ~> 1.1 or ~> 2.4", [hex: :cowboy, repo: "hexpm", optional: true]}, {:gen_smtp, "~> 0.13", [hex: :gen_smtp, repo: "hexpm", optional: true]}, {:hackney, "~> 1.9", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mail, "~> 0.2", [hex: :mail, repo: "hexpm", optional: true]}, {:mime, "~> 1.1", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_cowboy, ">= 1.0.0", [hex: :plug_cowboy, repo: "hexpm", optional: true]}], "hexpm"},
"syslog": {:git, "https://github.com/Vagabond/erlang-syslog.git", "4a6c6f2c996483e86c1320e9553f91d337bcb6aa", [tag: "1.0.5"]}, "syslog": {:git, "https://github.com/Vagabond/erlang-syslog.git", "4a6c6f2c996483e86c1320e9553f91d337bcb6aa", [tag: "1.0.5"]},
"telemetry": {:hex, :telemetry, "0.4.0", "8339bee3fa8b91cb84d14c2935f8ecf399ccd87301ad6da6b71c09553834b2ab", [:rebar3], [], "hexpm"}, "telemetry": {:hex, :telemetry, "0.4.1", "ae2718484892448a24470e6aa341bc847c3277bfb8d4e9289f7474d752c09c7f", [:rebar3], [], "hexpm"},
"tesla": {:hex, :tesla, "1.3.0", "f35d72f029e608f9cdc6f6d6fcc7c66cf6d6512a70cfef9206b21b8bd0203a30", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 0.4", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"}, "tesla": {:hex, :tesla, "1.3.0", "f35d72f029e608f9cdc6f6d6fcc7c66cf6d6512a70cfef9206b21b8bd0203a30", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "~> 4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 0.4", [hex: :mint, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.3", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm"},
"timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"}, "timex": {:hex, :timex, "3.6.1", "efdf56d0e67a6b956cc57774353b0329c8ab7726766a11547e529357ffdc1d56", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 0.1.8 or ~> 0.5 or ~> 1.0.0", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm"},
"trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"}, "trailing_format_plug": {:hex, :trailing_format_plug, "0.0.7", "64b877f912cf7273bed03379936df39894149e35137ac9509117e59866e10e45", [:mix], [{:plug, "> 0.12.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm"},

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

@ -0,0 +1,9 @@
defmodule Pleroma.Repo.Migrations.RemoveInfoFromUsers do
use Ecto.Migration
def change do
alter table(:users) do
remove(:info, :map, default: %{})
end
end
end

View file

@ -0,0 +1,53 @@
defmodule Pleroma.Repo.Migrations.FixMissingFollowingCount do
use Ecto.Migration
def up do
"""
UPDATE
users
SET
following_count = sub.count
FROM
(
SELECT
users.id AS sub_id
,COUNT (following_relationships.id)
FROM
following_relationships
,users
WHERE
users.id = following_relationships.follower_id
AND following_relationships.state = 'accept'
GROUP BY
users.id
) AS sub
WHERE
users.id = sub.sub_id
AND users.local = TRUE
;
"""
|> execute()
"""
UPDATE
users
SET
following_count = 0
WHERE
following_count IS NULL
"""
|> execute()
execute("ALTER TABLE users
ALTER COLUMN following_count SET DEFAULT 0,
ALTER COLUMN following_count SET NOT NULL
")
end
def down do
execute("ALTER TABLE users
ALTER COLUMN following_count DROP DEFAULT,
ALTER COLUMN following_count DROP NOT NULL
")
end
end

View file

@ -1 +1 @@
<!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,user-scalable=no"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/vendors~app.b2603a50868c68a1c192.css rel=stylesheet><link href=/static/css/app.fd71461124f3eb029b1b.css rel=stylesheet></head><body class=hidden><noscript>To use Pleroma, please enable JavaScript.</noscript><div id=app></div><script type=text/javascript src=/static/js/vendors~app.5c3fab032deb5f2793cb.js></script><script type=text/javascript src=/static/js/app.105d64a8fcdd6724ccde.js></script></body></html> <!DOCTYPE html><html lang=en><head><meta charset=utf-8><meta name=viewport content="width=device-width,initial-scale=1,user-scalable=no"><title>Pleroma</title><!--server-generated-meta--><link rel=icon type=image/png href=/favicon.png><link rel=stylesheet href=/static/font/css/fontello.css><link rel=stylesheet href=/static/font/css/animation.css><link href=/static/css/vendors~app.b2603a50868c68a1c192.css rel=stylesheet><link href=/static/css/app.fd71461124f3eb029b1b.css rel=stylesheet></head><body class=hidden><noscript>To use Pleroma, please enable JavaScript.</noscript><div id=app></div><script type=text/javascript src=/static/js/vendors~app.76db8e4cdf29decd5cab.js></script><script type=text/javascript src=/static/js/app.d20ca27d22d74eb7bce0.js></script></body></html>

View file

@ -1,2 +1,2 @@
(window.webpackJsonp=window.webpackJsonp||[]).push([[2],{1012:function(t,e,i){"use strict";i.r(e);var n=i(1013),c=i.n(n);for(var r in n)"default"!==r&&function(t){i.d(e,t,function(){return n[t]})}(r);var a=i(1016),s=i(0);var o=function(t){i(1014)},u=Object(s.a)(c.a,a.a,a.b,!1,o,null,null);e.default=u.exports},1013:function(t,e,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=c(i(348));function c(t){return t&&t.__esModule?t:{default:t}}var r={components:{TabSwitcher:c(i(205)).default},data:function(){return{meta:{stickers:[]},path:""}},computed:{pack:function(){return this.$store.state.instance.stickers||[]}},methods:{clear:function(){this.meta={stickers:[]}},pick:function(t,e){var i=this,c=this.$store;fetch(t).then(function(t){t.blob().then(function(t){var r=new File([t],e,{mimetype:"image/png"}),a=new FormData;a.append("file",r),n.default.uploadMedia({store:c,formData:a}).then(function(t){i.$emit("uploaded",t),i.clear()},function(t){console.warn("Can't attach sticker"),console.warn(t),i.$emit("upload-failed","default")})})})}}};e.default=r},1014:function(t,e,i){var n=i(1015);"string"==typeof n&&(n=[[t.i,n,""]]),n.locals&&(t.exports=n.locals);(0,i(2).default)("cc6cdea4",n,!0,{})},1015:function(t,e,i){(t.exports=i(1)(!1)).push([t.i,".sticker-picker{width:100%;position:relative}.sticker-picker .tab-switcher{position:absolute;top:0;bottom:0;left:0;right:0}.sticker-picker .sticker-picker-content .sticker{display:inline-block;width:20%;height:20%}.sticker-picker .sticker-picker-content .sticker img{width:100%}.sticker-picker .sticker-picker-content .sticker img:hover{filter:drop-shadow(0 0 5px var(--link,#d8a070))}",""])},1016:function(t,e,i){"use strict";i.d(e,"a",function(){return n}),i.d(e,"b",function(){return c});var n=function(){var t=this,e=t.$createElement,i=t._self._c||e;return i("div",{staticClass:"sticker-picker"},[i("tab-switcher",{staticClass:"tab-switcher",attrs:{"render-only-focused":!0,"scrollable-tabs":""}},t._l(t.pack,function(e){return i("div",{key:e.path,staticClass:"sticker-picker-content",attrs:{"image-tooltip":e.meta.title,image:e.path+e.meta.tabIcon}},t._l(e.meta.stickers,function(n){return i("div",{key:n,staticClass:"sticker",on:{click:function(i){i.stopPropagation(),i.preventDefault(),t.pick(e.path+n,e.meta.title)}}},[i("img",{attrs:{src:e.path+n}})])}),0)}),0)],1)},c=[]}}]); (window.webpackJsonp=window.webpackJsonp||[]).push([[2],{1023:function(t,e,i){"use strict";i.r(e);var n=i(1024),c=i.n(n);for(var r in n)"default"!==r&&function(t){i.d(e,t,function(){return n[t]})}(r);var a=i(1027),s=i(0);var o=function(t){i(1025)},u=Object(s.a)(c.a,a.a,a.b,!1,o,null,null);e.default=u.exports},1024:function(t,e,i){"use strict";Object.defineProperty(e,"__esModule",{value:!0});var n=c(i(352));function c(t){return t&&t.__esModule?t:{default:t}}var r={components:{TabSwitcher:c(i(207)).default},data:function(){return{meta:{stickers:[]},path:""}},computed:{pack:function(){return this.$store.state.instance.stickers||[]}},methods:{clear:function(){this.meta={stickers:[]}},pick:function(t,e){var i=this,c=this.$store;fetch(t).then(function(t){t.blob().then(function(t){var r=new File([t],e,{mimetype:"image/png"}),a=new FormData;a.append("file",r),n.default.uploadMedia({store:c,formData:a}).then(function(t){i.$emit("uploaded",t),i.clear()},function(t){console.warn("Can't attach sticker"),console.warn(t),i.$emit("upload-failed","default")})})})}}};e.default=r},1025:function(t,e,i){var n=i(1026);"string"==typeof n&&(n=[[t.i,n,""]]),n.locals&&(t.exports=n.locals);(0,i(2).default)("cc6cdea4",n,!0,{})},1026:function(t,e,i){(t.exports=i(1)(!1)).push([t.i,".sticker-picker{width:100%;position:relative}.sticker-picker .tab-switcher{position:absolute;top:0;bottom:0;left:0;right:0}.sticker-picker .sticker-picker-content .sticker{display:inline-block;width:20%;height:20%}.sticker-picker .sticker-picker-content .sticker img{width:100%}.sticker-picker .sticker-picker-content .sticker img:hover{filter:drop-shadow(0 0 5px var(--link,#d8a070))}",""])},1027:function(t,e,i){"use strict";i.d(e,"a",function(){return n}),i.d(e,"b",function(){return c});var n=function(){var t=this,e=t.$createElement,i=t._self._c||e;return i("div",{staticClass:"sticker-picker"},[i("tab-switcher",{staticClass:"tab-switcher",attrs:{"render-only-focused":!0,"scrollable-tabs":""}},t._l(t.pack,function(e){return i("div",{key:e.path,staticClass:"sticker-picker-content",attrs:{"image-tooltip":e.meta.title,image:e.path+e.meta.tabIcon}},t._l(e.meta.stickers,function(n){return i("div",{key:n,staticClass:"sticker",on:{click:function(i){i.stopPropagation(),i.preventDefault(),t.pick(e.path+n,e.meta.title)}}},[i("img",{attrs:{src:e.path+n}})])}),0)}),0)],1)},c=[]}}]);
//# sourceMappingURL=2.73375b727cef616c59b4.js.map //# sourceMappingURL=2.c96b30ae9f2d3f46f0ad.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,4 +1,4 @@
var serviceWorkerOption = {"assets":["/static/img/nsfw.74818f9.png","/static/css/app.fd71461124f3eb029b1b.css","/static/js/app.105d64a8fcdd6724ccde.js","/static/css/vendors~app.b2603a50868c68a1c192.css","/static/js/vendors~app.5c3fab032deb5f2793cb.js","/static/js/2.73375b727cef616c59b4.js"]}; var serviceWorkerOption = {"assets":["/static/img/nsfw.74818f9.png","/static/css/app.fd71461124f3eb029b1b.css","/static/js/app.d20ca27d22d74eb7bce0.js","/static/css/vendors~app.b2603a50868c68a1c192.css","/static/js/vendors~app.76db8e4cdf29decd5cab.js","/static/js/2.c96b30ae9f2d3f46f0ad.js"]};
!function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="/",t(t.s=0)}([function(e,n,t){"use strict";var r,o=t(1),i=(r=o)&&r.__esModule?r:{default:r};function a(){return clients.matchAll({includeUncontrolled:!0}).then(function(e){return e.filter(function(e){return"window"===e.type})})}self.addEventListener("push",function(e){e.data&&e.waitUntil(i.default.getItem("vuex-lz").then(function(e){return e.config.webPushNotifications}).then(function(n){return n&&a().then(function(n){var t=e.data.json();if(0===n.length)return self.registration.showNotification(t.title,t)})}))}),self.addEventListener("notificationclick",function(e){e.notification.close(),e.waitUntil(a().then(function(e){for(var n=0;n<e.length;n++){var t=e[n];if("/"===t.url&&"focus"in t)return t.focus()}if(clients.openWindow)return clients.openWindow("/")}))})},function(e,n){ !function(e){var n={};function t(r){if(n[r])return n[r].exports;var o=n[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,t),o.l=!0,o.exports}t.m=e,t.c=n,t.d=function(e,n,r){t.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:r})},t.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},t.t=function(e,n){if(1&n&&(e=t(e)),8&n)return e;if(4&n&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(t.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var o in e)t.d(r,o,function(n){return e[n]}.bind(null,o));return r},t.n=function(e){var n=e&&e.__esModule?function(){return e.default}:function(){return e};return t.d(n,"a",n),n},t.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},t.p="/",t(t.s=0)}([function(e,n,t){"use strict";var r,o=t(1),i=(r=o)&&r.__esModule?r:{default:r};function a(){return clients.matchAll({includeUncontrolled:!0}).then(function(e){return e.filter(function(e){return"window"===e.type})})}self.addEventListener("push",function(e){e.data&&e.waitUntil(i.default.getItem("vuex-lz").then(function(e){return e.config.webPushNotifications}).then(function(n){return n&&a().then(function(n){var t=e.data.json();if(0===n.length)return self.registration.showNotification(t.title,t)})}))}),self.addEventListener("notificationclick",function(e){e.notification.close(),e.waitUntil(a().then(function(e){for(var n=0;n<e.length;n++){var t=e[n];if("/"===t.url&&"focus"in t)return t.focus()}if(clients.openWindow)return clients.openWindow("/")}))})},function(e,n){
/*! /*!

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-2018 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Integration.FederationTest do
use Pleroma.DataCase
@moduletag :federated
import Pleroma.Cluster
setup_all do
Pleroma.Cluster.spawn_default_cluster()
:ok
end
@federated1 :"federated1@127.0.0.1"
describe "federated cluster primitives" do
test "within/2 captures local bindings and executes block on remote node" do
captured_binding = :captured
result =
within @federated1 do
user = Pleroma.Factory.insert(:user)
{captured_binding, node(), user}
end
assert {:captured, @federated1, user} = result
refute Pleroma.User.get_by_id(user.id)
assert user.id == within(@federated1, do: Pleroma.User.get_by_id(user.id)).id
end
test "runs webserver on customized port" do
{nickname, url, url_404} =
within @federated1 do
import Pleroma.Web.Router.Helpers
user = Pleroma.Factory.insert(:user)
user_url = account_url(Pleroma.Web.Endpoint, :show, user)
url_404 = account_url(Pleroma.Web.Endpoint, :show, "not-exists")
{user.nickname, user_url, url_404}
end
assert {:ok, {{_, 200, _}, _headers, body}} = :httpc.request(~c"#{url}")
assert %{"acct" => ^nickname} = Jason.decode!(body)
assert {:ok, {{_, 404, _}, _headers, _body}} = :httpc.request(~c"#{url_404}")
end
end
end

View file

@ -0,0 +1,19 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"vcard": "http://www.w3.org/2006/vcard/ns#",
"dfrn": "http://purl.org/macgirvin/dfrn/1.0/",
"diaspora": "https://diasporafoundation.org/ns/",
"litepub": "http://litepub.social/ns#",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"sensitive": "as:sensitive",
"Hashtag": "as:Hashtag",
"directMessage": "litepub:directMessage"
}
],
"id": "http://localhost:8080/followers/fuser3",
"type": "OrderedCollection",
"totalItems": 296
}

View file

@ -0,0 +1,19 @@
{
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"vcard": "http://www.w3.org/2006/vcard/ns#",
"dfrn": "http://purl.org/macgirvin/dfrn/1.0/",
"diaspora": "https://diasporafoundation.org/ns/",
"litepub": "http://litepub.social/ns#",
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
"sensitive": "as:sensitive",
"Hashtag": "as:Hashtag",
"directMessage": "litepub:directMessage"
}
],
"id": "http://localhost:8080/following/fuser3",
"type": "OrderedCollection",
"totalItems": 32
}

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

@ -228,5 +228,16 @@ test "skips microformats hashtags" do
assert url == "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140" assert url == "https://www.pixiv.net/member_illust.php?mode=medium&illust_id=72255140"
end end
test "does not crash when there is an HTML entity in a link" do
user = insert(:user)
{:ok, activity} =
CommonAPI.post(user, %{"status" => "\"http://cofe.com/?boomer=ok&foo=bar\""})
object = Object.normalize(activity)
assert {:ok, nil} = HTML.extract_first_external_url(object, object.data["content"])
end
end 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

@ -22,21 +22,39 @@ test "does nothing if a user is assigned", %{conn: conn} do
assert conn == ret_conn assert conn == ret_conn
end end
test "with secret set and given in the 'admin_token' parameter, it assigns an admin user", %{ describe "when secret set it assigns an admin user" do
conn: conn test "with `admin_token` query parameter", %{conn: conn} do
} do Pleroma.Config.put(:admin_token, "password123")
Pleroma.Config.put(:admin_token, "password123")
conn = conn =
%{conn | params: %{"admin_token" => "wrong_password"}} %{conn | params: %{"admin_token" => "wrong_password"}}
|> AdminSecretAuthenticationPlug.call(%{}) |> AdminSecretAuthenticationPlug.call(%{})
refute conn.assigns[:user] refute conn.assigns[:user]
conn = conn =
%{conn | params: %{"admin_token" => "password123"}} %{conn | params: %{"admin_token" => "password123"}}
|> AdminSecretAuthenticationPlug.call(%{}) |> AdminSecretAuthenticationPlug.call(%{})
assert conn.assigns[:user].is_admin assert conn.assigns[:user].is_admin
end
test "with `x-admin-token` HTTP header", %{conn: conn} do
Pleroma.Config.put(:admin_token, "☕️")
conn =
conn
|> put_req_header("x-admin-token", "🥛")
|> AdminSecretAuthenticationPlug.call(%{})
refute conn.assigns[:user]
conn =
conn
|> put_req_header("x-admin-token", "☕️")
|> AdminSecretAuthenticationPlug.call(%{})
assert conn.assigns[:user].is_admin
end
end end
end end

218
test/support/cluster.ex Normal file
View file

@ -0,0 +1,218 @@
defmodule Pleroma.Cluster do
@moduledoc """
Facilities for managing a cluster of slave VM's for federated testing.
## Spawning the federated cluster
`spawn_cluster/1` spawns a map of slave nodes that are started
within the running VM. During startup, the slave node is sent all configuration
from the parent node, as well as all code. After receiving configuration and
code, the slave then starts all applications currently running on the parent.
The configuration passed to `spawn_cluster/1` overrides any parent application
configuration for the provided OTP app and key. This is useful for customizing
the Ecto database, Phoenix webserver ports, etc.
For example, to start a single federated VM named ":federated1", with the
Pleroma Endpoint running on port 4123, and with a database named
"pleroma_test1", you would run:
endpoint_conf = Application.fetch_env!(:pleroma, Pleroma.Web.Endpoint)
repo_conf = Application.fetch_env!(:pleroma, Pleroma.Repo)
Pleroma.Cluster.spawn_cluster(%{
:"federated1@127.0.0.1" => [
{:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test1")},
{:pleroma, Pleroma.Web.Endpoint,
Keyword.merge(endpoint_conf, http: [port: 4011], url: [port: 4011], server: true)}
]
})
*Note*: application configuration for a given key is not merged,
so any customization requires first fetching the existing values
and merging yourself by providing the merged configuration,
such as above with the endpoint config and repo config.
## Executing code within a remote node
Use the `within/2` macro to execute code within the context of a remote
federated node. The code block captures all local variable bindings from
the parent's context and returns the result of the expression after executing
it on the remote node. For example:
import Pleroma.Cluster
parent_value = 123
result =
within :"federated1@127.0.0.1" do
{node(), parent_value}
end
assert result == {:"federated1@127.0.0.1, 123}
*Note*: while local bindings are captured and available within the block,
other parent contexts like required, aliased, or imported modules are not
in scope. Those will need to be reimported/aliases/required within the block
as `within/2` is a remote procedure call.
"""
@extra_apps Pleroma.Mixfile.application()[:extra_applications]
@doc """
Spawns the default Pleroma federated cluster.
Values before may be customized as needed for the test suite.
"""
def spawn_default_cluster do
endpoint_conf = Application.fetch_env!(:pleroma, Pleroma.Web.Endpoint)
repo_conf = Application.fetch_env!(:pleroma, Pleroma.Repo)
spawn_cluster(%{
:"federated1@127.0.0.1" => [
{:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test_federated1")},
{:pleroma, Pleroma.Web.Endpoint,
Keyword.merge(endpoint_conf, http: [port: 4011], url: [port: 4011], server: true)}
],
:"federated2@127.0.0.1" => [
{:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test_federated2")},
{:pleroma, Pleroma.Web.Endpoint,
Keyword.merge(endpoint_conf, http: [port: 4012], url: [port: 4012], server: true)}
]
})
end
@doc """
Spawns a configured map of federated nodes.
See `Pleroma.Cluster` module documentation for details.
"""
def spawn_cluster(node_configs) do
# Turn node into a distributed node with the given long name
:net_kernel.start([:"primary@127.0.0.1"])
# Allow spawned nodes to fetch all code from this node
{:ok, _} = :erl_boot_server.start([])
allow_boot("127.0.0.1")
silence_logger_warnings(fn ->
node_configs
|> Enum.map(&Task.async(fn -> start_slave(&1) end))
|> Enum.map(&Task.await(&1, 60_000))
end)
end
@doc """
Executes block of code again remote node.
See `Pleroma.Cluster` module documentation for details.
"""
defmacro within(node, do: block) do
quote do
rpc(unquote(node), unquote(__MODULE__), :eval_quoted, [
unquote(Macro.escape(block)),
binding()
])
end
end
@doc false
def eval_quoted(block, binding) do
{result, _binding} = Code.eval_quoted(block, binding, __ENV__)
result
end
defp start_slave({node_host, override_configs}) do
log(node_host, "booting federated VM")
{:ok, node} = :slave.start(~c"127.0.0.1", node_name(node_host), vm_args())
add_code_paths(node)
load_apps_and_transfer_configuration(node, override_configs)
ensure_apps_started(node)
{:ok, node}
end
def rpc(node, module, function, args) do
:rpc.block_call(node, module, function, args)
end
defp vm_args do
~c"-loader inet -hosts 127.0.0.1 -setcookie #{:erlang.get_cookie()}"
end
defp allow_boot(host) do
{:ok, ipv4} = :inet.parse_ipv4_address(~c"#{host}")
:ok = :erl_boot_server.add_slave(ipv4)
end
defp add_code_paths(node) do
rpc(node, :code, :add_paths, [:code.get_path()])
end
defp load_apps_and_transfer_configuration(node, override_configs) do
Enum.each(Application.loaded_applications(), fn {app_name, _, _} ->
app_name
|> Application.get_all_env()
|> Enum.each(fn {key, primary_config} ->
rpc(node, Application, :put_env, [app_name, key, primary_config, [persistent: true]])
end)
end)
Enum.each(override_configs, fn {app_name, key, val} ->
rpc(node, Application, :put_env, [app_name, key, val, [persistent: true]])
end)
end
defp log(node, msg), do: IO.puts("[#{node}] #{msg}")
defp ensure_apps_started(node) do
loaded_names = Enum.map(Application.loaded_applications(), fn {name, _, _} -> name end)
app_names = @extra_apps ++ (loaded_names -- @extra_apps)
rpc(node, Application, :ensure_all_started, [:mix])
rpc(node, Mix, :env, [Mix.env()])
rpc(node, __MODULE__, :prepare_database, [])
log(node, "starting application")
Enum.reduce(app_names, MapSet.new(), fn app, loaded ->
if Enum.member?(loaded, app) do
loaded
else
{:ok, started} = rpc(node, Application, :ensure_all_started, [app])
MapSet.union(loaded, MapSet.new(started))
end
end)
end
@doc false
def prepare_database do
log(node(), "preparing database")
repo_config = Application.get_env(:pleroma, Pleroma.Repo)
repo_config[:adapter].storage_down(repo_config)
repo_config[:adapter].storage_up(repo_config)
{:ok, _, _} =
Ecto.Migrator.with_repo(Pleroma.Repo, fn repo ->
Ecto.Migrator.run(repo, :up, log: false, all: true)
end)
Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual)
{:ok, _} = Application.ensure_all_started(:ex_machina)
end
defp silence_logger_warnings(func) do
prev_level = Logger.level()
Logger.configure(level: :error)
res = func.()
Logger.configure(level: prev_level)
res
end
defp node_name(node_host) do
node_host
|> to_string()
|> String.split("@")
|> Enum.at(0)
|> String.to_atom()
end
end

View file

@ -31,7 +31,6 @@ def user_factory do
nickname: sequence(:nickname, &"nick#{&1}"), nickname: sequence(:nickname, &"nick#{&1}"),
password_hash: Comeonin.Pbkdf2.hashpwsalt("test"), password_hash: Comeonin.Pbkdf2.hashpwsalt("test"),
bio: sequence(:bio, &"Tester Number #{&1}"), bio: sequence(:bio, &"Tester Number #{&1}"),
info: %{},
last_digest_emailed_at: NaiveDateTime.utc_now() last_digest_emailed_at: NaiveDateTime.utc_now()
} }

View file

@ -1035,6 +1035,22 @@ def get("http://localhost:4001/users/masto_closed/following?page=1", _, _, _) do
}} }}
end end
def get("http://localhost:8080/followers/fuser3", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/users_mock/friendica_followers.json")
}}
end
def get("http://localhost:8080/following/fuser3", _, _, _) do
{:ok,
%Tesla.Env{
status: 200,
body: File.read!("test/fixtures/users_mock/friendica_following.json")
}}
end
def get("http://localhost:4001/users/fuser2/followers", _, _, _) do def get("http://localhost:4001/users/fuser2/followers", _, _, _) do
{:ok, {:ok,
%Tesla.Env{ %Tesla.Env{

View file

@ -3,7 +3,8 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
os_exclude = if :os.type() == {:unix, :darwin}, do: [skip_on_mac: true], else: [] os_exclude = if :os.type() == {:unix, :darwin}, do: [skip_on_mac: true], else: []
ExUnit.start(exclude: os_exclude) ExUnit.start(exclude: [:federated | os_exclude])
Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual) Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual)
Mox.defmock(Pleroma.ReverseProxy.ClientMock, for: Pleroma.ReverseProxy.Client) Mox.defmock(Pleroma.ReverseProxy.ClientMock, for: Pleroma.ReverseProxy.Client)
{:ok, _} = Application.ensure_all_started(:ex_machina) {:ok, _} = Application.ensure_all_started(:ex_machina)

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
@ -347,18 +367,6 @@ test "it sets the password_hash and ap_id" do
assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers" assert changeset.changes.follower_address == "#{changeset.changes.ap_id}/followers"
end end
test "it ensures info is not nil" do
changeset = User.register_changeset(%User{}, @full_user_data)
assert changeset.valid?
{:ok, user} =
changeset
|> Repo.insert()
refute is_nil(user.info)
end
end end
describe "user registration, with :account_activation_required" do describe "user registration, with :account_activation_required" do
@ -412,8 +420,7 @@ test "gets an existing user by ap_id" do
:user, :user,
local: false, local: false,
nickname: "admin@mastodon.example.org", nickname: "admin@mastodon.example.org",
ap_id: ap_id, ap_id: ap_id
info: %{}
) )
{:ok, fetched_user} = User.get_or_fetch(ap_id) {:ok, fetched_user} = User.get_or_fetch(ap_id)
@ -474,8 +481,7 @@ test "updates an existing user, if stale" do
local: false, local: false,
nickname: "admin@mastodon.example.org", nickname: "admin@mastodon.example.org",
ap_id: "http://mastodon.example.org/users/admin", ap_id: "http://mastodon.example.org/users/admin",
last_refreshed_at: a_week_ago, last_refreshed_at: a_week_ago
info: %{}
) )
assert orig_user.last_refreshed_at == a_week_ago assert orig_user.last_refreshed_at == a_week_ago
@ -516,7 +522,6 @@ test "returns an ap_followers link for a user" do
name: "Someone", name: "Someone",
nickname: "a@b.de", nickname: "a@b.de",
ap_id: "http...", ap_id: "http...",
info: %{some: "info"},
avatar: %{some: "avatar"} avatar: %{some: "avatar"}
} }
@ -941,9 +946,9 @@ test "hide a user from followers" do
{:ok, user} = User.follow(user, user2) {:ok, user} = User.follow(user, user2)
{:ok, _user} = User.deactivate(user) {:ok, _user} = User.deactivate(user)
info = User.get_cached_user_info(user2) user2 = User.get_cached_by_id(user2.id)
assert info.follower_count == 0 assert user2.follower_count == 0
assert [] = User.get_followers(user2) assert [] = User.get_followers(user2)
end end
@ -952,13 +957,15 @@ 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) user2 = User.get_cached_by_id(user2.id)
assert info.following_count == 0 assert refresh_record(user2).following_count == 0
assert user2.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)
end end
@ -1121,8 +1128,7 @@ test "with an overly long bio" do
ap_id: user.ap_id, ap_id: user.ap_id,
name: user.name, name: user.name,
nickname: user.nickname, nickname: user.nickname,
bio: String.duplicate("h", current_max_length + 1), bio: String.duplicate("h", current_max_length + 1)
info: %{}
} }
assert {:ok, %User{}} = User.insert_or_update_user(data) assert {:ok, %User{}} = User.insert_or_update_user(data)
@ -1135,8 +1141,7 @@ test "with an overly long display name" do
data = %{ data = %{
ap_id: user.ap_id, ap_id: user.ap_id,
name: String.duplicate("h", current_max_length + 1), name: String.duplicate("h", current_max_length + 1),
nickname: user.nickname, nickname: user.nickname
info: %{}
} }
assert {:ok, %User{}} = User.insert_or_update_user(data) assert {:ok, %User{}} = User.insert_or_update_user(data)
@ -1160,13 +1165,12 @@ test "html_filter_policy returns TwitterText scrubber when rich-text is disabled
describe "caching" do describe "caching" do
test "invalidate_cache works" do test "invalidate_cache works" do
user = insert(:user) user = insert(:user)
_user_info = User.get_cached_user_info(user)
User.set_cache(user)
User.invalidate_cache(user) User.invalidate_cache(user)
{:ok, nil} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}") {:ok, nil} = Cachex.get(:user_cache, "ap_id:#{user.ap_id}")
{:ok, nil} = Cachex.get(:user_cache, "nickname:#{user.nickname}") {:ok, nil} = Cachex.get(:user_cache, "nickname:#{user.nickname}")
{:ok, nil} = Cachex.get(:user_cache, "user_info:#{user.id}")
end end
test "User.delete() plugs any possible zombie objects" do test "User.delete() plugs any possible zombie objects" do
@ -1322,7 +1326,7 @@ test "follower count is updated when a follower is blocked" do
{:ok, user} = User.block(user, follower) {:ok, user} = User.block(user, follower)
assert User.user_info(user).follower_count == 2 assert user.follower_count == 2
end end
describe "list_inactive_users_query/1" do describe "list_inactive_users_query/1" do
@ -1499,51 +1503,6 @@ test "external_users/1 external active users with limit", %{user1: user1, user2:
end end
end end
describe "set_info_cache/2" do
setup do
user = insert(:user)
{:ok, user: user}
end
test "update from args", %{user: user} do
User.set_info_cache(user, %{following_count: 15, follower_count: 18})
%{follower_count: followers, following_count: following} = User.get_cached_user_info(user)
assert followers == 18
assert following == 15
end
test "without args", %{user: user} do
User.set_info_cache(user, %{})
%{follower_count: followers, following_count: following} = User.get_cached_user_info(user)
assert followers == 0
assert following == 0
end
end
describe "user_info/2" do
setup do
user = insert(:user)
{:ok, user: user}
end
test "update from args", %{user: user} do
%{follower_count: followers, following_count: following} =
User.user_info(user, %{following_count: 15, follower_count: 18})
assert followers == 18
assert following == 15
end
test "without args", %{user: user} do
%{follower_count: followers, following_count: following} = User.user_info(user)
assert followers == 0
assert following == 0
end
end
describe "is_internal_user?/1" do describe "is_internal_user?/1" do
test "non-internal user returns false" do test "non-internal user returns false" do
user = insert(:user) user = insert(:user)
@ -1600,14 +1559,14 @@ test "updates the counters normally on following/getting a follow when disabled"
ap_enabled: true ap_enabled: true
) )
assert User.user_info(other_user).following_count == 0 assert other_user.following_count == 0
assert User.user_info(other_user).follower_count == 0 assert other_user.follower_count == 0
{:ok, user} = Pleroma.User.follow(user, other_user) {:ok, user} = Pleroma.User.follow(user, other_user)
other_user = Pleroma.User.get_by_id(other_user.id) other_user = Pleroma.User.get_by_id(other_user.id)
assert User.user_info(user).following_count == 1 assert user.following_count == 1
assert User.user_info(other_user).follower_count == 1 assert other_user.follower_count == 1
end end
test "syncronizes the counters with the remote instance for the followed when enabled" do test "syncronizes the counters with the remote instance for the followed when enabled" do
@ -1623,14 +1582,14 @@ test "syncronizes the counters with the remote instance for the followed when en
ap_enabled: true ap_enabled: true
) )
assert User.user_info(other_user).following_count == 0 assert other_user.following_count == 0
assert User.user_info(other_user).follower_count == 0 assert other_user.follower_count == 0
Pleroma.Config.put([:instance, :external_user_synchronization], true) Pleroma.Config.put([:instance, :external_user_synchronization], true)
{:ok, _user} = User.follow(user, other_user) {:ok, _user} = User.follow(user, other_user)
other_user = User.get_by_id(other_user.id) other_user = User.get_by_id(other_user.id)
assert User.user_info(other_user).follower_count == 437 assert other_user.follower_count == 437
end end
test "syncronizes the counters with the remote instance for the follower when enabled" do test "syncronizes the counters with the remote instance for the follower when enabled" do
@ -1646,13 +1605,13 @@ test "syncronizes the counters with the remote instance for the follower when en
ap_enabled: true ap_enabled: true
) )
assert User.user_info(other_user).following_count == 0 assert other_user.following_count == 0
assert User.user_info(other_user).follower_count == 0 assert other_user.follower_count == 0
Pleroma.Config.put([:instance, :external_user_synchronization], true) Pleroma.Config.put([:instance, :external_user_synchronization], true)
{:ok, other_user} = User.follow(other_user, user) {:ok, other_user} = User.follow(other_user, user)
assert User.user_info(other_user).following_count == 152 assert other_user.following_count == 152
end end
end end

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

@ -1554,6 +1554,21 @@ test "detects hidden follows" do
assert follow_info.hide_followers == false assert follow_info.hide_followers == false
assert follow_info.hide_follows == true assert follow_info.hide_follows == true
end end
test "detects hidden follows/followers for friendica" do
user =
insert(:user,
local: false,
follower_address: "http://localhost:8080/followers/fuser3",
following_address: "http://localhost:8080/following/fuser3"
)
{:ok, follow_info} = ActivityPub.fetch_follow_information_for_user(user)
assert follow_info.hide_followers == true
assert follow_info.follower_count == 296
assert follow_info.following_count == 32
assert follow_info.hide_follows == true
end
end end
describe "fetch_favourites/3" do describe "fetch_favourites/3" do

View file

@ -39,6 +39,7 @@ test "it ignores an incoming notice if we already have it" do
assert activity == returned_activity assert activity == returned_activity
end end
@tag capture_log: true
test "it fetches replied-to activities if we don't have them" do test "it fetches replied-to activities if we don't have them" do
data = data =
File.read!("test/fixtures/mastodon-post-activity.json") File.read!("test/fixtures/mastodon-post-activity.json")
@ -533,6 +534,7 @@ test "it works for incoming announces with an inlined activity" do
assert object.data["content"] == "this is a private toot" assert object.data["content"] == "this is a private toot"
end end
@tag capture_log: true
test "it rejects incoming announces with an inlined activity from another origin" do test "it rejects incoming announces with an inlined activity from another origin" do
data = data =
File.read!("test/fixtures/bogus-mastodon-announce.json") File.read!("test/fixtures/bogus-mastodon-announce.json")
@ -814,6 +816,7 @@ test "it fails for incoming deletes with spoofed origin" do
assert Activity.get_by_id(activity.id) assert Activity.get_by_id(activity.id)
end end
@tag capture_log: true
test "it works for incoming user deletes" do test "it works for incoming user deletes" do
%{ap_id: ap_id} = insert(:user, ap_id: "http://mastodon.example.org/users/admin") %{ap_id: ap_id} = insert(:user, ap_id: "http://mastodon.example.org/users/admin")
@ -1749,6 +1752,7 @@ test "returns object with inReplyToAtomUri when denied incoming reply", %{data:
assert modified_object["inReplyToAtomUri"] == "" assert modified_object["inReplyToAtomUri"] == ""
end end
@tag capture_log: true
test "returns modified object when allowed incoming reply", %{data: data} do test "returns modified object when allowed incoming reply", %{data: data} do
object_with_reply = object_with_reply =
Map.put( Map.put(
@ -1868,6 +1872,7 @@ test "returns nil when cannot normalize object" do
end) =~ "Unsupported URI scheme" end) =~ "Unsupported URI scheme"
end end
@tag capture_log: true
test "returns {:ok, %Object{}} for success case" do test "returns {:ok, %Object{}} for success case" do
assert {:ok, %Object{}} = assert {:ok, %Object{}} =
Transmogrifier.get_obj_helper("https://shitposter.club/notice/2827873") Transmogrifier.get_obj_helper("https://shitposter.club/notice/2827873")

View file

@ -225,7 +225,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)
@ -634,7 +635,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,
@ -644,7 +646,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"])
@ -685,7 +688,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
} }
] ]
} }
@ -709,7 +713,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
} }
] ]
} }
@ -733,7 +738,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
} }
] ]
} }
@ -757,7 +763,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
} }
] ]
} }
@ -781,7 +788,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
} }
] ]
} }
@ -805,7 +813,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
} }
] ]
} }
@ -824,7 +833,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
} }
] ]
} }
@ -853,7 +863,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
} }
] ]
} }
@ -880,7 +891,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,
@ -890,7 +902,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,
@ -900,7 +913,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"])
@ -929,7 +943,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,
@ -939,7 +954,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"])
@ -970,7 +986,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
} }
] ]
} }
@ -994,7 +1011,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,
@ -1004,7 +1022,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"])
@ -1040,7 +1059,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
} }
] ]
} }
@ -1066,7 +1086,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
} }
] ]
} }
@ -1135,7 +1156,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)
@ -1901,6 +1923,7 @@ test "with settings in db", %{conn: conn} do
Pleroma.Config.put([:instance, :dynamic_configuration], true) Pleroma.Config.put([:instance, :dynamic_configuration], true)
end end
@tag capture_log: true
test "create new config setting in db", %{conn: conn} do test "create new config setting in db", %{conn: conn} do
conn = conn =
post(conn, "/api/pleroma/admin/config", %{ post(conn, "/api/pleroma/admin/config", %{
@ -2839,6 +2862,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

@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do defmodule Pleroma.Web.MastodonAPI.FilterControllerTest do
use Pleroma.Web.ConnCase, async: true use Pleroma.Web.ConnCase
alias Pleroma.Web.MastodonAPI.FilterView alias Pleroma.Web.MastodonAPI.FilterView

View file

@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.MastodonAPI.ScheduledActivityControllerTest do defmodule Pleroma.Web.MastodonAPI.ScheduledActivityControllerTest do
use Pleroma.Web.ConnCase, async: true use Pleroma.Web.ConnCase
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.ScheduledActivity alias Pleroma.ScheduledActivity

View file

@ -350,7 +350,8 @@ test "represent an embedded relationship" do
} }
} }
assert expected == AccountView.render("show.json", %{user: user, for: other_user}) assert expected ==
AccountView.render("show.json", %{user: refresh_record(user), for: other_user})
end end
test "returns the settings store if the requesting user is the represented user and it's requested specifically" do test "returns the settings store if the requesting user is the represented user and it's requested specifically" do
@ -374,6 +375,14 @@ test "sanitizes display names" do
refute result.display_name == "<marquee> username </marquee>" refute result.display_name == "<marquee> username </marquee>"
end end
test "never display nil user follow counts" do
user = insert(:user, following_count: 0, follower_count: 0)
result = AccountView.render("show.json", %{user: user})
assert result.following_count == 0
assert result.followers_count == 0
end
describe "hiding follows/following" do describe "hiding follows/following" do
test "shows when follows/followers stats are hidden and sets follow/follower count to 0" do test "shows when follows/followers stats are hidden and sets follow/follower count to 0" do
user = user =

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"]
conn redirect_url =
|> get("/notice/#{note_activity.id}") conn
|> response(200) |> put_req_header("accept", "application/activity+json")
|> get("/notice/#{note_activity.id}")
|> 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)

View file

@ -15,7 +15,7 @@ defmodule Pleroma.Web.StreamerTest do
alias Pleroma.Web.Streamer.StreamerSocket alias Pleroma.Web.Streamer.StreamerSocket
alias Pleroma.Web.Streamer.Worker alias Pleroma.Web.Streamer.Worker
@moduletag needs_streamer: true @moduletag needs_streamer: true, capture_log: true
clear_config_all([:instance, :skip_thread_containment]) clear_config_all([:instance, :skip_thread_containment])
describe "user streams" do describe "user streams" do