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

This commit is contained in:
sadposter 2020-04-27 22:57:16 +01:00
commit 4f47317f39
68 changed files with 1333 additions and 487 deletions

View file

@ -19,12 +19,14 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
<summary>API Changes</summary> <summary>API Changes</summary>
- Mastodon API: Support for `include_types` in `/api/v1/notifications`. - Mastodon API: Support for `include_types` in `/api/v1/notifications`.
- Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint. - Mastodon API: Added `/api/v1/notifications/:id/dismiss` endpoint.
- Mastodon API: Add support for filtering replies in public and home timelines
- Admin API: endpoints for create/update/delete OAuth Apps. - Admin API: endpoints for create/update/delete OAuth Apps.
</details> </details>
### Fixed ### Fixed
- Support pagination in conversations API - Support pagination in conversations API
- **Breaking**: SimplePolicy `:reject` and `:accept` allow deletions again - **Breaking**: SimplePolicy `:reject` and `:accept` allow deletions again
- Fix follower/blocks import when nicknames starts with @
## [unreleased-patch] ## [unreleased-patch]
### Fixed ### Fixed

View file

@ -279,7 +279,7 @@ defmodule Pleroma.LoadTesting.Activities do
actor = get_actor(group, user, friends, non_friends) actor = get_actor(group, user, friends, non_friends)
with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(), with activity_id when not is_nil(activity_id) <- get_random_create_activity_id(),
{:ok, _activity, _object} <- CommonAPI.favorite(activity_id, actor) do {:ok, _activity} <- CommonAPI.favorite(actor, activity_id) do
:ok :ok
else else
{:error, _} -> {:error, _} ->
@ -313,7 +313,7 @@ defmodule Pleroma.LoadTesting.Activities do
tasks = get_reply_tasks(visibility, group) tasks = get_reply_tasks(visibility, group)
{:ok, activity} = {:ok, activity} =
CommonAPI.post(user, %{"status" => "Simple status", "visibility" => "unlisted"}) CommonAPI.post(user, %{"status" => "Simple status", "visibility" => visibility})
acc = {activity.id, ["@" <> actor.nickname, "reply to status"]} acc = {activity.id, ["@" <> actor.nickname, "reply to status"]}
insert_replies(tasks, visibility, user, friends, non_friends, acc) insert_replies(tasks, visibility, user, friends, non_friends, acc)

View file

@ -41,6 +41,7 @@ defmodule Pleroma.LoadTesting.Fetcher do
fetch_notifications(user) fetch_notifications(user)
fetch_favourites(user) fetch_favourites(user)
fetch_long_thread(user) fetch_long_thread(user)
fetch_timelines_with_reply_filtering(user)
end end
defp render_views(user) do defp render_views(user) do
@ -495,4 +496,58 @@ defmodule Pleroma.LoadTesting.Fetcher do
formatters: formatters() formatters: formatters()
) )
end end
defp fetch_timelines_with_reply_filtering(user) do
public_params = opts_for_public_timeline(user)
Benchee.run(
%{
"Public timeline without reply filtering" => fn ->
ActivityPub.fetch_public_activities(public_params)
end,
"Public timeline with reply filtering - following" => fn ->
public_params
|> Map.put("reply_visibility", "following")
|> Map.put("reply_filtering_user", user)
|> ActivityPub.fetch_public_activities()
end,
"Public timeline with reply filtering - self" => fn ->
public_params
|> Map.put("reply_visibility", "self")
|> Map.put("reply_filtering_user", user)
|> ActivityPub.fetch_public_activities()
end
},
formatters: formatters()
)
private_params = opts_for_home_timeline(user)
recipients = [user.ap_id | User.following(user)]
Benchee.run(
%{
"Home timeline without reply filtering" => fn ->
ActivityPub.fetch_activities(recipients, private_params)
end,
"Home timeline with reply filtering - following" => fn ->
private_params =
private_params
|> Map.put("reply_filtering_user", user)
|> Map.put("reply_visibility", "following")
ActivityPub.fetch_activities(recipients, private_params)
end,
"Home timeline with reply filtering - self" => fn ->
private_params =
private_params
|> Map.put("reply_filtering_user", user)
|> Map.put("reply_visibility", "self")
ActivityPub.fetch_activities(recipients, private_params)
end
},
formatters: formatters()
)
end
end end

View file

@ -44,6 +44,7 @@ defmodule Mix.Tasks.Pleroma.LoadTesting do
] ]
def run(args) do def run(args) do
Logger.configure(level: :error)
Mix.Pleroma.start_pleroma() Mix.Pleroma.start_pleroma()
clean_tables() clean_tables()
{opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases) {opts, _} = OptionParser.parse!(args, strict: @switches, aliases: @aliases)

View file

@ -14,6 +14,7 @@ Some apps operate under the assumption that no more than 4 attachments can be re
Adding the parameter `with_muted=true` to the timeline queries will also return activities by muted (not by blocked!) users. Adding the parameter `with_muted=true` to the timeline queries will also return activities by muted (not by blocked!) users.
Adding the parameter `exclude_visibilities` to the timeline queries will exclude the statuses with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`), e.g., `exclude_visibilities[]=direct&exclude_visibilities[]=private`. Adding the parameter `exclude_visibilities` to the timeline queries will exclude the statuses with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`), e.g., `exclude_visibilities[]=direct&exclude_visibilities[]=private`.
Adding the parameter `reply_visibility` to the public and home timelines queries will filter replies. Possible values: without parameter (default) shows all replies, `following` - replies directed to you or users you follow, `self` - replies directed to you.
## Statuses ## Statuses

View file

@ -36,7 +36,7 @@ content-security-policy:
default-src 'none'; default-src 'none';
base-uri 'self'; base-uri 'self';
frame-ancestors 'none'; frame-ancestors 'none';
img-src 'self' data: https:; img-src 'self' data: blob: https:;
media-src 'self' https:; media-src 'self' https:;
style-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';
font-src 'self'; font-src 'self';

View file

@ -7,13 +7,9 @@ This guide will assume you are on Debian Stretch. This guide should also work wi
* `postgresql` (9.6+, Ubuntu 16.04 comes with 9.5, you can get a newer version from [here](https://www.postgresql.org/download/linux/ubuntu/)) * `postgresql` (9.6+, Ubuntu 16.04 comes with 9.5, you can get a newer version from [here](https://www.postgresql.org/download/linux/ubuntu/))
* `postgresql-contrib` (9.6+, same situtation as above) * `postgresql-contrib` (9.6+, same situtation as above)
* `elixir` (1.5+, [install from here, Debian and Ubuntu ship older versions](https://elixir-lang.org/install.html#unix-and-unix-like) or use [asdf](https://github.com/asdf-vm/asdf) as the pleroma user) * `elixir` (1.8+, Follow the guide to install from the Erlang Solutions repo or use [asdf](https://github.com/asdf-vm/asdf) as the pleroma user)
* `erlang-dev` * `erlang-dev`
* `erlang-tools` * `erlang-nox`
* `erlang-parsetools`
* `erlang-eldap`, if you want to enable ldap authenticator
* `erlang-ssh`
* `erlang-xmerl`
* `git` * `git`
* `build-essential` * `build-essential`
@ -50,7 +46,7 @@ sudo dpkg -i /tmp/erlang-solutions_1.0_all.deb
```shell ```shell
sudo apt update sudo apt update
sudo apt install elixir erlang-dev erlang-parsetools erlang-xmerl erlang-tools erlang-ssh sudo apt install elixir erlang-dev erlang-nox
``` ```
### Install PleromaBE ### Install PleromaBE

View file

@ -10,21 +10,17 @@
### 必要なソフトウェア ### 必要なソフトウェア
- PostgreSQL 9.6以上 (Ubuntu16.04では9.5しか提供されていないので,[](https://www.postgresql.org/download/linux/ubuntu/)こちらから新しいバージョンを入手してください) - PostgreSQL 9.6以上 (Ubuntu16.04では9.5しか提供されていないので,[](https://www.postgresql.org/download/linux/ubuntu/)こちらから新しいバージョンを入手してください)
- postgresql-contrib 9.6以上 (同上) - `postgresql-contrib` 9.6以上 (同上)
- Elixir 1.5 以上 ([Debianのリポジトリからインストールしないこと ここからインストールすること!](https://elixir-lang.org/install.html#unix-and-unix-like)。または [asdf](https://github.com/asdf-vm/asdf) をpleromaユーザーでインストールしてください) - Elixir 1.8 以上 ([Debianのリポジトリからインストールしないこと ここからインストールすること!](https://elixir-lang.org/install.html#unix-and-unix-like)。または [asdf](https://github.com/asdf-vm/asdf) をpleromaユーザーでインストールしてください)
- erlang-dev - `erlang-dev`
- erlang-tools - `erlang-nox`
- erlang-parsetools - `git`
- erlang-eldap (LDAP認証を有効化するときのみ必要) - `build-essential`
- erlang-ssh
- erlang-xmerl
- git
- build-essential
#### このガイドで利用している追加パッケージ #### このガイドで利用している追加パッケージ
- nginx (おすすめです。他のリバースプロキシを使う場合は、参考となる設定をこのリポジトリから探してください) - `nginx` (おすすめです。他のリバースプロキシを使う場合は、参考となる設定をこのリポジトリから探してください)
- certbot (または何らかのLet's Encrypt向けACMEクライアント) - `certbot` (または何らかのLet's Encrypt向けACMEクライアント)
### システムを準備する ### システムを準備する
@ -51,7 +47,7 @@ sudo dpkg -i /tmp/erlang-solutions_1.0_all.deb
* ElixirとErlangをインストールします、 * ElixirとErlangをインストールします、
``` ```
sudo apt update sudo apt update
sudo apt install elixir erlang-dev erlang-parsetools erlang-xmerl erlang-tools erlang-ssh sudo apt install elixir erlang-dev erlang-nox
``` ```
### Pleroma BE (バックエンド) をインストールします ### Pleroma BE (バックエンド) をインストールします

View file

@ -47,7 +47,7 @@ defmodule Pleroma.Config.Loader do
@spec filter_group(atom(), keyword()) :: keyword() @spec filter_group(atom(), keyword()) :: keyword()
def filter_group(group, configs) do def filter_group(group, configs) do
Enum.reject(configs[group], fn {key, _v} -> Enum.reject(configs[group], fn {key, _v} ->
key in @reject_keys or (group == :phoenix and key == :serve_endpoints) key in @reject_keys or (group == :phoenix and key == :serve_endpoints) or group == :postgrex
end) end)
end end
end end

View file

@ -46,14 +46,6 @@ defmodule Pleroma.Config.TransferTask do
with {_, true} <- {:configurable, Config.get(:configurable_from_database)} do with {_, true} <- {:configurable, Config.get(:configurable_from_database)} do
# We need to restart applications for loaded settings take effect # We need to restart applications for loaded settings take effect
# TODO: some problem with prometheus after restart!
reject_restart =
if restart_pleroma? do
[nil, :prometheus]
else
[:pleroma, nil, :prometheus]
end
{logger, other} = {logger, other} =
(Repo.all(ConfigDB) ++ deleted_settings) (Repo.all(ConfigDB) ++ deleted_settings)
|> Enum.map(&transform_and_merge/1) |> Enum.map(&transform_and_merge/1)
@ -65,10 +57,20 @@ defmodule Pleroma.Config.TransferTask do
started_applications = Application.started_applications() started_applications = Application.started_applications()
# TODO: some problem with prometheus after restart!
reject = [nil, :prometheus, :postgrex]
reject =
if restart_pleroma? do
reject
else
[:pleroma | reject]
end
other other
|> Enum.map(&update/1) |> Enum.map(&update/1)
|> Enum.uniq() |> Enum.uniq()
|> Enum.reject(&(&1 in reject_restart)) |> Enum.reject(&(&1 in reject))
|> maybe_set_pleroma_last() |> maybe_set_pleroma_last()
|> Enum.each(&restart(started_applications, &1, Config.get(:env))) |> Enum.each(&restart(started_applications, &1, Config.get(:env)))

View file

@ -261,7 +261,7 @@ defmodule Pleroma.Object do
end end
end end
def increase_vote_count(ap_id, name) do def increase_vote_count(ap_id, name, actor) do
with %Object{} = object <- Object.normalize(ap_id), with %Object{} = object <- Object.normalize(ap_id),
"Question" <- object.data["type"] do "Question" <- object.data["type"] do
multiple = Map.has_key?(object.data, "anyOf") multiple = Map.has_key?(object.data, "anyOf")
@ -276,12 +276,15 @@ defmodule Pleroma.Object do
option option
end) end)
voters = [actor | object.data["voters"] || []] |> Enum.uniq()
data = data =
if multiple do if multiple do
Map.put(object.data, "anyOf", options) Map.put(object.data, "anyOf", options)
else else
Map.put(object.data, "oneOf", options) Map.put(object.data, "oneOf", options)
end end
|> Map.put("voters", voters)
object object
|> Object.change(%{data: data}) |> Object.change(%{data: data})

View file

@ -75,7 +75,7 @@ defmodule Pleroma.Plugs.HTTPSecurityPlug do
"default-src 'none'", "default-src 'none'",
"base-uri 'self'", "base-uri 'self'",
"frame-ancestors 'none'", "frame-ancestors 'none'",
"img-src 'self' data: https:", "img-src 'self' data: blob: https:",
"media-src 'self' https:", "media-src 'self' https:",
"style-src 'self' 'unsafe-inline'", "style-src 'self' 'unsafe-inline'",
"font-src 'self'", "font-src 'self'",

View file

@ -45,11 +45,11 @@ defmodule Pleroma.Stats do
end end
def init(_args) do def init(_args) do
{:ok, get_stat_data()} {:ok, calculate_stat_data()}
end end
def handle_call(:force_update, _from, _state) do def handle_call(:force_update, _from, _state) do
new_stats = get_stat_data() new_stats = calculate_stat_data()
{:reply, new_stats, new_stats} {:reply, new_stats, new_stats}
end end
@ -58,12 +58,12 @@ defmodule Pleroma.Stats do
end end
def handle_cast(:run_update, _state) do def handle_cast(:run_update, _state) do
new_stats = get_stat_data() new_stats = calculate_stat_data()
{:noreply, new_stats} {:noreply, new_stats}
end end
defp get_stat_data do def calculate_stat_data do
peers = peers =
from( from(
u in User, u in User,
@ -77,7 +77,15 @@ defmodule Pleroma.Stats do
status_count = Repo.aggregate(User.Query.build(%{local: true}), :sum, :note_count) status_count = Repo.aggregate(User.Query.build(%{local: true}), :sum, :note_count)
user_count = Repo.aggregate(User.Query.build(%{local: true, active: true}), :count, :id) users_query =
from(u in User,
where: u.deactivated != true,
where: u.local == true,
where: not is_nil(u.nickname),
where: not u.invisible
)
user_count = Repo.aggregate(users_query, :count, :id)
%{ %{
peers: peers, peers: peers,

View file

@ -832,6 +832,7 @@ defmodule Pleroma.User do
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, "friends_ap_ids:#{user.nickname}", get_user_friends_ap_ids(user))
{:ok, user} {:ok, user}
end end
@ -847,9 +848,22 @@ defmodule Pleroma.User do
end end
end end
def get_user_friends_ap_ids(user) do
from(u in User.get_friends_query(user), select: u.ap_id)
|> Repo.all()
end
@spec get_cached_user_friends_ap_ids(User.t()) :: [String.t()]
def get_cached_user_friends_ap_ids(user) do
Cachex.fetch!(:user_cache, "friends_ap_ids:#{user.ap_id}", fn _ ->
get_user_friends_ap_ids(user)
end)
end
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, "friends_ap_ids:#{user.ap_id}")
end end
@spec get_cached_by_ap_id(String.t()) :: User.t() | nil @spec get_cached_by_ap_id(String.t()) :: User.t() | nil
@ -1180,7 +1194,9 @@ defmodule Pleroma.User do
end end
@spec get_recipients_from_activity(Activity.t()) :: [User.t()] @spec get_recipients_from_activity(Activity.t()) :: [User.t()]
def get_recipients_from_activity(%Activity{recipients: to}) do def get_recipients_from_activity(%Activity{recipients: to, actor: actor}) do
to = [actor | to]
User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false}) User.Query.build(%{recipients_from_activity: to, local: true, deactivated: false})
|> Repo.all() |> Repo.all()
end end

View file

@ -54,13 +54,13 @@ defmodule Pleroma.User.Query do
select: term(), select: term(),
limit: pos_integer() limit: pos_integer()
} }
| %{} | map()
@ilike_criteria [:nickname, :name, :query] @ilike_criteria [:nickname, :name, :query]
@equal_criteria [:email] @equal_criteria [:email]
@contains_criteria [:ap_id, :nickname] @contains_criteria [:ap_id, :nickname]
@spec build(criteria()) :: Query.t() @spec build(Query.t(), criteria()) :: Query.t()
def build(query \\ base_query(), criteria) do def build(query \\ base_query(), criteria) do
prepare_query(query, criteria) prepare_query(query, criteria)
end end

View file

@ -118,9 +118,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
def increase_poll_votes_if_vote(%{ def increase_poll_votes_if_vote(%{
"object" => %{"inReplyTo" => reply_ap_id, "name" => name}, "object" => %{"inReplyTo" => reply_ap_id, "name" => name},
"type" => "Create" "type" => "Create",
"actor" => actor
}) do }) do
Object.increase_vote_count(reply_ap_id, name) Object.increase_vote_count(reply_ap_id, name, actor)
end end
def increase_poll_votes_if_vote(_create_data), do: :noop def increase_poll_votes_if_vote(_create_data), do: :noop
@ -397,36 +398,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
end end
end end
# TODO: This is weird, maybe we shouldn't check here if we can make the activity.
@spec like(User.t(), Object.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t(), Object.t()} | {:error, any()}
def like(user, object, activity_id \\ nil, local \\ true) do
with {:ok, result} <- Repo.transaction(fn -> do_like(user, object, activity_id, local) end) do
result
end
end
defp do_like(
%User{ap_id: ap_id} = user,
%Object{data: %{"id" => _}} = object,
activity_id,
local
) do
with nil <- get_existing_like(ap_id, object),
like_data <- make_like_data(user, object, activity_id),
{:ok, activity} <- insert(like_data, local),
{:ok, object} <- add_like_to_object(activity, object),
:ok <- maybe_federate(activity) do
{:ok, activity, object}
else
%Activity{} = activity ->
{:ok, activity, object}
{:error, error} ->
Repo.rollback(error)
end
end
@spec unlike(User.t(), Object.t(), String.t() | nil, boolean()) :: @spec unlike(User.t(), Object.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t(), Activity.t(), Object.t()} | {:ok, Object.t()} | {:error, any()} {:ok, Activity.t(), Activity.t(), Object.t()} | {:ok, Object.t()} | {:error, any()}
def unlike(%User{} = actor, %Object{} = object, activity_id \\ nil, local \\ true) do def unlike(%User{} = actor, %Object{} = object, activity_id \\ nil, local \\ true) do
@ -467,6 +438,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
defp do_announce(user, object, activity_id, local, public) do defp do_announce(user, object, activity_id, local, public) do
with true <- is_announceable?(object, user, public), with true <- is_announceable?(object, user, public),
object <- Object.get_by_id(object.id),
announce_data <- make_announce_data(user, object, activity_id, public), announce_data <- make_announce_data(user, object, activity_id, public),
{:ok, activity} <- insert(announce_data, local), {:ok, activity} <- insert(announce_data, local),
{:ok, object} <- add_announce_to_object(activity, object), {:ok, object} <- add_announce_to_object(activity, object),
@ -1076,6 +1048,41 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
) )
end end
defp restrict_replies(query, %{
"reply_filtering_user" => user,
"reply_visibility" => "self"
}) do
from(
[activity, object] in query,
where:
fragment(
"?->>'inReplyTo' is null OR ? = ANY(?)",
object.data,
^user.ap_id,
activity.recipients
)
)
end
defp restrict_replies(query, %{
"reply_filtering_user" => user,
"reply_visibility" => "following"
}) do
from(
[activity, object] in query,
where:
fragment(
"?->>'inReplyTo' is null OR ? && array_remove(?, ?) OR ? = ?",
object.data,
^[user.ap_id | User.get_cached_user_friends_ap_ids(user)],
activity.recipients,
activity.actor,
activity.actor,
^user.ap_id
)
)
end
defp restrict_replies(query, _), do: query defp restrict_replies(query, _), do: query
defp restrict_reblogs(query, %{"exclude_reblogs" => val}) when val == "true" or val == "1" do defp restrict_reblogs(query, %{"exclude_reblogs" => val}) when val == "true" or val == "1" do
@ -1290,6 +1297,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> maybe_set_thread_muted_field(opts) |> maybe_set_thread_muted_field(opts)
|> maybe_order(opts) |> maybe_order(opts)
|> restrict_recipients(recipients, opts["user"]) |> restrict_recipients(recipients, opts["user"])
|> restrict_replies(opts)
|> restrict_tag(opts) |> restrict_tag(opts)
|> restrict_tag_reject(opts) |> restrict_tag_reject(opts)
|> restrict_tag_all(opts) |> restrict_tag_all(opts)
@ -1304,7 +1312,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
|> restrict_media(opts) |> restrict_media(opts)
|> restrict_visibility(opts) |> restrict_visibility(opts)
|> restrict_thread_visibility(opts, config) |> restrict_thread_visibility(opts, config)
|> restrict_replies(opts)
|> restrict_reblogs(opts) |> restrict_reblogs(opts)
|> restrict_pinned(opts) |> restrict_pinned(opts)
|> restrict_muted_reblogs(restrict_muted_reblogs_opts) |> restrict_muted_reblogs(restrict_muted_reblogs_opts)

View file

@ -12,8 +12,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
alias Pleroma.Plugs.EnsureAuthenticatedPlug alias Pleroma.Plugs.EnsureAuthenticatedPlug
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.ActivityPub.Builder
alias Pleroma.Web.ActivityPub.InternalFetchActor alias Pleroma.Web.ActivityPub.InternalFetchActor
alias Pleroma.Web.ActivityPub.ObjectView alias Pleroma.Web.ActivityPub.ObjectView
alias Pleroma.Web.ActivityPub.Pipeline
alias Pleroma.Web.ActivityPub.Relay alias Pleroma.Web.ActivityPub.Relay
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.ActivityPub.UserView alias Pleroma.Web.ActivityPub.UserView
@ -421,7 +423,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubController do
defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do defp handle_user_activity(%User{} = user, %{"type" => "Like"} = params) do
with %Object{} = object <- Object.normalize(params["object"]), with %Object{} = object <- Object.normalize(params["object"]),
{:ok, activity, _object} <- ActivityPub.like(user, object) do {_, {:ok, like_object, meta}} <- {:build_object, Builder.like(user, object)},
{_, {:ok, %Activity{} = activity, _meta}} <-
{:common_pipeline,
Pipeline.common_pipeline(like_object, Keyword.put(meta, :local, true))} do
{:ok, activity} {:ok, activity}
else else
_ -> {:error, dgettext("errors", "Can't like object")} _ -> {:error, dgettext("errors", "Can't like object")}

View file

@ -15,12 +15,17 @@ defmodule Pleroma.Web.ActivityPub.SideEffects do
# - Add like to object # - Add like to object
# - Set up notification # - Set up notification
def handle(%{data: %{"type" => "Like"}} = object, meta) do def handle(%{data: %{"type" => "Like"}} = object, meta) do
{:ok, result} =
Pleroma.Repo.transaction(fn ->
liked_object = Object.get_by_ap_id(object.data["object"]) liked_object = Object.get_by_ap_id(object.data["object"])
Utils.add_like_to_object(object, liked_object) Utils.add_like_to_object(object, liked_object)
Notification.create_notifications(object) Notification.create_notifications(object)
{:ok, object, meta} {:ok, object, meta}
end)
result
end end
# Nothing to do # Nothing to do

View file

@ -8,15 +8,16 @@ defmodule Pleroma.Web.AdminAPI.StatusView do
require Pleroma.Constants require Pleroma.Constants
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.MastodonAPI.StatusView
def render("index.json", opts) do def render("index.json", opts) do
safe_render_many(opts.activities, __MODULE__, "show.json", opts) safe_render_many(opts.activities, __MODULE__, "show.json", opts)
end end
def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do def render("show.json", %{activity: %{data: %{"object" => _object}} = activity} = opts) do
user = get_user(activity.data["actor"]) user = StatusView.get_user(activity.data["actor"])
Pleroma.Web.MastodonAPI.StatusView.render("show.json", opts) StatusView.render("show.json", opts)
|> Map.merge(%{account: merge_account_views(user)}) |> Map.merge(%{account: merge_account_views(user)})
end end
@ -26,17 +27,4 @@ defmodule Pleroma.Web.AdminAPI.StatusView do
end end
defp merge_account_views(_), do: %{} defp merge_account_views(_), do: %{}
defp get_user(ap_id) do
cond do
user = User.get_cached_by_ap_id(ap_id) ->
user
user = User.get_by_guessed_nickname(ap_id) ->
user
true ->
User.error_user(ap_id)
end
end
end end

View file

@ -6,8 +6,6 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do
alias OpenApiSpex.Operation alias OpenApiSpex.Operation
alias OpenApiSpex.Schema alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Helpers alias Pleroma.Web.ApiSpec.Helpers
alias Pleroma.Web.ApiSpec.Schemas.AppCreateRequest
alias Pleroma.Web.ApiSpec.Schemas.AppCreateResponse
@spec open_api_operation(atom) :: Operation.t() @spec open_api_operation(atom) :: Operation.t()
def open_api_operation(action) do def open_api_operation(action) do
@ -22,9 +20,9 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do
summary: "Create an application", summary: "Create an application",
description: "Create a new application to obtain OAuth2 credentials", description: "Create a new application to obtain OAuth2 credentials",
operationId: "AppController.create", operationId: "AppController.create",
requestBody: Helpers.request_body("Parameters", AppCreateRequest, required: true), requestBody: Helpers.request_body("Parameters", create_request(), required: true),
responses: %{ responses: %{
200 => Operation.response("App", "application/json", AppCreateResponse), 200 => Operation.response("App", "application/json", create_response()),
422 => 422 =>
Operation.response( Operation.response(
"Unprocessable Entity", "Unprocessable Entity",
@ -93,4 +91,58 @@ defmodule Pleroma.Web.ApiSpec.AppOperation do
} }
} }
end end
defp create_request do
%Schema{
title: "AppCreateRequest",
description: "POST body for creating an app",
type: :object,
properties: %{
client_name: %Schema{type: :string, description: "A name for your application."},
redirect_uris: %Schema{
type: :string,
description:
"Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter."
},
scopes: %Schema{
type: :string,
description: "Space separated list of scopes",
default: "read"
},
website: %Schema{type: :string, description: "A URL to the homepage of your app"}
},
required: [:client_name, :redirect_uris],
example: %{
"client_name" => "My App",
"redirect_uris" => "https://myapp.com/auth/callback",
"website" => "https://myapp.com/"
}
}
end
defp create_response do
%Schema{
title: "AppCreateResponse",
description: "Response schema for an app",
type: :object,
properties: %{
id: %Schema{type: :string},
name: %Schema{type: :string},
client_id: %Schema{type: :string},
client_secret: %Schema{type: :string},
redirect_uri: %Schema{type: :string},
vapid_key: %Schema{type: :string},
website: %Schema{type: :string, nullable: true}
},
example: %{
"id" => "123",
"name" => "My App",
"client_id" => "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM",
"client_secret" => "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw",
"vapid_key" =>
"BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=",
"website" => "https://myapp.com/"
}
}
end
end end

View file

@ -0,0 +1,61 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.CustomEmojiOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.CustomEmoji
def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end
def index_operation do
%Operation{
tags: ["custom_emojis"],
summary: "List custom custom emojis",
description: "Returns custom emojis that are available on the server.",
operationId: "CustomEmojiController.index",
responses: %{
200 => Operation.response("Custom Emojis", "application/json", custom_emojis_resposnse())
}
}
end
defp custom_emojis_resposnse do
%Schema{
title: "CustomEmojisResponse",
description: "Response schema for custom emojis",
type: :array,
items: CustomEmoji,
example: [
%{
"category" => "Fun",
"shortcode" => "blank",
"static_url" => "https://lain.com/emoji/blank.png",
"tags" => ["Fun"],
"url" => "https://lain.com/emoji/blank.png",
"visible_in_picker" => false
},
%{
"category" => "Gif,Fun",
"shortcode" => "firefox",
"static_url" => "https://lain.com/emoji/Firefox.gif",
"tags" => ["Gif", "Fun"],
"url" => "https://lain.com/emoji/Firefox.gif",
"visible_in_picker" => true
},
%{
"category" => "pack:mixed",
"shortcode" => "sadcat",
"static_url" => "https://lain.com/emoji/mixed/sadcat.png",
"tags" => ["pack:mixed"],
"url" => "https://lain.com/emoji/mixed/sadcat.png",
"visible_in_picker" => true
}
]
}
end
end

View file

@ -6,8 +6,6 @@ defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do
alias OpenApiSpex.Operation alias OpenApiSpex.Operation
alias OpenApiSpex.Schema alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Helpers alias Pleroma.Web.ApiSpec.Helpers
alias Pleroma.Web.ApiSpec.Schemas.DomainBlockRequest
alias Pleroma.Web.ApiSpec.Schemas.DomainBlocksResponse
def open_api_operation(action) do def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation") operation = String.to_existing_atom("#{action}_operation")
@ -22,7 +20,13 @@ defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do
security: [%{"oAuth" => ["follow", "read:blocks"]}], security: [%{"oAuth" => ["follow", "read:blocks"]}],
operationId: "DomainBlockController.index", operationId: "DomainBlockController.index",
responses: %{ responses: %{
200 => Operation.response("Domain blocks", "application/json", DomainBlocksResponse) 200 =>
Operation.response("Domain blocks", "application/json", %Schema{
description: "Response schema for domain blocks",
type: :array,
items: %Schema{type: :string},
example: ["google.com", "facebook.com"]
})
} }
} }
end end
@ -40,7 +44,7 @@ defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do
- prevent following new users from it (but does not remove existing follows) - prevent following new users from it (but does not remove existing follows)
""", """,
operationId: "DomainBlockController.create", operationId: "DomainBlockController.create",
requestBody: Helpers.request_body("Parameters", DomainBlockRequest, required: true), requestBody: domain_block_request(),
security: [%{"oAuth" => ["follow", "write:blocks"]}], security: [%{"oAuth" => ["follow", "write:blocks"]}],
responses: %{ responses: %{
200 => Operation.response("Empty object", "application/json", %Schema{type: :object}) 200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
@ -54,11 +58,28 @@ defmodule Pleroma.Web.ApiSpec.DomainBlockOperation do
summary: "Unblock a domain", summary: "Unblock a domain",
description: "Remove a domain block, if it exists in the user's array of blocked domains.", description: "Remove a domain block, if it exists in the user's array of blocked domains.",
operationId: "DomainBlockController.delete", operationId: "DomainBlockController.delete",
requestBody: Helpers.request_body("Parameters", DomainBlockRequest, required: true), requestBody: domain_block_request(),
security: [%{"oAuth" => ["follow", "write:blocks"]}], security: [%{"oAuth" => ["follow", "write:blocks"]}],
responses: %{ responses: %{
200 => Operation.response("Empty object", "application/json", %Schema{type: :object}) 200 => Operation.response("Empty object", "application/json", %Schema{type: :object})
} }
} }
end end
defp domain_block_request do
Helpers.request_body(
"Parameters",
%Schema{
type: :object,
properties: %{
domain: %Schema{type: :string}
},
required: [:domain]
},
required: true,
example: %{
"domain" => "facebook.com"
}
)
end
end end

View file

@ -1,33 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.AppCreateRequest do
alias OpenApiSpex.Schema
require OpenApiSpex
OpenApiSpex.schema(%{
title: "AppCreateRequest",
description: "POST body for creating an app",
type: :object,
properties: %{
client_name: %Schema{type: :string, description: "A name for your application."},
redirect_uris: %Schema{
type: :string,
description:
"Where the user should be redirected after authorization. To display the authorization code to the user instead of redirecting to a web page, use `urn:ietf:wg:oauth:2.0:oob` in this parameter."
},
scopes: %Schema{
type: :string,
description: "Space separated list of scopes. If none is provided, defaults to `read`."
},
website: %Schema{type: :string, description: "A URL to the homepage of your app"}
},
required: [:client_name, :redirect_uris],
example: %{
"client_name" => "My App",
"redirect_uris" => "https://myapp.com/auth/callback",
"website" => "https://myapp.com/"
}
})
end

View file

@ -1,33 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.AppCreateResponse do
alias OpenApiSpex.Schema
require OpenApiSpex
OpenApiSpex.schema(%{
title: "AppCreateResponse",
description: "Response schema for an app",
type: :object,
properties: %{
id: %Schema{type: :string},
name: %Schema{type: :string},
client_id: %Schema{type: :string},
client_secret: %Schema{type: :string},
redirect_uri: %Schema{type: :string},
vapid_key: %Schema{type: :string},
website: %Schema{type: :string, nullable: true}
},
example: %{
"id" => "123",
"name" => "My App",
"client_id" => "TWhM-tNSuncnqN7DBJmoyeLnk6K3iJJ71KKXxgL1hPM",
"client_secret" => "ZEaFUFmF0umgBX1qKJDjaU99Q31lDkOU8NutzTOoliw",
"vapid_key" =>
"BCk-QqERU0q-CfYZjcuB6lnyyOYfJ2AifKqfeGIm7Z-HiTU5T9eTG5GxVA0_OH5mMlI4UkkDTpaZwozy0TzdZ2M=",
"website" => "https://myapp.com/"
}
})
end

View file

@ -0,0 +1,30 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.CustomEmoji do
alias OpenApiSpex.Schema
require OpenApiSpex
OpenApiSpex.schema(%{
title: "CustomEmoji",
description: "Response schema for an CustomEmoji",
type: :object,
properties: %{
shortcode: %Schema{type: :string},
url: %Schema{type: :string},
static_url: %Schema{type: :string},
visible_in_picker: %Schema{type: :boolean},
category: %Schema{type: :string},
tags: %Schema{type: :array}
},
example: %{
"shortcode" => "aaaa",
"url" => "https://files.mastodon.social/custom_emojis/images/000/007/118/original/aaaa.png",
"static_url" =>
"https://files.mastodon.social/custom_emojis/images/000/007/118/static/aaaa.png",
"visible_in_picker" => true
}
})
end

View file

@ -1,20 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.DomainBlockRequest do
alias OpenApiSpex.Schema
require OpenApiSpex
OpenApiSpex.schema(%{
title: "DomainBlockRequest",
type: :object,
properties: %{
domain: %Schema{type: :string}
},
required: [:domain],
example: %{
"domain" => "facebook.com"
}
})
end

View file

@ -1,16 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.Schemas.DomainBlocksResponse do
require OpenApiSpex
alias OpenApiSpex.Schema
OpenApiSpex.schema(%{
title: "DomainBlocksResponse",
description: "Response schema for domain blocks",
type: :array,
items: %Schema{type: :string},
example: ["google.com", "facebook.com"]
})
end

View file

@ -84,14 +84,18 @@ defmodule Pleroma.Web.CommonAPI.ActivityDraft do
%__MODULE__{draft | attachments: attachments} %__MODULE__{draft | attachments: attachments}
end end
defp in_reply_to(draft) do defp in_reply_to(%{params: %{"in_reply_to_status_id" => ""}} = draft), do: draft
case Map.get(draft.params, "in_reply_to_status_id") do
"" -> draft defp in_reply_to(%{params: %{"in_reply_to_status_id" => id}} = draft) when is_binary(id) do
nil -> draft %__MODULE__{draft | in_reply_to: Activity.get_by_id(id)}
id -> %__MODULE__{draft | in_reply_to: Activity.get_by_id(id)}
end end
defp in_reply_to(%{params: %{"in_reply_to_status_id" => %Activity{} = in_reply_to}} = draft) do
%__MODULE__{draft | in_reply_to: in_reply_to}
end end
defp in_reply_to(draft), do: draft
defp in_reply_to_conversation(draft) do defp in_reply_to_conversation(draft) do
in_reply_to_conversation = Participation.get(draft.params["in_reply_to_conversation_id"]) in_reply_to_conversation = Participation.get(draft.params["in_reply_to_conversation_id"])
%__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation} %__MODULE__{draft | in_reply_to_conversation: in_reply_to_conversation}

View file

@ -86,8 +86,9 @@ defmodule Pleroma.Web.CommonAPI do
end end
end end
def repeat(id_or_ap_id, user, params \\ %{}) do def repeat(id, user, params \\ %{}) do
with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)}, with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)},
object <- Object.normalize(activity), object <- Object.normalize(activity),
announce_activity <- Utils.get_existing_announce(user.ap_id, object), announce_activity <- Utils.get_existing_announce(user.ap_id, object),
public <- public_announce?(object, params) do public <- public_announce?(object, params) do
@ -102,8 +103,9 @@ defmodule Pleroma.Web.CommonAPI do
end end
end end
def unrepeat(id_or_ap_id, user) do def unrepeat(id, user) do
with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)} do
object = Object.normalize(activity) object = Object.normalize(activity)
ActivityPub.unannounce(user, object) ActivityPub.unannounce(user, object)
else else
@ -160,8 +162,9 @@ defmodule Pleroma.Web.CommonAPI do
end end
end end
def unfavorite(id_or_ap_id, user) do def unfavorite(id, user) do
with {_, %Activity{} = activity} <- {:find_activity, get_by_id_or_ap_id(id_or_ap_id)} do with {_, %Activity{data: %{"type" => "Create"}} = activity} <-
{:find_activity, Activity.get_by_id(id)} do
object = Object.normalize(activity) object = Object.normalize(activity)
ActivityPub.unlike(user, object) ActivityPub.unlike(user, object)
else else
@ -332,12 +335,12 @@ defmodule Pleroma.Web.CommonAPI do
defp maybe_create_activity_expiration(result, _), do: result defp maybe_create_activity_expiration(result, _), do: result
def pin(id_or_ap_id, %{ap_id: user_ap_id} = user) do def pin(id, %{ap_id: user_ap_id} = user) do
with %Activity{ with %Activity{
actor: ^user_ap_id, actor: ^user_ap_id,
data: %{"type" => "Create"}, data: %{"type" => "Create"},
object: %Object{data: %{"type" => object_type}} object: %Object{data: %{"type" => object_type}}
} = activity <- get_by_id_or_ap_id(id_or_ap_id), } = activity <- Activity.get_by_id_with_object(id),
true <- object_type in ["Note", "Article", "Question"], true <- object_type in ["Note", "Article", "Question"],
true <- Visibility.is_public?(activity), true <- Visibility.is_public?(activity),
{:ok, _user} <- User.add_pinnned_activity(user, activity) do {:ok, _user} <- User.add_pinnned_activity(user, activity) do
@ -348,8 +351,8 @@ defmodule Pleroma.Web.CommonAPI do
end end
end end
def unpin(id_or_ap_id, user) do def unpin(id, user) do
with %Activity{} = activity <- get_by_id_or_ap_id(id_or_ap_id), with %Activity{data: %{"type" => "Create"}} = activity <- Activity.get_by_id(id),
{:ok, _user} <- User.remove_pinnned_activity(user, activity) do {:ok, _user} <- User.remove_pinnned_activity(user, activity) do
{:ok, activity} {:ok, activity}
else else

View file

@ -22,24 +22,6 @@ defmodule Pleroma.Web.CommonAPI.Utils do
require Logger require Logger
require Pleroma.Constants require Pleroma.Constants
# This is a hack for twidere.
def get_by_id_or_ap_id(id) do
activity =
with true <- FlakeId.flake_id?(id),
%Activity{} = activity <- Activity.get_by_id_with_object(id) do
activity
else
_ -> Activity.get_create_by_object_ap_id_with_object(id)
end
activity &&
if activity.data["type"] == "Create" do
activity
else
Activity.get_create_by_object_ap_id_with_object(activity.data["object"])
end
end
def attachments_from_ids(%{"media_ids" => ids, "descriptions" => desc} = _) do def attachments_from_ids(%{"media_ids" => ids, "descriptions" => desc} = _) do
attachments_from_ids_descs(ids, desc) attachments_from_ids_descs(ids, desc)
end end

View file

@ -72,19 +72,24 @@ defmodule Pleroma.Web.Federator do
# actor shouldn't be acting on objects outside their own AP server. # actor shouldn't be acting on objects outside their own AP server.
with {:ok, _user} <- ap_enabled_actor(params["actor"]), with {:ok, _user} <- ap_enabled_actor(params["actor"]),
nil <- Activity.normalize(params["id"]), nil <- Activity.normalize(params["id"]),
:ok <- Containment.contain_origin_from_id(params["actor"], params), {_, :ok} <-
{:correct_origin?, Containment.contain_origin_from_id(params["actor"], params)},
{:ok, activity} <- Transmogrifier.handle_incoming(params) do {:ok, activity} <- Transmogrifier.handle_incoming(params) do
{:ok, activity} {:ok, activity}
else else
{:correct_origin?, _} ->
Logger.debug("Origin containment failure for #{params["id"]}")
{:error, :origin_containment_failed}
%Activity{} -> %Activity{} ->
Logger.debug("Already had #{params["id"]}") Logger.debug("Already had #{params["id"]}")
:error {:error, :already_present}
_e -> e ->
# Just drop those for now # Just drop those for now
Logger.debug("Unhandled activity") Logger.debug("Unhandled activity")
Logger.debug(Jason.encode!(params, pretty: true)) Logger.debug(Jason.encode!(params, pretty: true))
:error {:error, e}
end end
end end

View file

@ -293,7 +293,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "POST /api/v1/accounts/:id/follow" @doc "POST /api/v1/accounts/:id/follow"
def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do def follow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
{:error, :not_found} {:error, "Can not follow yourself"}
end end
def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do def follow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
@ -306,7 +306,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
@doc "POST /api/v1/accounts/:id/unfollow" @doc "POST /api/v1/accounts/:id/unfollow"
def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do def unfollow(%{assigns: %{user: %{id: id}, account: %{id: id}}}, _params) do
{:error, :not_found} {:error, "Can not unfollow yourself"}
end end
def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do def unfollow(%{assigns: %{user: follower, account: followed}} = conn, _params) do
@ -356,14 +356,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
end end
@doc "POST /api/v1/follows" @doc "POST /api/v1/follows"
def follows(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do def follows(conn, %{"uri" => uri}) do
with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)}, case User.get_cached_by_nickname(uri) do
{_, true} <- {:followed, follower.id != followed.id}, %User{} = user ->
{:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do conn
render(conn, "show.json", user: followed, for: follower) |> assign(:account, user)
else |> follow(%{})
{:followed, _} -> {:error, :not_found}
{:error, message} -> json_response(conn, :forbidden, %{error: message}) nil ->
{:error, :not_found}
end end
end end

View file

@ -5,6 +5,10 @@
defmodule Pleroma.Web.MastodonAPI.CustomEmojiController do defmodule Pleroma.Web.MastodonAPI.CustomEmojiController do
use Pleroma.Web, :controller use Pleroma.Web, :controller
plug(OpenApiSpex.Plug.CastAndValidate)
defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.CustomEmojiOperation
def index(conn, _params) do def index(conn, _params) do
render(conn, "index.json", custom_emojis: Pleroma.Emoji.get_all()) render(conn, "index.json", custom_emojis: Pleroma.Emoji.get_all())
end end

View file

@ -127,7 +127,8 @@ defmodule Pleroma.Web.MastodonAPI.StatusController do
def create( def create(
%{assigns: %{user: user}} = conn, %{assigns: %{user: user}} = conn,
%{"status" => _, "scheduled_at" => scheduled_at} = params %{"status" => _, "scheduled_at" => scheduled_at} = params
) do )
when not is_nil(scheduled_at) do
params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"]) params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])
with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)}, with {:far_enough, true} <- {:far_enough, ScheduledActivity.far_enough?(scheduled_at)},

View file

@ -37,6 +37,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> Map.put("type", ["Create", "Announce"]) |> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user) |> Map.put("blocking_user", user)
|> Map.put("muting_user", user) |> Map.put("muting_user", user)
|> Map.put("reply_filtering_user", user)
|> Map.put("user", user) |> Map.put("user", user)
recipients = [user.ap_id | User.following(user)] recipients = [user.ap_id | User.following(user)]
@ -100,6 +101,7 @@ defmodule Pleroma.Web.MastodonAPI.TimelineController do
|> Map.put("local_only", local_only) |> Map.put("local_only", local_only)
|> Map.put("blocking_user", user) |> Map.put("blocking_user", user)
|> Map.put("muting_user", user) |> Map.put("muting_user", user)
|> Map.put("reply_filtering_user", user)
|> ActivityPub.fetch_public_activities() |> ActivityPub.fetch_public_activities()
conn conn

View file

@ -19,6 +19,7 @@ defmodule Pleroma.Web.MastodonAPI.PollView do
expired: expired, expired: expired,
multiple: multiple, multiple: multiple,
votes_count: votes_count, votes_count: votes_count,
voters_count: (multiple || nil) && voters_count(object),
options: options, options: options,
voted: voted?(params), voted: voted?(params),
emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"]) emojis: Pleroma.Web.MastodonAPI.StatusView.build_emojis(object.data["emoji"])
@ -62,6 +63,12 @@ defmodule Pleroma.Web.MastodonAPI.PollView do
end) end)
end end
defp voters_count(%{data: %{"voters" => [_ | _] = voters}}) do
length(voters)
end
defp voters_count(_), do: 0
defp voted?(%{object: object} = opts) do defp voted?(%{object: object} = opts) do
if opts[:for] do if opts[:for] do
existing_votes = Pleroma.Web.ActivityPub.Utils.get_existing_votes(opts[:for].ap_id, object) existing_votes = Pleroma.Web.ActivityPub.Utils.get_existing_votes(opts[:for].ap_id, object)

View file

@ -45,7 +45,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
end) end)
end end
defp get_user(ap_id) do def get_user(ap_id, fake_record_fallback \\ true) do
cond do cond do
user = User.get_cached_by_ap_id(ap_id) -> user = User.get_cached_by_ap_id(ap_id) ->
user user
@ -53,8 +53,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
user = User.get_by_guessed_nickname(ap_id) -> user = User.get_by_guessed_nickname(ap_id) ->
user user
true -> fake_record_fallback ->
# TODO: refactor (fake records is never a good idea)
User.error_user(ap_id) User.error_user(ap_id)
true ->
nil
end end
end end
@ -97,7 +101,11 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
UserRelationship.view_relationships_option(nil, []) UserRelationship.view_relationships_option(nil, [])
true -> true ->
actors = Enum.map(activities ++ parent_activities, &get_user(&1.data["actor"])) # Note: unresolved users are filtered out
actors =
(activities ++ parent_activities)
|> Enum.map(&get_user(&1.data["actor"], false))
|> Enum.filter(& &1)
UserRelationship.view_relationships_option(reading_user, actors, UserRelationship.view_relationships_option(reading_user, actors,
source_mutes_only: opts[:skip_relationships] source_mutes_only: opts[:skip_relationships]

View file

@ -17,12 +17,8 @@ defmodule Pleroma.Web.OAuth.Scopes do
""" """
@spec fetch_scopes(map() | struct(), list()) :: list() @spec fetch_scopes(map() | struct(), list()) :: list()
def fetch_scopes(%Pleroma.Web.ApiSpec.Schemas.AppCreateRequest{scopes: scopes}, default) do
parse_scopes(scopes, default)
end
def fetch_scopes(params, default) do def fetch_scopes(params, default) do
parse_scopes(params["scope"] || params["scopes"], default) parse_scopes(params["scope"] || params["scopes"] || params[:scopes], default)
end end
def parse_scopes(scopes, _default) when is_list(scopes) do def parse_scopes(scopes, _default) when is_list(scopes) do

View file

@ -55,11 +55,12 @@ defmodule Pleroma.Web.Push.Impl do
|> Jason.encode!() |> Jason.encode!()
|> push_message(build_sub(subscription), gcm_api_key, subscription) |> push_message(build_sub(subscription), gcm_api_key, subscription)
end end
|> (&{:ok, &1}).()
end end
def perform(_) do def perform(_) do
Logger.warn("Unknown notification type") Logger.warn("Unknown notification type")
:error {:error, :unknown_type}
end end
@doc "Push message to web" @doc "Push message to web"

View file

@ -158,24 +158,6 @@ defmodule Pleroma.Web.Streamer.Worker do
should_send?(user, activity) should_send?(user, activity)
end end
def push_to_socket(topics, topic, %Activity{data: %{"type" => "Announce"}} = item) do
Enum.each(topics[topic] || [], fn %StreamerSocket{
transport_pid: transport_pid,
user: socket_user
} ->
# Get the current user so we have up-to-date blocks etc.
if socket_user do
user = User.get_cached_by_ap_id(socket_user.ap_id)
if should_send?(user, item) do
send(transport_pid, {:text, StreamerView.render("update.json", item, user)})
end
else
send(transport_pid, {:text, StreamerView.render("update.json", item)})
end
end)
end
def push_to_socket(topics, topic, %Participation{} = participation) do def push_to_socket(topics, topic, %Participation{} = participation) do
Enum.each(topics[topic] || [], fn %StreamerSocket{transport_pid: transport_pid} -> Enum.each(topics[topic] || [], fn %StreamerSocket{transport_pid: transport_pid} ->
send(transport_pid, {:text, StreamerView.render("conversation.json", participation)}) send(transport_pid, {:text, StreamerView.render("conversation.json", participation)})

View file

@ -1,8 +1,8 @@
<%= case @mediaType do %> <%= case @mediaType do %>
<% "audio" -> %> <% "audio" -> %>
<audio src="<%= @url %>" controls="controls"></audio> <audio class="u-audio" src="<%= @url %>" controls="controls"></audio>
<% "video" -> %> <% "video" -> %>
<video src="<%= @url %>" controls="controls"></video> <video class="u-video" src="<%= @url %>" controls="controls"></video>
<% _ -> %> <% _ -> %>
<img src="<%= @url %>" alt="<%= @name %>" title="<%= @name %>"> <img class="u-photo" src="<%= @url %>" alt="<%= @name %>" title="<%= @name %>">
<% end %> <% end %>

View file

@ -1,12 +1,16 @@
<div class="activity" <%= if @selected do %> id="selected" <% end %>> <div class="activity h-entry" <%= if @selected do %> id="selected" <% end %>>
<p class="pull-right"> <p class="pull-right">
<%= link format_date(@published), to: @link, class: "activity-link" %> <a class="activity-link u-url u-uid" href="<%= @link %>">
<time class="dt-published" datetime="<%= @published %>">
<%= format_date(@published) %>
</time>
</a>
</p> </p>
<%= render("_user_card.html", %{user: @user}) %> <%= render("_user_card.html", %{user: @user}) %>
<div class="activity-content"> <div class="activity-content">
<%= if @title != "" do %> <%= if @title != "" do %>
<details <%= if open_content?() do %>open<% end %>> <details <%= if open_content?() do %>open<% end %>>
<summary><%= raw @title %></summary> <summary class="p-name"><%= raw @title %></summary>
<div class="e-content"><%= raw @content %></div> <div class="e-content"><%= raw @content %></div>
</details> </details>
<% else %> <% else %>

View file

@ -1,10 +1,10 @@
<div class="p-author h-card"> <div class="p-author h-card">
<a class="u-url" rel="author noopener" href="<%= (@user.uri || @user.ap_id) %>"> <a class="u-url" rel="author noopener" href="<%= (@user.uri || @user.ap_id) %>">
<div class="avatar"> <div class="avatar">
<img src="<%= User.avatar_url(@user) |> MediaProxy.url %>" width="48" height="48" alt=""> <img class="u-photo" src="<%= User.avatar_url(@user) |> MediaProxy.url %>" width="48" height="48" alt="">
</div> </div>
<span class="display-name"> <span class="display-name">
<bdi><%= raw Formatter.emojify(@user.name, @user.emoji) %></bdi> <bdi class="p-name"><%= raw Formatter.emojify(@user.name, @user.emoji) %></bdi>
<span class="nickname"><%= @user.nickname %></span> <span class="nickname"><%= @user.nickname %></span>
</span> </span>
</a> </a>

View file

@ -199,27 +199,27 @@ defmodule Pleroma.Web.TwitterAPI.UtilController do
end end
def follow_import(%{assigns: %{user: follower}} = conn, %{"list" => list}) do def follow_import(%{assigns: %{user: follower}} = conn, %{"list" => list}) do
with lines <- String.split(list, "\n"), followed_identifiers =
followed_identifiers <- list
Enum.map(lines, fn line -> |> String.split("\n")
String.split(line, ",") |> List.first() |> Enum.map(&(&1 |> String.split(",") |> List.first()))
end) |> List.delete("Account address")
|> List.delete("Account address") do |> Enum.map(&(&1 |> String.trim() |> String.trim_leading("@")))
|> Enum.reject(&(&1 == ""))
User.follow_import(follower, followed_identifiers) User.follow_import(follower, followed_identifiers)
json(conn, "job started") json(conn, "job started")
end end
end
def blocks_import(conn, %{"list" => %Plug.Upload{} = listfile}) do def blocks_import(conn, %{"list" => %Plug.Upload{} = listfile}) do
blocks_import(conn, %{"list" => File.read!(listfile.path)}) blocks_import(conn, %{"list" => File.read!(listfile.path)})
end end
def blocks_import(%{assigns: %{user: blocker}} = conn, %{"list" => list}) do def blocks_import(%{assigns: %{user: blocker}} = conn, %{"list" => list}) do
with blocked_identifiers <- String.split(list) do blocked_identifiers = list |> String.split() |> Enum.map(&String.trim_leading(&1, "@"))
User.blocks_import(blocker, blocked_identifiers) User.blocks_import(blocker, blocked_identifiers)
json(conn, "job started") json(conn, "job started")
end end
end
def change_password(%{assigns: %{user: user}} = conn, params) do def change_password(%{assigns: %{user: user}} = conn, params) do
case CommonAPI.Utils.confirm_current_password(user, params["password"]) do case CommonAPI.Utils.confirm_current_password(user, params["password"]) do

View file

@ -35,7 +35,7 @@ defmodule Pleroma.Workers.BackgroundWorker do
_job _job
) do ) do
blocker = User.get_cached_by_id(blocker_id) blocker = User.get_cached_by_id(blocker_id)
User.perform(:blocks_import, blocker, blocked_identifiers) {:ok, User.perform(:blocks_import, blocker, blocked_identifiers)}
end end
def perform( def perform(
@ -47,7 +47,7 @@ defmodule Pleroma.Workers.BackgroundWorker do
_job _job
) do ) do
follower = User.get_cached_by_id(follower_id) follower = User.get_cached_by_id(follower_id)
User.perform(:follow_import, follower, followed_identifiers) {:ok, User.perform(:follow_import, follower, followed_identifiers)}
end end
def perform(%{"op" => "media_proxy_preload", "message" => message}, _job) do def perform(%{"op" => "media_proxy_preload", "message" => message}, _job) do

View file

@ -16,6 +16,7 @@ defmodule Pleroma.Config.TransferTaskTest do
refute Application.get_env(:pleroma, :test_key) refute Application.get_env(:pleroma, :test_key)
refute Application.get_env(:idna, :test_key) refute Application.get_env(:idna, :test_key)
refute Application.get_env(:quack, :test_key) refute Application.get_env(:quack, :test_key)
refute Application.get_env(:postgrex, :test_key)
initial = Application.get_env(:logger, :level) initial = Application.get_env(:logger, :level)
ConfigDB.create(%{ ConfigDB.create(%{
@ -36,6 +37,12 @@ defmodule Pleroma.Config.TransferTaskTest do
value: [:test_value1, :test_value2] value: [:test_value1, :test_value2]
}) })
ConfigDB.create(%{
group: ":postgrex",
key: ":test_key",
value: :value
})
ConfigDB.create(%{group: ":logger", key: ":level", value: :debug}) ConfigDB.create(%{group: ":logger", key: ":level", value: :debug})
TransferTask.start_link([]) TransferTask.start_link([])
@ -44,11 +51,13 @@ defmodule Pleroma.Config.TransferTaskTest do
assert Application.get_env(:idna, :test_key) == [live: 15, com: 35] assert Application.get_env(:idna, :test_key) == [live: 15, com: 35]
assert Application.get_env(:quack, :test_key) == [:test_value1, :test_value2] assert Application.get_env(:quack, :test_key) == [:test_value1, :test_value2]
assert Application.get_env(:logger, :level) == :debug assert Application.get_env(:logger, :level) == :debug
assert Application.get_env(:postgrex, :test_key) == :value
on_exit(fn -> on_exit(fn ->
Application.delete_env(:pleroma, :test_key) Application.delete_env(:pleroma, :test_key)
Application.delete_env(:idna, :test_key) Application.delete_env(:idna, :test_key)
Application.delete_env(:quack, :test_key) Application.delete_env(:quack, :test_key)
Application.delete_env(:postgrex, :test_key)
Application.put_env(:logger, :level, initial) Application.put_env(:logger, :level, initial)
end) end)
end end

View file

@ -7,3 +7,5 @@ config :pleroma, :second_setting, key: "value2", key2: ["Activity"]
config :quack, level: :info config :quack, level: :info
config :pleroma, Pleroma.Repo, pool: Ecto.Adapters.SQL.Sandbox config :pleroma, Pleroma.Repo, pool: Ecto.Adapters.SQL.Sandbox
config :postgrex, :json_library, Poison

View file

@ -2,11 +2,21 @@
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/> # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.StateTest do defmodule Pleroma.StatsTest do
use Pleroma.DataCase use Pleroma.DataCase
import Pleroma.Factory import Pleroma.Factory
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
describe "user count" do
test "it ignores internal users" do
_user = insert(:user, local: true)
_internal = insert(:user, local: true, nickname: nil)
_internal = Pleroma.Web.ActivityPub.Relay.get_actor()
assert match?(%{stats: %{user_count: 1}}, Pleroma.Stats.calculate_stat_data())
end
end
describe "status visibility count" do describe "status visibility count" do
test "on new status" do test "on new status" do
user = insert(:user) user = insert(:user)

View file

@ -0,0 +1,57 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Tests.ApiSpecHelpers do
@moduledoc """
OpenAPI spec test helpers
"""
import ExUnit.Assertions
alias OpenApiSpex.Cast.Error
alias OpenApiSpex.Reference
alias OpenApiSpex.Schema
def assert_schema(value, schema) do
api_spec = Pleroma.Web.ApiSpec.spec()
case OpenApiSpex.cast_value(value, schema, api_spec) do
{:ok, data} ->
data
{:error, errors} ->
errors =
Enum.map(errors, fn error ->
message = Error.message(error)
path = Error.path_to_string(error)
"#{message} at #{path}"
end)
flunk(
"Value does not conform to schema #{schema.title}: #{Enum.join(errors, "\n")}\n#{
inspect(value)
}"
)
end
end
def resolve_schema(%Schema{} = schema), do: schema
def resolve_schema(%Reference{} = ref) do
schemas = Pleroma.Web.ApiSpec.spec().components.schemas
Reference.resolve_schema(ref, schemas)
end
def api_operations do
paths = Pleroma.Web.ApiSpec.spec().paths
Enum.flat_map(paths, fn {_, path_item} ->
path_item
|> Map.take([:delete, :get, :head, :options, :patch, :post, :put, :trace])
|> Map.values()
|> Enum.reject(&is_nil/1)
|> Enum.uniq()
end)
end
end

View file

@ -51,6 +51,42 @@ defmodule Pleroma.Web.ConnCase do
%{user: user, token: token, conn: conn} %{user: user, token: token, conn: conn}
end end
defp json_response_and_validate_schema(conn, status \\ nil) do
content_type =
conn
|> Plug.Conn.get_resp_header("content-type")
|> List.first()
|> String.split(";")
|> List.first()
status = status || conn.status
%{private: %{open_api_spex: %{operation_id: op_id, operation_lookup: lookup, spec: spec}}} =
conn
schema = lookup[op_id].responses[status].content[content_type].schema
json = json_response(conn, status)
case OpenApiSpex.cast_value(json, schema, spec) do
{:ok, _data} ->
json
{:error, errors} ->
errors =
Enum.map(errors, fn error ->
message = OpenApiSpex.Cast.Error.message(error)
path = OpenApiSpex.Cast.Error.path_to_string(error)
"#{message} at #{path}"
end)
flunk(
"Response does not conform to schema of #{op_id} operation: #{
Enum.join(errors, "\n")
}\n#{inspect(json)}"
)
end
end
defp ensure_federating_or_authenticated(conn, url, user) do defp ensure_federating_or_authenticated(conn, url, user) do
initial_setting = Config.get([:instance, :federating]) initial_setting = Config.get([:instance, :federating])
on_exit(fn -> Config.put([:instance, :federating], initial_setting) end) on_exit(fn -> Config.put([:instance, :federating], initial_setting) end)

View file

@ -38,7 +38,7 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
on_exit(fn -> Application.put_env(:quack, :level, initial) end) on_exit(fn -> Application.put_env(:quack, :level, initial) end)
end end
test "settings are migrated to db" do test "filtered settings are migrated to db" do
assert Repo.all(ConfigDB) == [] assert Repo.all(ConfigDB) == []
Mix.Tasks.Pleroma.Config.migrate_to_db("test/fixtures/config/temp.secret.exs") Mix.Tasks.Pleroma.Config.migrate_to_db("test/fixtures/config/temp.secret.exs")
@ -47,6 +47,7 @@ defmodule Mix.Tasks.Pleroma.ConfigTest do
config2 = ConfigDB.get_by_params(%{group: ":pleroma", key: ":second_setting"}) config2 = ConfigDB.get_by_params(%{group: ":pleroma", key: ":second_setting"})
config3 = ConfigDB.get_by_params(%{group: ":quack", key: ":level"}) config3 = ConfigDB.get_by_params(%{group: ":quack", key: ":level"})
refute ConfigDB.get_by_params(%{group: ":pleroma", key: "Pleroma.Repo"}) refute ConfigDB.get_by_params(%{group: ":pleroma", key: "Pleroma.Repo"})
refute ConfigDB.get_by_params(%{group: ":postgrex", key: ":json_library"})
assert ConfigDB.from_binary(config1.value) == [key: "value", key2: [Repo]] assert ConfigDB.from_binary(config1.value) == [key: "value", key2: [Repo]]
assert ConfigDB.from_binary(config2.value) == [key: "value2", key2: ["Activity"]] assert ConfigDB.from_binary(config2.value) == [key: "value2", key2: ["Activity"]]

View file

@ -756,8 +756,8 @@ defmodule Pleroma.UserTest do
] ]
{:ok, job} = User.follow_import(user1, identifiers) {:ok, job} = User.follow_import(user1, identifiers)
result = ObanHelpers.perform(job)
assert {:ok, result} = ObanHelpers.perform(job)
assert is_list(result) assert is_list(result)
assert result == [user2, user3] assert result == [user2, user3]
end end
@ -979,14 +979,26 @@ defmodule Pleroma.UserTest do
] ]
{:ok, job} = User.blocks_import(user1, identifiers) {:ok, job} = User.blocks_import(user1, identifiers)
result = ObanHelpers.perform(job)
assert {:ok, result} = ObanHelpers.perform(job)
assert is_list(result) assert is_list(result)
assert result == [user2, user3] assert result == [user2, user3]
end end
end end
describe "get_recipients_from_activity" do describe "get_recipients_from_activity" do
test "works for announces" do
actor = insert(:user)
user = insert(:user, local: true)
{:ok, activity} = CommonAPI.post(actor, %{"status" => "hello"})
{:ok, announce, _} = CommonAPI.repeat(activity.id, user)
recipients = User.get_recipients_from_activity(announce)
assert user in recipients
end
test "get recipients" do test "get recipients" do
actor = insert(:user) actor = insert(:user)
user = insert(:user, local: true) user = insert(:user, local: true)

View file

@ -994,72 +994,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
end end
end end
describe "like an object" do
test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
Config.put([:instance, :federating], true)
note_activity = insert(:note_activity)
assert object_activity = Object.normalize(note_activity)
user = insert(:user)
{:ok, like_activity, _object} = ActivityPub.like(user, object_activity)
assert called(Federator.publish(like_activity))
end
test "returns exist activity if object already liked" do
note_activity = insert(:note_activity)
assert object_activity = Object.normalize(note_activity)
user = insert(:user)
{:ok, like_activity, _object} = ActivityPub.like(user, object_activity)
{:ok, like_activity_exist, _object} = ActivityPub.like(user, object_activity)
assert like_activity == like_activity_exist
end
test "reverts like activity on error" do
note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user)
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
assert {:error, :reverted} = ActivityPub.like(user, object)
end
assert Repo.aggregate(Activity, :count, :id) == 1
assert Repo.get(Object, object.id) == object
end
test "adds a like activity to the db" do
note_activity = insert(:note_activity)
assert object = Object.normalize(note_activity)
user = insert(:user)
user_two = insert(:user)
{:ok, like_activity, object} = ActivityPub.like(user, object)
assert like_activity.data["actor"] == user.ap_id
assert like_activity.data["type"] == "Like"
assert like_activity.data["object"] == object.data["id"]
assert like_activity.data["to"] == [User.ap_followers(user), note_activity.data["actor"]]
assert like_activity.data["context"] == object.data["context"]
assert object.data["like_count"] == 1
assert object.data["likes"] == [user.ap_id]
# Just return the original activity if the user already liked it.
{:ok, same_like_activity, object} = ActivityPub.like(user, object)
assert like_activity == same_like_activity
assert object.data["likes"] == [user.ap_id]
assert object.data["like_count"] == 1
{:ok, _like_activity, object} = ActivityPub.like(user_two, object)
assert object.data["like_count"] == 2
end
end
describe "unliking" do describe "unliking" do
test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
Config.put([:instance, :federating], true) Config.put([:instance, :federating], true)
@ -1071,7 +1005,8 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
{:ok, object} = ActivityPub.unlike(user, object) {:ok, object} = ActivityPub.unlike(user, object)
refute called(Federator.publish()) refute called(Federator.publish())
{:ok, _like_activity, object} = ActivityPub.like(user, object) {:ok, _like_activity} = CommonAPI.favorite(user, note_activity.id)
object = Object.get_by_id(object.id)
assert object.data["like_count"] == 1 assert object.data["like_count"] == 1
{:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object) {:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
@ -1082,10 +1017,10 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
test "reverts unliking on error" do test "reverts unliking on error" do
note_activity = insert(:note_activity) note_activity = insert(:note_activity)
object = Object.normalize(note_activity)
user = insert(:user) user = insert(:user)
{:ok, like_activity, object} = ActivityPub.like(user, object) {:ok, like_activity} = CommonAPI.favorite(user, note_activity.id)
object = Object.normalize(note_activity)
assert object.data["like_count"] == 1 assert object.data["like_count"] == 1
with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
@ -1106,7 +1041,9 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
{:ok, object} = ActivityPub.unlike(user, object) {:ok, object} = ActivityPub.unlike(user, object)
assert object.data["like_count"] == 0 assert object.data["like_count"] == 0
{:ok, like_activity, object} = ActivityPub.like(user, object) {:ok, like_activity} = CommonAPI.favorite(user, note_activity.id)
object = Object.get_by_id(object.id)
assert object.data["like_count"] == 1 assert object.data["like_count"] == 1
{:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object) {:ok, unlike_activity, _, object} = ActivityPub.unlike(user, object)
@ -1973,4 +1910,497 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
ActivityPub.move(old_user, new_user) ActivityPub.move(old_user, new_user)
end end
end end
test "doesn't retrieve replies activities with exclude_replies" do
user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "yeah"})
{:ok, _reply} =
CommonAPI.post(user, %{"status" => "yeah", "in_reply_to_status_id" => activity.id})
[result] = ActivityPub.fetch_public_activities(%{"exclude_replies" => "true"})
assert result.id == activity.id
assert length(ActivityPub.fetch_public_activities()) == 2
end
describe "replies filtering with public messages" do
setup :public_messages
test "public timeline", %{users: %{u1: user}} do
activities_ids =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", false)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("reply_filtering_user", user)
|> ActivityPub.fetch_public_activities()
|> Enum.map(& &1.id)
assert length(activities_ids) == 16
end
test "public timeline with reply_visibility `following`", %{
users: %{u1: user},
u1: u1,
u2: u2,
u3: u3,
u4: u4,
activities: activities
} do
activities_ids =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", false)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("reply_visibility", "following")
|> Map.put("reply_filtering_user", user)
|> ActivityPub.fetch_public_activities()
|> Enum.map(& &1.id)
assert length(activities_ids) == 14
visible_ids =
Map.values(u1) ++ Map.values(u2) ++ Map.values(u4) ++ Map.values(activities) ++ [u3[:r1]]
assert Enum.all?(visible_ids, &(&1 in activities_ids))
end
test "public timeline with reply_visibility `self`", %{
users: %{u1: user},
u1: u1,
u2: u2,
u3: u3,
u4: u4,
activities: activities
} do
activities_ids =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", false)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("reply_visibility", "self")
|> Map.put("reply_filtering_user", user)
|> ActivityPub.fetch_public_activities()
|> Enum.map(& &1.id)
assert length(activities_ids) == 10
visible_ids = Map.values(u1) ++ [u2[:r1], u3[:r1], u4[:r1]] ++ Map.values(activities)
assert Enum.all?(visible_ids, &(&1 in activities_ids))
end
test "home timeline", %{
users: %{u1: user},
activities: activities,
u1: u1,
u2: u2,
u3: u3,
u4: u4
} do
params =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
|> Map.put("reply_filtering_user", user)
activities_ids =
ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
|> Enum.map(& &1.id)
assert length(activities_ids) == 13
visible_ids =
Map.values(u1) ++
Map.values(u3) ++
[
activities[:a1],
activities[:a2],
activities[:a4],
u2[:r1],
u2[:r3],
u4[:r1],
u4[:r2]
]
assert Enum.all?(visible_ids, &(&1 in activities_ids))
end
test "home timeline with reply_visibility `following`", %{
users: %{u1: user},
activities: activities,
u1: u1,
u2: u2,
u3: u3,
u4: u4
} do
params =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
|> Map.put("reply_visibility", "following")
|> Map.put("reply_filtering_user", user)
activities_ids =
ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
|> Enum.map(& &1.id)
assert length(activities_ids) == 11
visible_ids =
Map.values(u1) ++
[
activities[:a1],
activities[:a2],
activities[:a4],
u2[:r1],
u2[:r3],
u3[:r1],
u4[:r1],
u4[:r2]
]
assert Enum.all?(visible_ids, &(&1 in activities_ids))
end
test "home timeline with reply_visibility `self`", %{
users: %{u1: user},
activities: activities,
u1: u1,
u2: u2,
u3: u3,
u4: u4
} do
params =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
|> Map.put("reply_visibility", "self")
|> Map.put("reply_filtering_user", user)
activities_ids =
ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
|> Enum.map(& &1.id)
assert length(activities_ids) == 9
visible_ids =
Map.values(u1) ++
[
activities[:a1],
activities[:a2],
activities[:a4],
u2[:r1],
u3[:r1],
u4[:r1]
]
assert Enum.all?(visible_ids, &(&1 in activities_ids))
end
end
describe "replies filtering with private messages" do
setup :private_messages
test "public timeline", %{users: %{u1: user}} do
activities_ids =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", false)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
|> ActivityPub.fetch_public_activities()
|> Enum.map(& &1.id)
assert activities_ids == []
end
test "public timeline with default reply_visibility `following`", %{users: %{u1: user}} do
activities_ids =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", false)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("reply_visibility", "following")
|> Map.put("reply_filtering_user", user)
|> Map.put("user", user)
|> ActivityPub.fetch_public_activities()
|> Enum.map(& &1.id)
assert activities_ids == []
end
test "public timeline with default reply_visibility `self`", %{users: %{u1: user}} do
activities_ids =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("local_only", false)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("reply_visibility", "self")
|> Map.put("reply_filtering_user", user)
|> Map.put("user", user)
|> ActivityPub.fetch_public_activities()
|> Enum.map(& &1.id)
assert activities_ids == []
end
test "home timeline", %{users: %{u1: user}} do
params =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
activities_ids =
ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
|> Enum.map(& &1.id)
assert length(activities_ids) == 12
end
test "home timeline with default reply_visibility `following`", %{users: %{u1: user}} do
params =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
|> Map.put("reply_visibility", "following")
|> Map.put("reply_filtering_user", user)
activities_ids =
ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
|> Enum.map(& &1.id)
assert length(activities_ids) == 12
end
test "home timeline with default reply_visibility `self`", %{
users: %{u1: user},
activities: activities,
u1: u1,
u2: u2,
u3: u3,
u4: u4
} do
params =
%{}
|> Map.put("type", ["Create", "Announce"])
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)
|> Map.put("reply_visibility", "self")
|> Map.put("reply_filtering_user", user)
activities_ids =
ActivityPub.fetch_activities([user.ap_id | User.following(user)], params)
|> Enum.map(& &1.id)
assert length(activities_ids) == 10
visible_ids =
Map.values(u1) ++ Map.values(u4) ++ [u2[:r1], u3[:r1]] ++ Map.values(activities)
assert Enum.all?(visible_ids, &(&1 in activities_ids))
end
end
defp public_messages(_) do
[u1, u2, u3, u4] = insert_list(4, :user)
{:ok, u1} = User.follow(u1, u2)
{:ok, u2} = User.follow(u2, u1)
{:ok, u1} = User.follow(u1, u4)
{:ok, u4} = User.follow(u4, u1)
{:ok, u2} = User.follow(u2, u3)
{:ok, u3} = User.follow(u3, u2)
{:ok, a1} = CommonAPI.post(u1, %{"status" => "Status"})
{:ok, r1_1} =
CommonAPI.post(u2, %{
"status" => "@#{u1.nickname} reply from u2 to u1",
"in_reply_to_status_id" => a1.id
})
{:ok, r1_2} =
CommonAPI.post(u3, %{
"status" => "@#{u1.nickname} reply from u3 to u1",
"in_reply_to_status_id" => a1.id
})
{:ok, r1_3} =
CommonAPI.post(u4, %{
"status" => "@#{u1.nickname} reply from u4 to u1",
"in_reply_to_status_id" => a1.id
})
{:ok, a2} = CommonAPI.post(u2, %{"status" => "Status"})
{:ok, r2_1} =
CommonAPI.post(u1, %{
"status" => "@#{u2.nickname} reply from u1 to u2",
"in_reply_to_status_id" => a2.id
})
{:ok, r2_2} =
CommonAPI.post(u3, %{
"status" => "@#{u2.nickname} reply from u3 to u2",
"in_reply_to_status_id" => a2.id
})
{:ok, r2_3} =
CommonAPI.post(u4, %{
"status" => "@#{u2.nickname} reply from u4 to u2",
"in_reply_to_status_id" => a2.id
})
{:ok, a3} = CommonAPI.post(u3, %{"status" => "Status"})
{:ok, r3_1} =
CommonAPI.post(u1, %{
"status" => "@#{u3.nickname} reply from u1 to u3",
"in_reply_to_status_id" => a3.id
})
{:ok, r3_2} =
CommonAPI.post(u2, %{
"status" => "@#{u3.nickname} reply from u2 to u3",
"in_reply_to_status_id" => a3.id
})
{:ok, r3_3} =
CommonAPI.post(u4, %{
"status" => "@#{u3.nickname} reply from u4 to u3",
"in_reply_to_status_id" => a3.id
})
{:ok, a4} = CommonAPI.post(u4, %{"status" => "Status"})
{:ok, r4_1} =
CommonAPI.post(u1, %{
"status" => "@#{u4.nickname} reply from u1 to u4",
"in_reply_to_status_id" => a4.id
})
{:ok, r4_2} =
CommonAPI.post(u2, %{
"status" => "@#{u4.nickname} reply from u2 to u4",
"in_reply_to_status_id" => a4.id
})
{:ok, r4_3} =
CommonAPI.post(u3, %{
"status" => "@#{u4.nickname} reply from u3 to u4",
"in_reply_to_status_id" => a4.id
})
{:ok,
users: %{u1: u1, u2: u2, u3: u3, u4: u4},
activities: %{a1: a1.id, a2: a2.id, a3: a3.id, a4: a4.id},
u1: %{r1: r1_1.id, r2: r1_2.id, r3: r1_3.id},
u2: %{r1: r2_1.id, r2: r2_2.id, r3: r2_3.id},
u3: %{r1: r3_1.id, r2: r3_2.id, r3: r3_3.id},
u4: %{r1: r4_1.id, r2: r4_2.id, r3: r4_3.id}}
end
defp private_messages(_) do
[u1, u2, u3, u4] = insert_list(4, :user)
{:ok, u1} = User.follow(u1, u2)
{:ok, u2} = User.follow(u2, u1)
{:ok, u1} = User.follow(u1, u3)
{:ok, u3} = User.follow(u3, u1)
{:ok, u1} = User.follow(u1, u4)
{:ok, u4} = User.follow(u4, u1)
{:ok, u2} = User.follow(u2, u3)
{:ok, u3} = User.follow(u3, u2)
{:ok, a1} = CommonAPI.post(u1, %{"status" => "Status", "visibility" => "private"})
{:ok, r1_1} =
CommonAPI.post(u2, %{
"status" => "@#{u1.nickname} reply from u2 to u1",
"in_reply_to_status_id" => a1.id,
"visibility" => "private"
})
{:ok, r1_2} =
CommonAPI.post(u3, %{
"status" => "@#{u1.nickname} reply from u3 to u1",
"in_reply_to_status_id" => a1.id,
"visibility" => "private"
})
{:ok, r1_3} =
CommonAPI.post(u4, %{
"status" => "@#{u1.nickname} reply from u4 to u1",
"in_reply_to_status_id" => a1.id,
"visibility" => "private"
})
{:ok, a2} = CommonAPI.post(u2, %{"status" => "Status", "visibility" => "private"})
{:ok, r2_1} =
CommonAPI.post(u1, %{
"status" => "@#{u2.nickname} reply from u1 to u2",
"in_reply_to_status_id" => a2.id,
"visibility" => "private"
})
{:ok, r2_2} =
CommonAPI.post(u3, %{
"status" => "@#{u2.nickname} reply from u3 to u2",
"in_reply_to_status_id" => a2.id,
"visibility" => "private"
})
{:ok, a3} = CommonAPI.post(u3, %{"status" => "Status", "visibility" => "private"})
{:ok, r3_1} =
CommonAPI.post(u1, %{
"status" => "@#{u3.nickname} reply from u1 to u3",
"in_reply_to_status_id" => a3.id,
"visibility" => "private"
})
{:ok, r3_2} =
CommonAPI.post(u2, %{
"status" => "@#{u3.nickname} reply from u2 to u3",
"in_reply_to_status_id" => a3.id,
"visibility" => "private"
})
{:ok, a4} = CommonAPI.post(u4, %{"status" => "Status", "visibility" => "private"})
{:ok, r4_1} =
CommonAPI.post(u1, %{
"status" => "@#{u4.nickname} reply from u1 to u4",
"in_reply_to_status_id" => a4.id,
"visibility" => "private"
})
{:ok,
users: %{u1: u1, u2: u2, u3: u3, u4: u4},
activities: %{a1: a1.id, a2: a2.id, a3: a3.id, a4: a4.id},
u1: %{r1: r1_1.id, r2: r1_2.id, r3: r1_3.id},
u2: %{r1: r2_1.id, r2: r2_2.id},
u3: %{r1: r3_1.id, r2: r3_2.id},
u4: %{r1: r4_1.id}}
end
end end

View file

@ -224,8 +224,7 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
object = Object.normalize(activity) object = Object.normalize(activity)
{:ok, [vote], object} = CommonAPI.vote(other_user, object, [0]) {:ok, [vote], object} = CommonAPI.vote(other_user, object, [0])
vote_object = Object.normalize(vote) {:ok, _activity} = CommonAPI.favorite(user, activity.id)
{:ok, _activity, _object} = ActivityPub.like(user, vote_object)
[fetched_vote] = Utils.get_existing_votes(other_user.ap_id, object) [fetched_vote] = Utils.get_existing_votes(other_user.ap_id, object)
assert fetched_vote.id == vote.id assert fetched_vote.id == vote.id
end end
@ -346,7 +345,7 @@ defmodule Pleroma.Web.ActivityPub.UtilsTest do
user = insert(:user) user = insert(:user)
refute Utils.get_existing_like(user.ap_id, object) refute Utils.get_existing_like(user.ap_id, object)
{:ok, like_activity, _object} = ActivityPub.like(user, object) {:ok, like_activity} = CommonAPI.favorite(user, note_activity.id)
assert ^like_activity = Utils.get_existing_like(user.ap_id, object) assert ^like_activity = Utils.get_existing_like(user.ap_id, object)
end end

View file

@ -1,45 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.AppOperationTest do
use Pleroma.Web.ConnCase, async: true
alias Pleroma.Web.ApiSpec
alias Pleroma.Web.ApiSpec.Schemas.AppCreateRequest
alias Pleroma.Web.ApiSpec.Schemas.AppCreateResponse
import OpenApiSpex.TestAssertions
import Pleroma.Factory
test "AppCreateRequest example matches schema" do
api_spec = ApiSpec.spec()
schema = AppCreateRequest.schema()
assert_schema(schema.example, "AppCreateRequest", api_spec)
end
test "AppCreateResponse example matches schema" do
api_spec = ApiSpec.spec()
schema = AppCreateResponse.schema()
assert_schema(schema.example, "AppCreateResponse", api_spec)
end
test "AppController produces a AppCreateResponse", %{conn: conn} do
api_spec = ApiSpec.spec()
app_attrs = build(:oauth_app)
json =
conn
|> put_req_header("content-type", "application/json")
|> post(
"/api/v1/apps",
Jason.encode!(%{
client_name: app_attrs.client_name,
redirect_uris: app_attrs.redirect_uris
})
)
|> json_response(200)
assert_schema(json, "AppCreateResponse", api_spec)
end
end

View file

@ -0,0 +1,43 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only
defmodule Pleroma.Web.ApiSpec.SchemaExamplesTest do
use ExUnit.Case, async: true
import Pleroma.Tests.ApiSpecHelpers
@content_type "application/json"
for operation <- api_operations() do
describe operation.operationId <> " Request Body" do
if operation.requestBody do
@media_type operation.requestBody.content[@content_type]
@schema resolve_schema(@media_type.schema)
if @media_type.example do
test "request body media type example matches schema" do
assert_schema(@media_type.example, @schema)
end
end
if @schema.example do
test "request body schema example matches schema" do
assert_schema(@schema.example, @schema)
end
end
end
end
for {status, response} <- operation.responses do
describe "#{operation.operationId} - #{status} Response" do
@schema resolve_schema(response.content[@content_type].schema)
if @schema.example do
test "example matches schema" do
assert_schema(@schema.example, @schema)
end
end
end
end
end
end

View file

@ -21,6 +21,60 @@ defmodule Pleroma.Web.CommonAPITest do
setup do: clear_config([:instance, :limit]) setup do: clear_config([:instance, :limit])
setup do: clear_config([:instance, :max_pinned_statuses]) setup do: clear_config([:instance, :max_pinned_statuses])
test "favoriting race condition" do
user = insert(:user)
users_serial = insert_list(10, :user)
users = insert_list(10, :user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "."})
users_serial
|> Enum.map(fn user ->
CommonAPI.favorite(user, activity.id)
end)
object = Object.get_by_ap_id(activity.data["object"])
assert object.data["like_count"] == 10
users
|> Enum.map(fn user ->
Task.async(fn ->
CommonAPI.favorite(user, activity.id)
end)
end)
|> Enum.map(&Task.await/1)
object = Object.get_by_ap_id(activity.data["object"])
assert object.data["like_count"] == 20
end
test "repeating race condition" do
user = insert(:user)
users_serial = insert_list(10, :user)
users = insert_list(10, :user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "."})
users_serial
|> Enum.map(fn user ->
CommonAPI.repeat(activity.id, user)
end)
object = Object.get_by_ap_id(activity.data["object"])
assert object.data["announcement_count"] == 10
users
|> Enum.map(fn user ->
Task.async(fn ->
CommonAPI.repeat(activity.id, user)
end)
end)
|> Enum.map(&Task.await/1)
object = Object.get_by_ap_id(activity.data["object"])
assert object.data["announcement_count"] == 20
end
test "when replying to a conversation / participation, it will set the correct context id even if no explicit reply_to is given" do test "when replying to a conversation / participation, it will set the correct context id even if no explicit reply_to is given" do
user = insert(:user) user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"}) {:ok, activity} = CommonAPI.post(user, %{"status" => ".", "visibility" => "direct"})
@ -256,6 +310,16 @@ defmodule Pleroma.Web.CommonAPITest do
{:ok, %Activity{}, _} = CommonAPI.repeat(activity.id, user) {:ok, %Activity{}, _} = CommonAPI.repeat(activity.id, user)
end end
test "can't repeat a repeat" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
{:ok, %Activity{} = announce, _} = CommonAPI.repeat(activity.id, other_user)
refute match?({:ok, %Activity{}, _}, CommonAPI.repeat(announce.id, user))
end
test "repeating a status privately" do test "repeating a status privately" do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
@ -285,8 +349,8 @@ defmodule Pleroma.Web.CommonAPITest do
other_user = insert(:user) other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"}) {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
{:ok, %Activity{} = activity, object} = CommonAPI.repeat(activity.id, user) {:ok, %Activity{} = announce, object} = CommonAPI.repeat(activity.id, user)
{:ok, ^activity, ^object} = CommonAPI.repeat(activity.id, user) {:ok, ^announce, ^object} = CommonAPI.repeat(activity.id, user)
end end
test "favoriting a status twice returns ok, but without the like activity" do test "favoriting a status twice returns ok, but without the like activity" do
@ -360,7 +424,9 @@ defmodule Pleroma.Web.CommonAPITest do
user = refresh_record(user) user = refresh_record(user)
assert {:ok, ^activity} = CommonAPI.unpin(activity.id, user) id = activity.id
assert match?({:ok, %{id: ^id}}, CommonAPI.unpin(activity.id, user))
user = refresh_record(user) user = refresh_record(user)

View file

@ -335,26 +335,6 @@ defmodule Pleroma.Web.CommonAPI.UtilsTest do
end end
end end
describe "get_by_id_or_ap_id/1" do
test "get activity by id" do
activity = insert(:note_activity)
%Pleroma.Activity{} = note = Utils.get_by_id_or_ap_id(activity.id)
assert note.id == activity.id
end
test "get activity by ap_id" do
activity = insert(:note_activity)
%Pleroma.Activity{} = note = Utils.get_by_id_or_ap_id(activity.data["object"])
assert note.id == activity.id
end
test "get activity by object when type isn't `Create` " do
activity = insert(:like_activity)
%Pleroma.Activity{} = like = Utils.get_by_id_or_ap_id(activity.id)
assert like.data["object"] == activity.data["object"]
end
end
describe "to_master_date/1" do describe "to_master_date/1" do
test "removes microseconds from date (NaiveDateTime)" do test "removes microseconds from date (NaiveDateTime)" do
assert Utils.to_masto_date(~N[2015-01-23 23:50:07.123]) == "2015-01-23T23:50:07.000Z" assert Utils.to_masto_date(~N[2015-01-23 23:50:07.123]) == "2015-01-23T23:50:07.000Z"

View file

@ -130,6 +130,9 @@ defmodule Pleroma.Web.FederatorTest do
assert {:ok, job} = Federator.incoming_ap_doc(params) assert {:ok, job} = Federator.incoming_ap_doc(params)
assert {:ok, _activity} = ObanHelpers.perform(job) assert {:ok, _activity} = ObanHelpers.perform(job)
assert {:ok, job} = Federator.incoming_ap_doc(params)
assert {:error, :already_present} = ObanHelpers.perform(job)
end end
test "rejects incoming AP docs with incorrect origin" do test "rejects incoming AP docs with incorrect origin" do
@ -148,7 +151,7 @@ defmodule Pleroma.Web.FederatorTest do
} }
assert {:ok, job} = Federator.incoming_ap_doc(params) assert {:ok, job} = Federator.incoming_ap_doc(params)
assert :error = ObanHelpers.perform(job) assert {:error, :origin_containment_failed} = ObanHelpers.perform(job)
end end
test "it does not crash if MRF rejects the post" do test "it does not crash if MRF rejects the post" do
@ -164,7 +167,7 @@ defmodule Pleroma.Web.FederatorTest do
|> Poison.decode!() |> Poison.decode!()
assert {:ok, job} = Federator.incoming_ap_doc(params) assert {:ok, job} = Federator.incoming_ap_doc(params)
assert :error = ObanHelpers.perform(job) assert {:error, _} = ObanHelpers.perform(job)
end end
end end
end end

View file

@ -681,17 +681,17 @@ defmodule Pleroma.Web.MastodonAPI.AccountControllerTest do
test "following / unfollowing errors", %{user: user, conn: conn} do test "following / unfollowing errors", %{user: user, conn: conn} do
# self follow # self follow
conn_res = post(conn, "/api/v1/accounts/#{user.id}/follow") conn_res = post(conn, "/api/v1/accounts/#{user.id}/follow")
assert %{"error" => "Record not found"} = json_response(conn_res, 404) assert %{"error" => "Can not follow yourself"} = json_response(conn_res, 400)
# self unfollow # self unfollow
user = User.get_cached_by_id(user.id) user = User.get_cached_by_id(user.id)
conn_res = post(conn, "/api/v1/accounts/#{user.id}/unfollow") conn_res = post(conn, "/api/v1/accounts/#{user.id}/unfollow")
assert %{"error" => "Record not found"} = json_response(conn_res, 404) assert %{"error" => "Can not unfollow yourself"} = json_response(conn_res, 400)
# self follow via uri # self follow via uri
user = User.get_cached_by_id(user.id) user = User.get_cached_by_id(user.id)
conn_res = post(conn, "/api/v1/follows", %{"uri" => user.nickname}) conn_res = post(conn, "/api/v1/follows", %{"uri" => user.nickname})
assert %{"error" => "Record not found"} = json_response(conn_res, 404) assert %{"error" => "Can not follow yourself"} = json_response(conn_res, 400)
# follow non existing user # follow non existing user
conn_res = post(conn, "/api/v1/accounts/doesntexist/follow") conn_res = post(conn, "/api/v1/accounts/doesntexist/follow")

View file

@ -27,7 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.AppControllerTest do
"vapid_key" => Push.vapid_config() |> Keyword.get(:public_key) "vapid_key" => Push.vapid_config() |> Keyword.get(:public_key)
} }
assert expected == json_response(conn, 200) assert expected == json_response_and_validate_schema(conn, 200)
end end
test "creates an oauth app", %{conn: conn} do test "creates an oauth app", %{conn: conn} do
@ -55,6 +55,6 @@ defmodule Pleroma.Web.MastodonAPI.AppControllerTest do
"vapid_key" => Push.vapid_config() |> Keyword.get(:public_key) "vapid_key" => Push.vapid_config() |> Keyword.get(:public_key)
} }
assert expected == json_response(conn, 200) assert expected == json_response_and_validate_schema(conn, 200)
end end
end end

View file

@ -4,13 +4,16 @@
defmodule Pleroma.Web.MastodonAPI.CustomEmojiControllerTest do defmodule Pleroma.Web.MastodonAPI.CustomEmojiControllerTest do
use Pleroma.Web.ConnCase, async: true use Pleroma.Web.ConnCase, async: true
alias Pleroma.Web.ApiSpec
import OpenApiSpex.TestAssertions
test "with tags", %{conn: conn} do test "with tags", %{conn: conn} do
[emoji | _body] = assert resp =
conn conn
|> get("/api/v1/custom_emojis") |> get("/api/v1/custom_emojis")
|> json_response(200) |> json_response_and_validate_schema(200)
assert [emoji | _body] = resp
assert Map.has_key?(emoji, "shortcode") assert Map.has_key?(emoji, "shortcode")
assert Map.has_key?(emoji, "static_url") assert Map.has_key?(emoji, "static_url")
assert Map.has_key?(emoji, "tags") assert Map.has_key?(emoji, "tags")
@ -18,5 +21,6 @@ defmodule Pleroma.Web.MastodonAPI.CustomEmojiControllerTest do
assert Map.has_key?(emoji, "category") assert Map.has_key?(emoji, "category")
assert Map.has_key?(emoji, "url") assert Map.has_key?(emoji, "url")
assert Map.has_key?(emoji, "visible_in_picker") assert Map.has_key?(emoji, "visible_in_picker")
assert_schema(emoji, "CustomEmoji", ApiSpec.spec())
end end
end end

View file

@ -6,11 +6,8 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockControllerTest do
use Pleroma.Web.ConnCase use Pleroma.Web.ConnCase
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ApiSpec
alias Pleroma.Web.ApiSpec.Schemas.DomainBlocksResponse
import Pleroma.Factory import Pleroma.Factory
import OpenApiSpex.TestAssertions
test "blocking / unblocking a domain" do test "blocking / unblocking a domain" do
%{user: user, conn: conn} = oauth_access(["write:blocks"]) %{user: user, conn: conn} = oauth_access(["write:blocks"])
@ -21,7 +18,7 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockControllerTest do
|> put_req_header("content-type", "application/json") |> put_req_header("content-type", "application/json")
|> post("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"}) |> post("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"})
assert %{} = json_response(ret_conn, 200) assert %{} == json_response_and_validate_schema(ret_conn, 200)
user = User.get_cached_by_ap_id(user.ap_id) user = User.get_cached_by_ap_id(user.ap_id)
assert User.blocks?(user, other_user) assert User.blocks?(user, other_user)
@ -30,7 +27,7 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockControllerTest do
|> put_req_header("content-type", "application/json") |> put_req_header("content-type", "application/json")
|> delete("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"}) |> delete("/api/v1/domain_blocks", %{"domain" => "dogwhistle.zone"})
assert %{} = json_response(ret_conn, 200) assert %{} == json_response_and_validate_schema(ret_conn, 200)
user = User.get_cached_by_ap_id(user.ap_id) user = User.get_cached_by_ap_id(user.ap_id)
refute User.blocks?(user, other_user) refute User.blocks?(user, other_user)
end end
@ -41,21 +38,10 @@ defmodule Pleroma.Web.MastodonAPI.DomainBlockControllerTest do
{:ok, user} = User.block_domain(user, "bad.site") {:ok, user} = User.block_domain(user, "bad.site")
{:ok, user} = User.block_domain(user, "even.worse.site") {:ok, user} = User.block_domain(user, "even.worse.site")
conn = assert ["even.worse.site", "bad.site"] ==
conn conn
|> assign(:user, user) |> assign(:user, user)
|> get("/api/v1/domain_blocks") |> get("/api/v1/domain_blocks")
|> json_response_and_validate_schema(200)
domain_blocks = json_response(conn, 200)
assert "bad.site" in domain_blocks
assert "even.worse.site" in domain_blocks
assert_schema(domain_blocks, "DomainBlocksResponse", ApiSpec.spec())
end
test "DomainBlocksResponse example matches schema" do
api_spec = ApiSpec.spec()
schema = DomainBlocksResponse.schema()
assert_schema(schema.example, "DomainBlocksResponse", api_spec)
end end
end end

View file

@ -302,6 +302,17 @@ defmodule Pleroma.Web.MastodonAPI.StatusControllerTest do
assert [] == Repo.all(Activity) assert [] == Repo.all(Activity)
end end
test "ignores nil values", %{conn: conn} do
conn =
post(conn, "/api/v1/statuses", %{
"status" => "not scheduled",
"scheduled_at" => nil
})
assert result = json_response(conn, 200)
assert Activity.get_by_id(result["id"])
end
test "creates a scheduled activity with a media attachment", %{user: user, conn: conn} do test "creates a scheduled activity with a media attachment", %{user: user, conn: conn} do
scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond) scheduled_at = NaiveDateTime.add(NaiveDateTime.utc_now(), :timer.minutes(120), :millisecond)

View file

@ -43,7 +43,8 @@ defmodule Pleroma.Web.MastodonAPI.PollViewTest do
%{title: "why are you even asking?", votes_count: 0} %{title: "why are you even asking?", votes_count: 0}
], ],
voted: false, voted: false,
votes_count: 0 votes_count: 0,
voters_count: nil
} }
result = PollView.render("show.json", %{object: object}) result = PollView.render("show.json", %{object: object})
@ -69,9 +70,20 @@ defmodule Pleroma.Web.MastodonAPI.PollViewTest do
} }
}) })
voter = insert(:user)
object = Object.normalize(activity) object = Object.normalize(activity)
assert %{multiple: true} = PollView.render("show.json", %{object: object}) {:ok, _votes, object} = CommonAPI.vote(voter, object, [0, 1])
assert match?(
%{
multiple: true,
voters_count: 1,
votes_count: 2
},
PollView.render("show.json", %{object: object})
)
end end
test "detects emoji" do test "detects emoji" do

View file

@ -63,12 +63,12 @@ defmodule Pleroma.Web.Push.ImplTest do
activity: activity activity: activity
) )
assert Impl.perform(notif) == [:ok, :ok] assert Impl.perform(notif) == {:ok, [:ok, :ok]}
end end
@tag capture_log: true @tag capture_log: true
test "returns error if notif does not match " do test "returns error if notif does not match " do
assert Impl.perform(%{}) == :error assert Impl.perform(%{}) == {:error, :unknown_type}
end end
test "successful message sending" do test "successful message sending" do

View file

@ -28,6 +28,42 @@ defmodule Pleroma.Web.StreamerTest do
{:ok, %{user: user, notify: notify}} {:ok, %{user: user, notify: notify}}
end end
test "it streams the user's post in the 'user' stream", %{user: user} do
task =
Task.async(fn ->
assert_receive {:text, _}, @streamer_timeout
end)
Streamer.add_socket(
"user",
%{transport_pid: task.pid, assigns: %{user: user}}
)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey"})
Streamer.stream("user", activity)
Task.await(task)
end
test "it streams boosts of the user in the 'user' stream", %{user: user} do
task =
Task.async(fn ->
assert_receive {:text, _}, @streamer_timeout
end)
Streamer.add_socket(
"user",
%{transport_pid: task.pid, assigns: %{user: user}}
)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "hey"})
{:ok, announce, _} = CommonAPI.repeat(activity.id, user)
Streamer.stream("user", announce)
Task.await(task)
end
test "it sends notify to in the 'user' stream", %{user: user, notify: notify} do test "it sends notify to in the 'user' stream", %{user: user, notify: notify} do
task = task =
Task.async(fn -> Task.async(fn ->

View file

@ -95,6 +95,30 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
end end
end end
end end
test "it imports follows with different nickname variations", %{conn: conn} do
[user2, user3, user4, user5, user6] = insert_list(5, :user)
identifiers =
[
user2.ap_id,
user3.nickname,
" ",
"@" <> user4.nickname,
user5.nickname <> "@localhost",
"@" <> user6.nickname <> "@localhost"
]
|> Enum.join("\n")
response =
conn
|> post("/api/pleroma/follow_import", %{"list" => identifiers})
|> json_response(:ok)
assert response == "job started"
assert [{:ok, job_result}] = ObanHelpers.perform_all()
assert job_result == [user2, user3, user4, user5, user6]
end
end end
describe "POST /api/pleroma/blocks_import" do describe "POST /api/pleroma/blocks_import" do
@ -136,6 +160,29 @@ defmodule Pleroma.Web.TwitterAPI.UtilControllerTest do
) )
end end
end end
test "it imports blocks with different nickname variations", %{conn: conn} do
[user2, user3, user4, user5, user6] = insert_list(5, :user)
identifiers =
[
user2.ap_id,
user3.nickname,
"@" <> user4.nickname,
user5.nickname <> "@localhost",
"@" <> user6.nickname <> "@localhost"
]
|> Enum.join(" ")
response =
conn
|> post("/api/pleroma/blocks_import", %{"list" => identifiers})
|> json_response(:ok)
assert response == "job started"
assert [{:ok, job_result}] = ObanHelpers.perform_all()
assert job_result == [user2, user3, user4, user5, user6]
end
end end
describe "PUT /api/pleroma/notification_settings" do describe "PUT /api/pleroma/notification_settings" do