Merge branch 'develop' into gun

This commit is contained in:
Alexander Strizhakov 2020-03-30 12:15:23 +03:00
commit f497cf2f7c
No known key found for this signature in database
GPG key ID: 022896A53AEF1381
43 changed files with 1529 additions and 318 deletions

View file

@ -78,6 +78,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mastodon API: User timelines will now respect blocks, unless you are getting the user timeline of somebody you blocked (which would be empty otherwise). - Mastodon API: User timelines will now respect blocks, unless you are getting the user timeline of somebody you blocked (which would be empty otherwise).
- Mastodon API: Favoriting / Repeating a post multiple times will now return the identical response every time. Before, executing that action twice would return an error ("already favorited") on the second try. - Mastodon API: Favoriting / Repeating a post multiple times will now return the identical response every time. Before, executing that action twice would return an error ("already favorited") on the second try.
- Mastodon API: Limit timeline requests to 3 per timeline per 500ms per user/ip by default. - Mastodon API: Limit timeline requests to 3 per timeline per 500ms per user/ip by default.
- Admin API: `PATCH /api/pleroma/admin/users/:nickname/credentials` and `GET /api/pleroma/admin/users/:nickname/credentials`
</details> </details>
### Added ### Added

View file

@ -22,9 +22,10 @@ def generate_like_activities(user, posts) do
def generate_users(opts) do def generate_users(opts) do
IO.puts("Starting generating #{opts[:users_max]} users...") IO.puts("Starting generating #{opts[:users_max]} users...")
{time, _} = :timer.tc(fn -> do_generate_users(opts) end) {time, users} = :timer.tc(fn -> do_generate_users(opts) end)
IO.puts("Inserting users take #{to_sec(time)} sec.\n") IO.puts("Inserting users took #{to_sec(time)} sec.\n")
users
end end
defp do_generate_users(opts) do defp do_generate_users(opts) do

View file

@ -0,0 +1,76 @@
defmodule Mix.Tasks.Pleroma.Benchmarks.Timelines do
use Mix.Task
alias Pleroma.Repo
alias Pleroma.LoadTesting.Generator
alias Pleroma.Web.CommonAPI
def run(_args) do
Mix.Pleroma.start_pleroma()
# Cleaning tables
clean_tables()
[{:ok, user} | users] = Generator.generate_users(users_max: 1000)
# Let the user make 100 posts
1..100
|> Enum.each(fn i -> CommonAPI.post(user, %{"status" => to_string(i)}) end)
# Let 10 random users post
posts =
users
|> Enum.take_random(10)
|> Enum.map(fn {:ok, random_user} ->
{:ok, activity} = CommonAPI.post(random_user, %{"status" => "."})
activity
end)
# let our user repeat them
posts
|> Enum.each(fn activity ->
CommonAPI.repeat(activity.id, user)
end)
Benchee.run(
%{
"user timeline, no followers" => fn reading_user ->
conn =
Phoenix.ConnTest.build_conn()
|> Plug.Conn.assign(:user, reading_user)
|> Plug.Conn.assign(:skip_link_headers, true)
Pleroma.Web.MastodonAPI.AccountController.statuses(conn, %{"id" => user.id})
end
},
inputs: %{"user" => user, "no user" => nil},
time: 60
)
users
|> Enum.each(fn {:ok, follower} -> Pleroma.User.follow(follower, user) end)
Benchee.run(
%{
"user timeline, all following" => fn reading_user ->
conn =
Phoenix.ConnTest.build_conn()
|> Plug.Conn.assign(:user, reading_user)
|> Plug.Conn.assign(:skip_link_headers, true)
Pleroma.Web.MastodonAPI.AccountController.statuses(conn, %{"id" => user.id})
end
},
inputs: %{"user" => user, "no user" => nil},
time: 60
)
end
defp clean_tables do
IO.puts("Deleting old data...\n")
Ecto.Adapters.SQL.query!(Repo, "TRUNCATE users CASCADE;")
Ecto.Adapters.SQL.query!(Repo, "TRUNCATE activities CASCADE;")
Ecto.Adapters.SQL.query!(Repo, "TRUNCATE objects CASCADE;")
end
end

View file

@ -2442,7 +2442,7 @@
%{ %{
key: :relations_actions, key: :relations_actions,
type: [:tuple, {:list, :tuple}], type: [:tuple, {:list, :tuple}],
description: "For actions on relations with all users (follow, unfollow)", description: "For actions on relationships with all users (follow, unfollow)",
suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]] suggestions: [{1000, 10}, [{10_000, 10}, {10_000, 50}]]
}, },
%{ %{

View file

@ -414,6 +414,83 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
- `nicknames` - `nicknames`
- Response: none (code `204`) - Response: none (code `204`)
## `GET /api/pleroma/admin/users/:nickname/credentials`
### Get the user's email, password, display and settings-related fields
- Params:
- `nickname`
- Response:
```json
{
"actor_type": "Person",
"allow_following_move": true,
"avatar": "https://pleroma.social/media/7e8e7508fd545ef580549b6881d80ec0ff2c81ed9ad37b9bdbbdf0e0d030159d.jpg",
"background": "https://pleroma.social/media/4de34c0bd10970d02cbdef8972bef0ebbf55f43cadc449554d4396156162fe9a.jpg",
"banner": "https://pleroma.social/media/8d92ba2bd244b613520abf557dd448adcd30f5587022813ee9dd068945986946.jpg",
"bio": "bio",
"default_scope": "public",
"discoverable": false,
"email": "user@example.com",
"fields": [
{
"name": "example",
"value": "<a href=\"https://example.com\" rel=\"ugc\">https://example.com</a>"
}
],
"hide_favorites": false,
"hide_followers": false,
"hide_followers_count": false,
"hide_follows": false,
"hide_follows_count": false,
"id": "9oouHaEEUR54hls968",
"locked": true,
"name": "user",
"no_rich_text": true,
"pleroma_settings_store": {},
"raw_fields": [
{
"id": 1,
"name": "example",
"value": "https://example.com"
},
],
"show_role": true,
"skip_thread_containment": false
}
```
## `PATCH /api/pleroma/admin/users/:nickname/credentials`
### Change the user's email, password, display and settings-related fields
- Params:
- `email`
- `password`
- `name`
- `bio`
- `avatar`
- `locked`
- `no_rich_text`
- `default_scope`
- `banner`
- `hide_follows`
- `hide_followers`
- `hide_followers_count`
- `hide_follows_count`
- `hide_favorites`
- `allow_following_move`
- `background`
- `show_role`
- `skip_thread_containment`
- `fields`
- `discoverable`
- `actor_type`
- Response: none (code `200`)
## `GET /api/pleroma/admin/reports` ## `GET /api/pleroma/admin/reports`
### Get a list of reports ### Get a list of reports

View file

@ -95,6 +95,17 @@ def with_preloaded_object(query, join_type \\ :inner) do
|> preload([activity, object: object], object: object) |> preload([activity, object: object], object: object)
end end
# Note: applies to fake activities (ActivityPub.Utils.get_notified_from_object/1 etc.)
def user_actor(%Activity{actor: nil}), do: nil
def user_actor(%Activity{} = activity) do
with %User{} <- activity.user_actor do
activity.user_actor
else
_ -> User.get_cached_by_ap_id(activity.actor)
end
end
def with_joined_user_actor(query, join_type \\ :inner) do def with_joined_user_actor(query, join_type \\ :inner) do
join(query, join_type, [activity], u in User, join(query, join_type, [activity], u in User,
on: u.ap_id == activity.actor, on: u.ap_id == activity.actor,

View file

@ -35,6 +35,13 @@ def by_author(query \\ Activity, %User{ap_id: ap_id}) do
from(a in query, where: a.actor == ^ap_id) from(a in query, where: a.actor == ^ap_id)
end end
def find_by_object_ap_id(activities, object_ap_id) do
Enum.find(
activities,
&(object_ap_id in [is_map(&1.data["object"]) && &1.data["object"]["id"], &1.data["object"]])
)
end
@spec by_object_id(query, String.t() | [String.t()]) :: query @spec by_object_id(query, String.t() | [String.t()]) :: query
def by_object_id(query \\ Activity, object_id) def by_object_id(query \\ Activity, object_id)

View file

@ -129,21 +129,18 @@ def for_user(user, params \\ %{}) do
end end
def restrict_recipients(query, user, %{"recipients" => user_ids}) do def restrict_recipients(query, user, %{"recipients" => user_ids}) do
user_ids = user_binary_ids =
[user.id | user_ids] [user.id | user_ids]
|> Enum.uniq() |> Enum.uniq()
|> Enum.reduce([], fn user_id, acc -> |> User.binary_id()
{:ok, user_id} = FlakeId.Ecto.CompatType.dump(user_id)
[user_id | acc]
end)
conversation_subquery = conversation_subquery =
__MODULE__ __MODULE__
|> group_by([p], p.conversation_id) |> group_by([p], p.conversation_id)
|> having( |> having(
[p], [p],
count(p.user_id) == ^length(user_ids) and count(p.user_id) == ^length(user_binary_ids) and
fragment("array_agg(?) @> ?", p.user_id, ^user_ids) fragment("array_agg(?) @> ?", p.user_id, ^user_binary_ids)
) )
|> select([p], %{id: p.conversation_id}) |> select([p], %{id: p.conversation_id})

View file

@ -129,4 +129,32 @@ def move_following(origin, target) do
move_following(origin, target) move_following(origin, target)
end end
end end
def all_between_user_sets(
source_users,
target_users
)
when is_list(source_users) and is_list(target_users) do
source_user_ids = User.binary_id(source_users)
target_user_ids = User.binary_id(target_users)
__MODULE__
|> where(
fragment(
"(follower_id = ANY(?) AND following_id = ANY(?)) OR \
(follower_id = ANY(?) AND following_id = ANY(?))",
^source_user_ids,
^target_user_ids,
^target_user_ids,
^source_user_ids
)
)
|> Repo.all()
end
def find(following_relationships, follower, following) do
Enum.find(following_relationships, fn
fr -> fr.follower_id == follower.id and fr.following_id == following.id
end)
end
end end

View file

@ -605,6 +605,17 @@ def get_log_entry_message(%ModerationLog{
}" }"
end end
@spec get_log_entry_message(ModerationLog) :: String.t()
def get_log_entry_message(%ModerationLog{
data: %{
"actor" => %{"nickname" => actor_nickname},
"action" => "updated_users",
"subject" => subjects
}
}) do
"@#{actor_nickname} updated users: #{users_to_nicknames_string(subjects)}"
end
defp nicknames_to_string(nicknames) do defp nicknames_to_string(nicknames) do
nicknames nicknames
|> Enum.map(&"@#{&1}") |> Enum.map(&"@#{&1}")

View file

@ -10,6 +10,7 @@ defmodule Pleroma.Notification do
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Pagination alias Pleroma.Pagination
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.ThreadMute
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.Push alias Pleroma.Web.Push
@ -17,6 +18,7 @@ defmodule Pleroma.Notification do
import Ecto.Query import Ecto.Query
import Ecto.Changeset import Ecto.Changeset
require Logger require Logger
@type t :: %__MODULE__{} @type t :: %__MODULE__{}
@ -37,11 +39,11 @@ def changeset(%Notification{} = notification, attrs) do
end end
defp for_user_query_ap_id_opts(user, opts) do defp for_user_query_ap_id_opts(user, opts) do
ap_id_relations = ap_id_relationships =
[:block] ++ [:block] ++
if opts[@include_muted_option], do: [], else: [:notification_mute] if opts[@include_muted_option], do: [], else: [:notification_mute]
preloaded_ap_ids = User.outgoing_relations_ap_ids(user, ap_id_relations) preloaded_ap_ids = User.outgoing_relationships_ap_ids(user, ap_id_relationships)
exclude_blocked_opts = Map.merge(%{blocked_users_ap_ids: preloaded_ap_ids[:block]}, opts) exclude_blocked_opts = Map.merge(%{blocked_users_ap_ids: preloaded_ap_ids[:block]}, opts)
@ -100,7 +102,7 @@ defp exclude_notification_muted(query, user, opts) do
query query
|> where([n, a], a.actor not in ^notification_muted_ap_ids) |> where([n, a], a.actor not in ^notification_muted_ap_ids)
|> join(:left, [n, a], tm in Pleroma.ThreadMute, |> join(:left, [n, a], tm in ThreadMute,
on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data) on: tm.user_id == ^user.id and tm.context == fragment("?->>'context'", a.data)
) )
|> where([n, a, o, tm], is_nil(tm.user_id)) |> where([n, a, o, tm], is_nil(tm.user_id))
@ -275,58 +277,111 @@ def dismiss(%{id: user_id} = _user, id) do
def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do
object = Object.normalize(activity) object = Object.normalize(activity)
unless object && object.data["type"] == "Answer" do if object && object.data["type"] == "Answer" do
users = get_notified_from_activity(activity)
notifications = Enum.map(users, fn user -> create_notification(activity, user) end)
{:ok, notifications}
else
{:ok, []} {:ok, []}
else
do_create_notifications(activity)
end end
end end
def create_notifications(%Activity{data: %{"type" => type}} = activity) def create_notifications(%Activity{data: %{"type" => type}} = activity)
when type in ["Like", "Announce", "Follow", "Move", "EmojiReact"] do when type in ["Like", "Announce", "Follow", "Move", "EmojiReact"] do
notifications = do_create_notifications(activity)
activity
|> get_notified_from_activity()
|> Enum.map(&create_notification(activity, &1))
{:ok, notifications}
end end
def create_notifications(_), do: {:ok, []} def create_notifications(_), do: {:ok, []}
defp do_create_notifications(%Activity{} = activity) do
{enabled_receivers, disabled_receivers} = get_notified_from_activity(activity)
potential_receivers = enabled_receivers ++ disabled_receivers
notifications =
Enum.map(potential_receivers, fn user ->
do_send = user in enabled_receivers
create_notification(activity, user, do_send)
end)
{:ok, notifications}
end
# TODO move to sql, too. # TODO move to sql, too.
def create_notification(%Activity{} = activity, %User{} = user) do def create_notification(%Activity{} = activity, %User{} = user, do_send \\ true) do
unless skip?(activity, user) do unless skip?(activity, user) do
notification = %Notification{user_id: user.id, activity: activity} notification = %Notification{user_id: user.id, activity: activity}
{:ok, notification} = Repo.insert(notification) {:ok, notification} = Repo.insert(notification)
["user", "user:notification"] if do_send do
|> Streamer.stream(notification) Streamer.stream(["user", "user:notification"], notification)
Push.send(notification) Push.send(notification)
end
notification notification
end end
end end
@doc """
Returns a tuple with 2 elements:
{enabled notification receivers, currently disabled receivers (blocking / [thread] muting)}
NOTE: might be called for FAKE Activities, see ActivityPub.Utils.get_notified_from_object/1
"""
def get_notified_from_activity(activity, local_only \\ true) def get_notified_from_activity(activity, local_only \\ true)
def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only) def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, local_only)
when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact"] do when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact"] do
potential_receiver_ap_ids =
[] []
|> Utils.maybe_notify_to_recipients(activity) |> Utils.maybe_notify_to_recipients(activity)
|> Utils.maybe_notify_mentioned_recipients(activity) |> Utils.maybe_notify_mentioned_recipients(activity)
|> Utils.maybe_notify_subscribers(activity) |> Utils.maybe_notify_subscribers(activity)
|> Utils.maybe_notify_followers(activity) |> Utils.maybe_notify_followers(activity)
|> Enum.uniq() |> Enum.uniq()
# Since even subscribers and followers can mute / thread-mute, filtering all above AP IDs
notification_enabled_ap_ids =
potential_receiver_ap_ids
|> exclude_relationship_restricted_ap_ids(activity)
|> exclude_thread_muter_ap_ids(activity)
potential_receivers =
potential_receiver_ap_ids
|> Enum.uniq()
|> User.get_users_from_set(local_only) |> User.get_users_from_set(local_only)
notification_enabled_users =
Enum.filter(potential_receivers, fn u -> u.ap_id in notification_enabled_ap_ids end)
{notification_enabled_users, potential_receivers -- notification_enabled_users}
end end
def get_notified_from_activity(_, _local_only), do: [] def get_notified_from_activity(_, _local_only), do: {[], []}
@doc "Filters out AP IDs of users basing on their relationships with activity actor user"
def exclude_relationship_restricted_ap_ids([], _activity), do: []
def exclude_relationship_restricted_ap_ids(ap_ids, %Activity{} = activity) do
relationship_restricted_ap_ids =
activity
|> Activity.user_actor()
|> User.incoming_relationships_ungrouped_ap_ids([
:block,
:notification_mute
])
Enum.uniq(ap_ids) -- relationship_restricted_ap_ids
end
@doc "Filters out AP IDs of users who mute activity thread"
def exclude_thread_muter_ap_ids([], _activity), do: []
def exclude_thread_muter_ap_ids(ap_ids, %Activity{} = activity) do
thread_muter_ap_ids = ThreadMute.muter_ap_ids(activity.data["context"])
Enum.uniq(ap_ids) -- thread_muter_ap_ids
end
@spec skip?(Activity.t(), User.t()) :: boolean() @spec skip?(Activity.t(), User.t()) :: boolean()
def skip?(activity, user) do def skip?(%Activity{} = activity, %User{} = user) do
[ [
:self, :self,
:followers, :followers,
@ -335,18 +390,20 @@ def skip?(activity, user) do
:non_follows, :non_follows,
:recently_followed :recently_followed
] ]
|> Enum.any?(&skip?(&1, activity, user)) |> Enum.find(&skip?(&1, activity, user))
end end
def skip?(_, _), do: false
@spec skip?(atom(), Activity.t(), User.t()) :: boolean() @spec skip?(atom(), Activity.t(), User.t()) :: boolean()
def skip?(:self, activity, user) do def skip?(:self, %Activity{} = activity, %User{} = user) do
activity.data["actor"] == user.ap_id activity.data["actor"] == user.ap_id
end end
def skip?( def skip?(
:followers, :followers,
activity, %Activity{} = activity,
%{notification_settings: %{followers: false}} = user %User{notification_settings: %{followers: false}} = user
) do ) do
actor = activity.data["actor"] actor = activity.data["actor"]
follower = User.get_cached_by_ap_id(actor) follower = User.get_cached_by_ap_id(actor)
@ -355,15 +412,19 @@ def skip?(
def skip?( def skip?(
:non_followers, :non_followers,
activity, %Activity{} = activity,
%{notification_settings: %{non_followers: false}} = user %User{notification_settings: %{non_followers: false}} = user
) do ) do
actor = activity.data["actor"] actor = activity.data["actor"]
follower = User.get_cached_by_ap_id(actor) follower = User.get_cached_by_ap_id(actor)
!User.following?(follower, user) !User.following?(follower, user)
end end
def skip?(:follows, activity, %{notification_settings: %{follows: false}} = user) do def skip?(
:follows,
%Activity{} = activity,
%User{notification_settings: %{follows: false}} = user
) do
actor = activity.data["actor"] actor = activity.data["actor"]
followed = User.get_cached_by_ap_id(actor) followed = User.get_cached_by_ap_id(actor)
User.following?(user, followed) User.following?(user, followed)
@ -371,15 +432,16 @@ def skip?(:follows, activity, %{notification_settings: %{follows: false}} = user
def skip?( def skip?(
:non_follows, :non_follows,
activity, %Activity{} = activity,
%{notification_settings: %{non_follows: false}} = user %User{notification_settings: %{non_follows: false}} = user
) do ) do
actor = activity.data["actor"] actor = activity.data["actor"]
followed = User.get_cached_by_ap_id(actor) followed = User.get_cached_by_ap_id(actor)
!User.following?(user, followed) !User.following?(user, followed)
end end
def skip?(:recently_followed, %{data: %{"type" => "Follow"}} = activity, user) do # To do: consider defining recency in hours and checking FollowingRelationship with a single SQL
def skip?(:recently_followed, %Activity{data: %{"type" => "Follow"}} = activity, %User{} = user) do
actor = activity.data["actor"] actor = activity.data["actor"]
Notification.for_user(user) Notification.for_user(user)

View file

@ -9,7 +9,8 @@ defmodule Pleroma.ThreadMute do
alias Pleroma.ThreadMute alias Pleroma.ThreadMute
alias Pleroma.User alias Pleroma.User
require Ecto.Query import Ecto.Changeset
import Ecto.Query
schema "thread_mutes" do schema "thread_mutes" do
belongs_to(:user, User, type: FlakeId.Ecto.CompatType) belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
@ -18,19 +19,44 @@ defmodule Pleroma.ThreadMute do
def changeset(mute, params \\ %{}) do def changeset(mute, params \\ %{}) do
mute mute
|> Ecto.Changeset.cast(params, [:user_id, :context]) |> cast(params, [:user_id, :context])
|> Ecto.Changeset.foreign_key_constraint(:user_id) |> foreign_key_constraint(:user_id)
|> Ecto.Changeset.unique_constraint(:user_id, name: :unique_index) |> unique_constraint(:user_id, name: :unique_index)
end end
def query(user_id, context) do def query(user_id, context) do
{:ok, user_id} = FlakeId.Ecto.CompatType.dump(user_id) user_binary_id = User.binary_id(user_id)
ThreadMute ThreadMute
|> Ecto.Query.where(user_id: ^user_id) |> where(user_id: ^user_binary_id)
|> Ecto.Query.where(context: ^context) |> where(context: ^context)
end end
def muters_query(context) do
ThreadMute
|> join(:inner, [tm], u in assoc(tm, :user))
|> where([tm], tm.context == ^context)
|> select([tm, u], u.ap_id)
end
def muter_ap_ids(context, ap_ids \\ nil)
# Note: applies to fake activities (ActivityPub.Utils.get_notified_from_object/1 etc.)
def muter_ap_ids(context, _ap_ids) when is_nil(context), do: []
def muter_ap_ids(context, ap_ids) do
context
|> muters_query()
|> maybe_filter_on_ap_id(ap_ids)
|> Repo.all()
end
defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
where(query, [tm, u], u.ap_id in ^ap_ids)
end
defp maybe_filter_on_ap_id(query, _ap_ids), do: query
def add_mute(user_id, context) do def add_mute(user_id, context) do
%ThreadMute{} %ThreadMute{}
|> changeset(%{user_id: user_id, context: context}) |> changeset(%{user_id: user_id, context: context})
@ -42,8 +68,8 @@ def remove_mute(user_id, context) do
|> Repo.delete_all() |> Repo.delete_all()
end end
def check_muted(user_id, context) do def exists?(user_id, context) do
query(user_id, context) query(user_id, context)
|> Repo.all() |> Repo.exists?()
end end
end end

View file

@ -150,22 +150,26 @@ defmodule Pleroma.User do
{outgoing_relation, outgoing_relation_target}, {outgoing_relation, outgoing_relation_target},
{incoming_relation, incoming_relation_source} {incoming_relation, incoming_relation_source}
]} <- @user_relationships_config do ]} <- @user_relationships_config do
# Definitions of `has_many :blocker_blocks`, `has_many :muter_mutes` etc. # Definitions of `has_many` relations: :blocker_blocks, :muter_mutes, :reblog_muter_mutes,
# :notification_muter_mutes, :subscribee_subscriptions
has_many(outgoing_relation, UserRelationship, has_many(outgoing_relation, UserRelationship,
foreign_key: :source_id, foreign_key: :source_id,
where: [relationship_type: relationship_type] where: [relationship_type: relationship_type]
) )
# Definitions of `has_many :blockee_blocks`, `has_many :mutee_mutes` etc. # Definitions of `has_many` relations: :blockee_blocks, :mutee_mutes, :reblog_mutee_mutes,
# :notification_mutee_mutes, :subscriber_subscriptions
has_many(incoming_relation, UserRelationship, has_many(incoming_relation, UserRelationship,
foreign_key: :target_id, foreign_key: :target_id,
where: [relationship_type: relationship_type] where: [relationship_type: relationship_type]
) )
# Definitions of `has_many :blocked_users`, `has_many :muted_users` etc. # Definitions of `has_many` relations: :blocked_users, :muted_users, :reblog_muted_users,
# :notification_muted_users, :subscriber_users
has_many(outgoing_relation_target, through: [outgoing_relation, :target]) has_many(outgoing_relation_target, through: [outgoing_relation, :target])
# Definitions of `has_many :blocker_users`, `has_many :muter_users` etc. # Definitions of `has_many` relations: :blocker_users, :muter_users, :reblog_muter_users,
# :notification_muter_users, :subscribee_users
has_many(incoming_relation_source, through: [incoming_relation, :source]) has_many(incoming_relation_source, through: [incoming_relation, :source])
end end
@ -185,7 +189,9 @@ defmodule Pleroma.User do
for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <- for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
@user_relationships_config do @user_relationships_config do
# Definitions of `blocked_users_relation/1`, `muted_users_relation/1`, etc. # `def blocked_users_relation/2`, `def muted_users_relation/2`,
# `def reblog_muted_users_relation/2`, `def notification_muted_users/2`,
# `def subscriber_users/2`
def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated? \\ false) do
target_users_query = assoc(user, unquote(outgoing_relation_target)) target_users_query = assoc(user, unquote(outgoing_relation_target))
@ -196,7 +202,8 @@ def unquote(:"#{outgoing_relation_target}_relation")(user, restrict_deactivated?
end end
end end
# Definitions of `blocked_users/1`, `muted_users/1`, etc. # `def blocked_users/2`, `def muted_users/2`, `def reblog_muted_users/2`,
# `def notification_muted_users/2`, `def subscriber_users/2`
def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do
__MODULE__ __MODULE__
|> apply(unquote(:"#{outgoing_relation_target}_relation"), [ |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
@ -206,7 +213,8 @@ def unquote(outgoing_relation_target)(user, restrict_deactivated? \\ false) do
|> Repo.all() |> Repo.all()
end end
# Definitions of `blocked_users_ap_ids/1`, `muted_users_ap_ids/1`, etc. # `def blocked_users_ap_ids/2`, `def muted_users_ap_ids/2`, `def reblog_muted_users_ap_ids/2`,
# `def notification_muted_users_ap_ids/2`, `def subscriber_users_ap_ids/2`
def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \\ false) do
__MODULE__ __MODULE__
|> apply(unquote(:"#{outgoing_relation_target}_relation"), [ |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
@ -218,6 +226,24 @@ def unquote(:"#{outgoing_relation_target}_ap_ids")(user, restrict_deactivated? \
end end
end end
@doc """
Dumps Flake Id to SQL-compatible format (16-byte UUID).
E.g. "9pQtDGXuq4p3VlcJEm" -> <<0, 0, 1, 110, 179, 218, 42, 92, 213, 41, 44, 227, 95, 213, 0, 0>>
"""
def binary_id(source_id) when is_binary(source_id) do
with {:ok, dumped_id} <- FlakeId.Ecto.CompatType.dump(source_id) do
dumped_id
else
_ -> source_id
end
end
def binary_id(source_ids) when is_list(source_ids) do
Enum.map(source_ids, &binary_id/1)
end
def binary_id(%User{} = user), do: binary_id(user.id)
@doc "Returns status account" @doc "Returns status account"
@spec account_status(User.t()) :: account_status() @spec account_status(User.t()) :: account_status()
def account_status(%User{deactivated: true}), do: :deactivated def account_status(%User{deactivated: true}), do: :deactivated
@ -292,24 +318,6 @@ def ap_followers(%User{} = user), do: "#{ap_id(user)}/followers"
def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
def ap_following(%User{} = user), do: "#{ap_id(user)}/following" def ap_following(%User{} = user), do: "#{ap_id(user)}/following"
def follow_state(%User{} = user, %User{} = target) do
case Utils.fetch_latest_follow(user, target) do
%{data: %{"state" => state}} -> state
# Ideally this would be nil, but then Cachex does not commit the value
_ -> false
end
end
def get_cached_follow_state(user, target) do
key = "follow_state:#{user.ap_id}|#{target.ap_id}"
Cachex.fetch!(:user_cache, key, fn _ -> {:commit, follow_state(user, target)} end)
end
@spec set_follow_state_cache(String.t(), String.t(), String.t()) :: {:ok | :error, boolean()}
def set_follow_state_cache(user_ap_id, target_ap_id, state) do
Cachex.put(:user_cache, "follow_state:#{user_ap_id}|#{target_ap_id}", state)
end
@spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t() @spec restrict_deactivated(Ecto.Query.t()) :: Ecto.Query.t()
def restrict_deactivated(query) do def restrict_deactivated(query) do
from(u in query, where: u.deactivated != ^true) from(u in query, where: u.deactivated != ^true)
@ -428,9 +436,55 @@ def update_changeset(struct, params \\ %{}) do
|> validate_format(:nickname, local_nickname_regex()) |> validate_format(:nickname, local_nickname_regex())
|> validate_length(:bio, max: bio_limit) |> validate_length(:bio, max: bio_limit)
|> validate_length(:name, min: 1, max: name_limit) |> validate_length(:name, min: 1, max: name_limit)
|> put_fields()
|> put_change_if_present(:bio, &{:ok, parse_bio(&1, struct)})
|> put_change_if_present(:avatar, &put_upload(&1, :avatar))
|> put_change_if_present(:banner, &put_upload(&1, :banner))
|> put_change_if_present(:background, &put_upload(&1, :background))
|> put_change_if_present(
:pleroma_settings_store,
&{:ok, Map.merge(struct.pleroma_settings_store, &1)}
)
|> validate_fields(false) |> validate_fields(false)
end end
defp put_fields(changeset) do
if raw_fields = get_change(changeset, :raw_fields) do
raw_fields =
raw_fields
|> Enum.filter(fn %{"name" => n} -> n != "" end)
fields =
raw_fields
|> Enum.map(fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
changeset
|> put_change(:raw_fields, raw_fields)
|> put_change(:fields, fields)
else
changeset
end
end
defp put_change_if_present(changeset, map_field, value_function) do
if value = get_change(changeset, map_field) do
with {:ok, new_value} <- value_function.(value) do
put_change(changeset, map_field, new_value)
else
_ -> changeset
end
else
changeset
end
end
defp put_upload(value, type) do
with %Plug.Upload{} <- value,
{:ok, object} <- ActivityPub.upload(value, type: type) do
{:ok, object.data}
end
end
def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000) bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
name_limit = Pleroma.Config.get([:instance, :user_name_length], 100) name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
@ -474,6 +528,27 @@ def upgrade_changeset(struct, params \\ %{}, remote? \\ false) do
|> validate_fields(remote?) |> validate_fields(remote?)
end end
def update_as_admin_changeset(struct, params) do
struct
|> update_changeset(params)
|> cast(params, [:email])
|> delete_change(:also_known_as)
|> unique_constraint(:email)
|> validate_format(:email, @email_regex)
end
@spec update_as_admin(%User{}, map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def update_as_admin(user, params) do
params = Map.put(params, "password_confirmation", params["password"])
changeset = update_as_admin_changeset(user, params)
if params["password"] do
reset_password(user, changeset, params)
else
User.update_and_set_cache(changeset)
end
end
def password_update_changeset(struct, params) do def password_update_changeset(struct, params) do
struct struct
|> cast(params, [:password, :password_confirmation]) |> cast(params, [:password, :password_confirmation])
@ -484,10 +559,14 @@ def password_update_changeset(struct, params) do
end end
@spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()} @spec reset_password(User.t(), map) :: {:ok, User.t()} | {:error, Ecto.Changeset.t()}
def reset_password(%User{id: user_id} = user, data) do def reset_password(%User{} = user, params) do
reset_password(user, user, params)
end
def reset_password(%User{id: user_id} = user, struct, params) do
multi = multi =
Multi.new() Multi.new()
|> Multi.update(:user, password_update_changeset(user, data)) |> Multi.update(:user, password_update_changeset(struct, params))
|> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id)) |> Multi.delete_all(:tokens, OAuth.Token.Query.get_by_user(user_id))
|> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user)) |> Multi.delete_all(:auth, OAuth.Authorization.delete_by_user_query(user))
@ -692,7 +771,14 @@ def unfollow(%User{} = follower, %User{} = followed) do
def get_follow_state(%User{} = follower, %User{} = following) do def get_follow_state(%User{} = follower, %User{} = following) do
following_relationship = FollowingRelationship.get(follower, following) following_relationship = FollowingRelationship.get(follower, following)
get_follow_state(follower, following, following_relationship)
end
def get_follow_state(
%User{} = follower,
%User{} = following,
following_relationship
) do
case {following_relationship, following.local} do case {following_relationship, following.local} do
{nil, false} -> {nil, false} ->
case Utils.fetch_latest_follow(follower, following) do case Utils.fetch_latest_follow(follower, following) do
@ -1225,13 +1311,15 @@ def subscribed_to?(%User{} = user, %{ap_id: ap_id}) do
end end
@doc """ @doc """
Returns map of outgoing (blocked, muted etc.) relations' user AP IDs by relation type. Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
E.g. `outgoing_relations_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}` E.g. `outgoing_relationships_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
""" """
@spec outgoing_relations_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())} @spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
def outgoing_relations_ap_ids(_, []), do: %{} def outgoing_relationships_ap_ids(_user, []), do: %{}
def outgoing_relations_ap_ids(%User{} = user, relationship_types) def outgoing_relationships_ap_ids(nil, _relationship_types), do: %{}
def outgoing_relationships_ap_ids(%User{} = user, relationship_types)
when is_list(relationship_types) do when is_list(relationship_types) do
db_result = db_result =
user user
@ -1250,6 +1338,30 @@ def outgoing_relations_ap_ids(%User{} = user, relationship_types)
) )
end end
def incoming_relationships_ungrouped_ap_ids(user, relationship_types, ap_ids \\ nil)
def incoming_relationships_ungrouped_ap_ids(_user, [], _ap_ids), do: []
def incoming_relationships_ungrouped_ap_ids(nil, _relationship_types, _ap_ids), do: []
def incoming_relationships_ungrouped_ap_ids(%User{} = user, relationship_types, ap_ids)
when is_list(relationship_types) do
user
|> assoc(:incoming_relationships)
|> join(:inner, [user_rel], u in assoc(user_rel, :source))
|> where([user_rel, u], user_rel.relationship_type in ^relationship_types)
|> maybe_filter_on_ap_id(ap_ids)
|> select([user_rel, u], u.ap_id)
|> distinct(true)
|> Repo.all()
end
defp maybe_filter_on_ap_id(query, ap_ids) when is_list(ap_ids) do
where(query, [user_rel, u], u.ap_id in ^ap_ids)
end
defp maybe_filter_on_ap_id(query, _ap_ids), do: query
def deactivate_async(user, status \\ true) do def deactivate_async(user, status \\ true) do
BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status}) BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
end end
@ -1660,8 +1772,12 @@ def all_superusers do
|> Repo.all() |> Repo.all()
end end
def muting_reblogs?(%User{} = user, %User{} = target) do
UserRelationship.reblog_mute_exists?(user, target)
end
def showing_reblogs?(%User{} = user, %User{} = target) do def showing_reblogs?(%User{} = user, %User{} = target) do
not UserRelationship.reblog_mute_exists?(user, target) not muting_reblogs?(user, target)
end end
@doc """ @doc """
@ -1867,6 +1983,17 @@ def fields(%{fields: nil}), do: []
def fields(%{fields: fields}), do: fields def fields(%{fields: fields}), do: fields
def sanitized_fields(%User{} = user) do
user
|> User.fields()
|> Enum.map(fn %{"name" => name, "value" => value} ->
%{
"name" => name,
"value" => Pleroma.HTML.filter_tags(value, Pleroma.HTML.Scrubber.LinksOnly)
}
end)
end
def validate_fields(changeset, remote? \\ false) do def validate_fields(changeset, remote? \\ false) do
limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
limit = Pleroma.Config.get([:instance, limit_name], 0) limit = Pleroma.Config.get([:instance, limit_name], 0)

View file

@ -8,6 +8,7 @@ defmodule Pleroma.UserRelationship do
import Ecto.Changeset import Ecto.Changeset
import Ecto.Query import Ecto.Query
alias Pleroma.FollowingRelationship
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserRelationship alias Pleroma.UserRelationship
@ -21,19 +22,26 @@ defmodule Pleroma.UserRelationship do
end end
for relationship_type <- Keyword.keys(UserRelationshipTypeEnum.__enum_map__()) do for relationship_type <- Keyword.keys(UserRelationshipTypeEnum.__enum_map__()) do
# Definitions of `create_block/2`, `create_mute/2` etc. # `def create_block/2`, `def create_mute/2`, `def create_reblog_mute/2`,
# `def create_notification_mute/2`, `def create_inverse_subscription/2`
def unquote(:"create_#{relationship_type}")(source, target), def unquote(:"create_#{relationship_type}")(source, target),
do: create(unquote(relationship_type), source, target) do: create(unquote(relationship_type), source, target)
# Definitions of `delete_block/2`, `delete_mute/2` etc. # `def delete_block/2`, `def delete_mute/2`, `def delete_reblog_mute/2`,
# `def delete_notification_mute/2`, `def delete_inverse_subscription/2`
def unquote(:"delete_#{relationship_type}")(source, target), def unquote(:"delete_#{relationship_type}")(source, target),
do: delete(unquote(relationship_type), source, target) do: delete(unquote(relationship_type), source, target)
# Definitions of `block_exists?/2`, `mute_exists?/2` etc. # `def block_exists?/2`, `def mute_exists?/2`, `def reblog_mute_exists?/2`,
# `def notification_mute_exists?/2`, `def inverse_subscription_exists?/2`
def unquote(:"#{relationship_type}_exists?")(source, target), def unquote(:"#{relationship_type}_exists?")(source, target),
do: exists?(unquote(relationship_type), source, target) do: exists?(unquote(relationship_type), source, target)
end end
def user_relationship_types, do: Keyword.keys(user_relationship_mappings())
def user_relationship_mappings, do: UserRelationshipTypeEnum.__enum_map__()
def changeset(%UserRelationship{} = user_relationship, params \\ %{}) do def changeset(%UserRelationship{} = user_relationship, params \\ %{}) do
user_relationship user_relationship
|> cast(params, [:relationship_type, :source_id, :target_id]) |> cast(params, [:relationship_type, :source_id, :target_id])
@ -72,6 +80,73 @@ def delete(relationship_type, %User{} = source, %User{} = target) do
end end
end end
def dictionary(
source_users,
target_users,
source_to_target_rel_types \\ nil,
target_to_source_rel_types \\ nil
)
when is_list(source_users) and is_list(target_users) do
source_user_ids = User.binary_id(source_users)
target_user_ids = User.binary_id(target_users)
get_rel_type_codes = fn rel_type -> user_relationship_mappings()[rel_type] end
source_to_target_rel_types =
Enum.map(source_to_target_rel_types || user_relationship_types(), &get_rel_type_codes.(&1))
target_to_source_rel_types =
Enum.map(target_to_source_rel_types || user_relationship_types(), &get_rel_type_codes.(&1))
__MODULE__
|> where(
fragment(
"(source_id = ANY(?) AND target_id = ANY(?) AND relationship_type = ANY(?)) OR \
(source_id = ANY(?) AND target_id = ANY(?) AND relationship_type = ANY(?))",
^source_user_ids,
^target_user_ids,
^source_to_target_rel_types,
^target_user_ids,
^source_user_ids,
^target_to_source_rel_types
)
)
|> select([ur], [ur.relationship_type, ur.source_id, ur.target_id])
|> Repo.all()
end
def exists?(dictionary, rel_type, source, target, func) do
cond do
is_nil(source) or is_nil(target) ->
false
dictionary ->
[rel_type, source.id, target.id] in dictionary
true ->
func.(source, target)
end
end
@doc ":relationships option for StatusView / AccountView / NotificationView"
def view_relationships_option(nil = _reading_user, _actors) do
%{user_relationships: [], following_relationships: []}
end
def view_relationships_option(%User{} = reading_user, actors) do
user_relationships =
UserRelationship.dictionary(
[reading_user],
actors,
[:block, :mute, :notification_mute, :reblog_mute],
[:block, :inverse_subscription]
)
following_relationships = FollowingRelationship.all_between_user_sets([reading_user], actors)
%{user_relationships: user_relationships, following_relationships: following_relationships}
end
defp validate_not_self_relationship(%Ecto.Changeset{} = changeset) do defp validate_not_self_relationship(%Ecto.Changeset{} = changeset) do
changeset changeset
|> validate_change(:target_id, fn _, target_id -> |> validate_change(:target_id, fn _, target_id ->

View file

@ -503,8 +503,7 @@ def follow(follower, followed, activity_id \\ nil, local \\ true) do
defp do_follow(follower, followed, activity_id, local) do defp do_follow(follower, followed, activity_id, local) do
with data <- make_follow_data(follower, followed, activity_id), with data <- make_follow_data(follower, followed, activity_id),
{:ok, activity} <- insert(data, local), {:ok, activity} <- insert(data, local),
:ok <- maybe_federate(activity), :ok <- maybe_federate(activity) do
_ <- User.set_follow_state_cache(follower.ap_id, followed.ap_id, activity.data["state"]) do
{:ok, activity} {:ok, activity}
else else
{:error, error} -> Repo.rollback(error) {:error, error} -> Repo.rollback(error)
@ -584,6 +583,16 @@ defp do_delete(%Object{data: %{"id" => id, "actor" => actor}} = object, options)
end end
end end
defp do_delete(%Object{data: %{"type" => "Tombstone", "id" => ap_id}}, _) do
activity =
ap_id
|> Activity.Queries.by_object_id()
|> Activity.Queries.by_type("Delete")
|> Repo.one()
{:ok, activity}
end
@spec block(User.t(), User.t(), String.t() | nil, boolean()) :: @spec block(User.t(), User.t(), String.t() | nil, boolean()) ::
{:ok, Activity.t()} | {:error, any()} {:ok, Activity.t()} | {:error, any()}
def block(blocker, blocked, activity_id \\ nil, local \\ true) do def block(blocker, blocked, activity_id \\ nil, local \\ true) do
@ -1230,17 +1239,17 @@ defp maybe_order(query, _), do: query
defp fetch_activities_query_ap_ids_ops(opts) do defp fetch_activities_query_ap_ids_ops(opts) do
source_user = opts["muting_user"] source_user = opts["muting_user"]
ap_id_relations = if source_user, do: [:mute, :reblog_mute], else: [] ap_id_relationships = if source_user, do: [:mute, :reblog_mute], else: []
ap_id_relations = ap_id_relationships =
ap_id_relations ++ ap_id_relationships ++
if opts["blocking_user"] && opts["blocking_user"] == source_user do if opts["blocking_user"] && opts["blocking_user"] == source_user do
[:block] [:block]
else else
[] []
end end
preloaded_ap_ids = User.outgoing_relations_ap_ids(source_user, ap_id_relations) preloaded_ap_ids = User.outgoing_relationships_ap_ids(source_user, ap_id_relationships)
restrict_blocked_opts = Map.merge(%{"blocked_users_ap_ids" => preloaded_ap_ids[:block]}, opts) restrict_blocked_opts = Map.merge(%{"blocked_users_ap_ids" => preloaded_ap_ids[:block]}, opts)
restrict_muted_opts = Map.merge(%{"muted_users_ap_ids" => preloaded_ap_ids[:mute]}, opts) restrict_muted_opts = Map.merge(%{"muted_users_ap_ids" => preloaded_ap_ids[:mute]}, opts)

View file

@ -229,7 +229,8 @@ def fix_url(%{"url" => url} = object) when is_map(url) do
Map.put(object, "url", url["href"]) Map.put(object, "url", url["href"])
end end
def fix_url(%{"type" => "Video", "url" => url} = object) when is_list(url) do def fix_url(%{"type" => object_type, "url" => url} = object)
when object_type in ["Video", "Audio"] and is_list(url) do
first_element = Enum.at(url, 0) first_element = Enum.at(url, 0)
link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end) link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end)
@ -398,7 +399,7 @@ def handle_incoming(
%{"type" => "Create", "object" => %{"type" => objtype} = object} = data, %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
options options
) )
when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer"] do when objtype in ["Article", "Event", "Note", "Video", "Page", "Question", "Answer", "Audio"] do
actor = Containment.get_actor(data) actor = Containment.get_actor(data)
data = data =
@ -1108,13 +1109,11 @@ def add_hashtags(object) do
end end
def add_mention_tags(object) do def add_mention_tags(object) do
mentions = {enabled_receivers, disabled_receivers} = Utils.get_notified_from_object(object)
object potential_receivers = enabled_receivers ++ disabled_receivers
|> Utils.get_notified_from_object() mentions = Enum.map(potential_receivers, &build_mention_tag/1)
|> Enum.map(&build_mention_tag/1)
tags = object["tag"] || [] tags = object["tag"] || []
Map.put(object, "tag", tags ++ mentions) Map.put(object, "tag", tags ++ mentions)
end end

View file

@ -440,22 +440,19 @@ def update_follow_state_for_all(
|> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)]) |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
|> Repo.update_all([]) |> Repo.update_all([])
User.set_follow_state_cache(actor, object, state)
activity = Activity.get_by_id(activity.id) activity = Activity.get_by_id(activity.id)
{:ok, activity} {:ok, activity}
end end
def update_follow_state( def update_follow_state(
%Activity{data: %{"actor" => actor, "object" => object}} = activity, %Activity{} = activity,
state state
) do ) do
new_data = Map.put(activity.data, "state", state) new_data = Map.put(activity.data, "state", state)
changeset = Changeset.change(activity, data: new_data) changeset = Changeset.change(activity, data: new_data)
with {:ok, activity} <- Repo.update(changeset) do with {:ok, activity} <- Repo.update(changeset) do
User.set_follow_state_cache(actor, object, state)
{:ok, activity} {:ok, activity}
end end
end end

View file

@ -38,7 +38,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
plug( plug(
OAuthScopesPlug, OAuthScopesPlug,
%{scopes: ["read:accounts"], admin: true} %{scopes: ["read:accounts"], admin: true}
when action in [:list_users, :user_show, :right_get] when action in [:list_users, :user_show, :right_get, :show_user_credentials]
) )
plug( plug(
@ -54,7 +54,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
:tag_users, :tag_users,
:untag_users, :untag_users,
:right_add, :right_add,
:right_delete :right_delete,
:update_user_credentials
] ]
) )
@ -658,6 +659,52 @@ def force_password_reset(%{assigns: %{user: admin}} = conn, %{"nicknames" => nic
json_response(conn, :no_content, "") json_response(conn, :no_content, "")
end end
@doc "Show a given user's credentials"
def show_user_credentials(%{assigns: %{user: admin}} = conn, %{"nickname" => nickname}) do
with %User{} = user <- User.get_cached_by_nickname_or_id(nickname) do
conn
|> put_view(AccountView)
|> render("credentials.json", %{user: user, for: admin})
else
_ -> {:error, :not_found}
end
end
@doc "Updates a given user"
def update_user_credentials(
%{assigns: %{user: admin}} = conn,
%{"nickname" => nickname} = params
) do
with {_, user} <- {:user, User.get_cached_by_nickname(nickname)},
{:ok, _user} <-
User.update_as_admin(user, params) do
ModerationLog.insert_log(%{
actor: admin,
subject: [user],
action: "updated_users"
})
if params["password"] do
User.force_password_reset_async(user)
end
ModerationLog.insert_log(%{
actor: admin,
subject: [user],
action: "force_password_reset"
})
json(conn, %{status: "success"})
else
{:error, changeset} ->
{_, {error, _}} = Enum.at(changeset.errors, 0)
json(conn, %{error: "New password #{error}."})
_ ->
json(conn, %{error: "Unable to change password."})
end
end
def list_reports(conn, params) do def list_reports(conn, params) do
{page, page_size} = page_params(params) {page, page_size} = page_params(params)

View file

@ -23,6 +23,43 @@ def render("index.json", %{users: users}) do
} }
end end
def render("credentials.json", %{user: user, for: for_user}) do
user = User.sanitize_html(user, User.html_filter_policy(for_user))
avatar = User.avatar_url(user) |> MediaProxy.url()
banner = User.banner_url(user) |> MediaProxy.url()
background = image_url(user.background) |> MediaProxy.url()
user
|> Map.take([
:id,
:bio,
:email,
:fields,
:name,
:nickname,
:locked,
:no_rich_text,
:default_scope,
:hide_follows,
:hide_followers_count,
:hide_follows_count,
:hide_followers,
:hide_favorites,
:allow_following_move,
:show_role,
:skip_thread_containment,
:pleroma_settings_store,
:raw_fields,
:discoverable,
:actor_type
])
|> Map.merge(%{
"avatar" => avatar,
"banner" => banner,
"background" => background
})
end
def render("show.json", %{user: user}) do def render("show.json", %{user: user}) do
avatar = User.avatar_url(user) |> MediaProxy.url() avatar = User.avatar_url(user) |> MediaProxy.url()
display_name = Pleroma.HTML.strip_tags(user.name || user.nickname) display_name = Pleroma.HTML.strip_tags(user.name || user.nickname)
@ -104,4 +141,7 @@ defp parse_error(errors) do
"" ""
end end
end end
defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
defp image_url(_), do: nil
end end

View file

@ -358,7 +358,7 @@ def remove_mute(user, activity) do
def thread_muted?(%{id: nil} = _user, _activity), do: false def thread_muted?(%{id: nil} = _user, _activity), do: false
def thread_muted?(user, activity) do def thread_muted?(user, activity) do
ThreadMute.check_muted(user.id, activity.data["context"]) != [] ThreadMute.exists?(user.id, activity.data["context"])
end end
def report(user, %{"account_id" => account_id} = data) do def report(user, %{"account_id" => account_id} = data) do

View file

@ -34,7 +34,12 @@ defp param_to_integer(val, default) when is_binary(val) do
defp param_to_integer(_, default), do: default defp param_to_integer(_, default), do: default
def add_link_headers(conn, activities, extra_params \\ %{}) do def add_link_headers(conn, activities, extra_params \\ %{})
def add_link_headers(%{assigns: %{skip_link_headers: true}} = conn, _activities, _extra_params),
do: conn
def add_link_headers(conn, activities, extra_params) do
case List.last(activities) do case List.last(activities) do
%{id: max_id} -> %{id: max_id} ->
params = params =

View file

@ -8,7 +8,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
import Pleroma.Web.ControllerHelper, import Pleroma.Web.ControllerHelper,
only: [add_link_headers: 2, truthy_param?: 1, assign_account_by_id: 2, json_response: 3] only: [add_link_headers: 2, truthy_param?: 1, assign_account_by_id: 2, json_response: 3]
alias Pleroma.Emoji
alias Pleroma.Plugs.OAuthScopesPlug alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Plugs.RateLimiter alias Pleroma.Plugs.RateLimiter
alias Pleroma.User alias Pleroma.User
@ -63,11 +62,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
when action not in [:create, :show, :statuses] when action not in [:create, :show, :statuses]
) )
@relations [:follow, :unfollow] @relationship_actions [:follow, :unfollow]
@needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a @needs_account ~W(followers following lists follow unfollow mute unmute block unblock)a
plug(RateLimiter, [name: :relations_id_action, params: ["id", "uri"]] when action in @relations) plug(
plug(RateLimiter, [name: :relations_actions] when action in @relations) RateLimiter,
[name: :relation_id_action, params: ["id", "uri"]] when action in @relationship_actions
)
plug(RateLimiter, [name: :relations_actions] when action in @relationship_actions)
plug(RateLimiter, [name: :app_account_creation] when action == :create) plug(RateLimiter, [name: :app_account_creation] when action == :create)
plug(:assign_account_by_id when action in @needs_account) plug(:assign_account_by_id when action in @needs_account)
@ -140,17 +143,6 @@ def verify_credentials(%{assigns: %{user: user}} = conn, _) do
def update_credentials(%{assigns: %{user: original_user}} = conn, params) do def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
user = original_user user = original_user
params =
if Map.has_key?(params, "fields_attributes") do
Map.update!(params, "fields_attributes", fn fields ->
fields
|> normalize_fields_attributes()
|> Enum.filter(fn %{"name" => n} -> n != "" end)
end)
else
params
end
user_params = user_params =
[ [
:no_rich_text, :no_rich_text,
@ -169,46 +161,20 @@ def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)}) add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
end) end)
|> add_if_present(params, "display_name", :name) |> add_if_present(params, "display_name", :name)
|> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end) |> add_if_present(params, "note", :bio)
|> add_if_present(params, "avatar", :avatar, fn value -> |> add_if_present(params, "avatar", :avatar)
with %Plug.Upload{} <- value, |> add_if_present(params, "header", :banner)
{:ok, object} <- ActivityPub.upload(value, type: :avatar) do |> add_if_present(params, "pleroma_background_image", :background)
{:ok, object.data} |> add_if_present(
end params,
end) "fields_attributes",
|> add_if_present(params, "header", :banner, fn value -> :raw_fields,
with %Plug.Upload{} <- value, &{:ok, normalize_fields_attributes(&1)}
{:ok, object} <- ActivityPub.upload(value, type: :banner) do )
{:ok, object.data} |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store)
end
end)
|> add_if_present(params, "pleroma_background_image", :background, fn value ->
with %Plug.Upload{} <- value,
{:ok, object} <- ActivityPub.upload(value, type: :background) do
{:ok, object.data}
end
end)
|> add_if_present(params, "fields_attributes", :fields, fn fields ->
fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
{:ok, fields}
end)
|> add_if_present(params, "fields_attributes", :raw_fields)
|> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
{:ok, Map.merge(user.pleroma_settings_store, value)}
end)
|> add_if_present(params, "default_scope", :default_scope) |> add_if_present(params, "default_scope", :default_scope)
|> add_if_present(params, "actor_type", :actor_type) |> add_if_present(params, "actor_type", :actor_type)
emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
user_emojis =
user
|> Map.get(:emoji, [])
|> Enum.concat(Emoji.Formatter.get_emoji_map(emojis_text))
|> Enum.dedup()
user_params = Map.put(user_params, :emoji, user_emojis)
changeset = User.update_changeset(user, user_params) changeset = User.update_changeset(user, user_params)
with {:ok, user} <- User.update_and_set_cache(changeset) do with {:ok, user} <- User.update_and_set_cache(changeset) do

View file

@ -5,12 +5,28 @@
defmodule Pleroma.Web.MastodonAPI.AccountView do defmodule Pleroma.Web.MastodonAPI.AccountView do
use Pleroma.Web, :view use Pleroma.Web, :view
alias Pleroma.FollowingRelationship
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserRelationship
alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MediaProxy alias Pleroma.Web.MediaProxy
def render("index.json", %{users: users} = opts) do def render("index.json", %{users: users} = opts) do
relationships_opt =
cond do
Map.has_key?(opts, :relationships) ->
opts[:relationships]
is_nil(opts[:for]) ->
UserRelationship.view_relationships_option(nil, [])
true ->
UserRelationship.view_relationships_option(opts[:for], users)
end
opts = Map.put(opts, :relationships, relationships_opt)
users users
|> render_many(AccountView, "show.json", opts) |> render_many(AccountView, "show.json", opts)
|> Enum.filter(&Enum.any?/1) |> Enum.filter(&Enum.any?/1)
@ -35,34 +51,107 @@ def render("relationship.json", %{user: nil, target: _target}) do
%{} %{}
end end
def render("relationship.json", %{user: %User{} = user, target: %User{} = target}) do def render(
follow_state = User.get_cached_follow_state(user, target) "relationship.json",
%{user: %User{} = reading_user, target: %User{} = target} = opts
) do
user_relationships = get_in(opts, [:relationships, :user_relationships])
following_relationships = get_in(opts, [:relationships, :following_relationships])
requested = follow_state =
if follow_state && !User.following?(user, target) do if following_relationships do
follow_state == "pending" user_to_target_following_relation =
FollowingRelationship.find(following_relationships, reading_user, target)
User.get_follow_state(reading_user, target, user_to_target_following_relation)
else else
false User.get_follow_state(reading_user, target)
end end
followed_by =
if following_relationships do
case FollowingRelationship.find(following_relationships, target, reading_user) do
%{state: "accept"} -> true
_ -> false
end
else
User.following?(target, reading_user)
end
# NOTE: adjust UserRelationship.view_relationships_option/2 on new relation-related flags
%{ %{
id: to_string(target.id), id: to_string(target.id),
following: User.following?(user, target), following: follow_state == "accept",
followed_by: User.following?(target, user), followed_by: followed_by,
blocking: User.blocks_user?(user, target), blocking:
blocked_by: User.blocks_user?(target, user), UserRelationship.exists?(
muting: User.mutes?(user, target), user_relationships,
muting_notifications: User.muted_notifications?(user, target), :block,
subscribing: User.subscribed_to?(user, target), reading_user,
requested: requested, target,
domain_blocking: User.blocks_domain?(user, target), &User.blocks_user?(&1, &2)
showing_reblogs: User.showing_reblogs?(user, target), ),
blocked_by:
UserRelationship.exists?(
user_relationships,
:block,
target,
reading_user,
&User.blocks_user?(&1, &2)
),
muting:
UserRelationship.exists?(
user_relationships,
:mute,
reading_user,
target,
&User.mutes?(&1, &2)
),
muting_notifications:
UserRelationship.exists?(
user_relationships,
:notification_mute,
reading_user,
target,
&User.muted_notifications?(&1, &2)
),
subscribing:
UserRelationship.exists?(
user_relationships,
:inverse_subscription,
target,
reading_user,
&User.subscribed_to?(&2, &1)
),
requested: follow_state == "pending",
domain_blocking: User.blocks_domain?(reading_user, target),
showing_reblogs:
not UserRelationship.exists?(
user_relationships,
:reblog_mute,
reading_user,
target,
&User.muting_reblogs?(&1, &2)
),
endorsed: false endorsed: false
} }
end end
def render("relationships.json", %{user: user, targets: targets}) do def render("relationships.json", %{user: user, targets: targets} = opts) do
render_many(targets, AccountView, "relationship.json", user: user, as: :target) relationships_opt =
cond do
Map.has_key?(opts, :relationships) ->
opts[:relationships]
is_nil(opts[:for]) ->
UserRelationship.view_relationships_option(nil, [])
true ->
UserRelationship.view_relationships_option(user, targets)
end
render_opts = %{as: :target, user: user, relationships: relationships_opt}
render_many(targets, AccountView, "relationship.json", render_opts)
end end
defp do_render("show.json", %{user: user} = opts) do defp do_render("show.json", %{user: user} = opts) do
@ -100,7 +189,12 @@ defp do_render("show.json", %{user: user} = opts) do
} }
end) end)
relationship = render("relationship.json", %{user: opts[:for], target: user}) relationship =
render("relationship.json", %{
user: opts[:for],
target: user,
relationships: opts[:relationships]
})
%{ %{
id: to_string(user.id), id: to_string(user.id),
@ -122,7 +216,7 @@ defp do_render("show.json", %{user: user} = opts) do
fields: user.fields, fields: user.fields,
bot: bot, bot: bot,
source: %{ source: %{
note: Pleroma.HTML.strip_tags((user.bio || "") |> String.replace("<br>", "\n")), note: (user.bio || "") |> String.replace(~r(<br */?>), "\n") |> Pleroma.HTML.strip_tags(),
sensitive: false, sensitive: false,
fields: user.raw_fields, fields: user.raw_fields,
pleroma: %{ pleroma: %{

View file

@ -8,24 +8,86 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
alias Pleroma.Activity alias Pleroma.Activity
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserRelationship
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.NotificationView alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
def render("index.json", %{notifications: notifications, for: user}) do def render("index.json", %{notifications: notifications, for: reading_user} = opts) do
safe_render_many(notifications, NotificationView, "show.json", %{for: user}) activities = Enum.map(notifications, & &1.activity)
parent_activities =
activities
|> Enum.filter(
&(Activity.mastodon_notification_type(&1) in [
"favourite",
"reblog",
"pleroma:emoji_reaction"
])
)
|> Enum.map(& &1.data["object"])
|> Activity.create_by_object_ap_id()
|> Activity.with_preloaded_object(:left)
|> Pleroma.Repo.all()
relationships_opt =
cond do
Map.has_key?(opts, :relationships) ->
opts[:relationships]
is_nil(opts[:for]) ->
UserRelationship.view_relationships_option(nil, [])
true ->
move_activities_targets =
activities
|> Enum.filter(&(Activity.mastodon_notification_type(&1) == "move"))
|> Enum.map(&User.get_cached_by_ap_id(&1.data["target"]))
actors =
activities
|> Enum.map(fn a -> User.get_cached_by_ap_id(a.data["actor"]) end)
|> Enum.filter(& &1)
|> Kernel.++(move_activities_targets)
UserRelationship.view_relationships_option(reading_user, actors)
end end
def render("show.json", %{ opts = %{
for: reading_user,
parent_activities: parent_activities,
relationships: relationships_opt
}
safe_render_many(notifications, NotificationView, "show.json", opts)
end
def render(
"show.json",
%{
notification: %Notification{activity: activity} = notification, notification: %Notification{activity: activity} = notification,
for: user for: reading_user
}) do } = opts
) do
actor = User.get_cached_by_ap_id(activity.data["actor"]) actor = User.get_cached_by_ap_id(activity.data["actor"])
parent_activity = Activity.get_create_by_object_ap_id(activity.data["object"])
parent_activity_fn = fn ->
if opts[:parent_activities] do
Activity.Queries.find_by_object_ap_id(opts[:parent_activities], activity.data["object"])
else
Activity.get_create_by_object_ap_id(activity.data["object"])
end
end
mastodon_type = Activity.mastodon_notification_type(activity) mastodon_type = Activity.mastodon_notification_type(activity)
with %{id: _} = account <- AccountView.render("show.json", %{user: actor, for: user}) do with %{id: _} = account <-
AccountView.render("show.json", %{
user: actor,
for: reading_user,
relationships: opts[:relationships]
}) do
response = %{ response = %{
id: to_string(notification.id), id: to_string(notification.id),
type: mastodon_type, type: mastodon_type,
@ -36,24 +98,28 @@ def render("show.json", %{
} }
} }
render_opts = %{relationships: opts[:relationships]}
case mastodon_type do case mastodon_type do
"mention" -> "mention" ->
put_status(response, activity, user) put_status(response, activity, reading_user, render_opts)
"favourite" -> "favourite" ->
put_status(response, parent_activity, user) put_status(response, parent_activity_fn.(), reading_user, render_opts)
"reblog" -> "reblog" ->
put_status(response, parent_activity, user) put_status(response, parent_activity_fn.(), reading_user, render_opts)
"move" -> "move" ->
put_target(response, activity, user) put_target(response, activity, reading_user, render_opts)
"follow" -> "follow" ->
response response
"pleroma:emoji_reaction" -> "pleroma:emoji_reaction" ->
put_status(response, parent_activity, user) |> put_emoji(activity) response
|> put_status(parent_activity_fn.(), reading_user, render_opts)
|> put_emoji(activity)
_ -> _ ->
nil nil
@ -64,16 +130,21 @@ def render("show.json", %{
end end
defp put_emoji(response, activity) do defp put_emoji(response, activity) do
response Map.put(response, :emoji, activity.data["content"])
|> Map.put(:emoji, activity.data["content"])
end end
defp put_status(response, activity, user) do defp put_status(response, activity, reading_user, opts) do
Map.put(response, :status, StatusView.render("show.json", %{activity: activity, for: user})) status_render_opts = Map.merge(opts, %{activity: activity, for: reading_user})
status_render = StatusView.render("show.json", status_render_opts)
Map.put(response, :status, status_render)
end end
defp put_target(response, activity, user) do defp put_target(response, activity, reading_user, opts) do
target = User.get_cached_by_ap_id(activity.data["target"]) target_user = User.get_cached_by_ap_id(activity.data["target"])
Map.put(response, :target, AccountView.render("show.json", %{user: target, for: user})) target_render_opts = Map.merge(opts, %{user: target_user, for: reading_user})
target_render = AccountView.render("show.json", target_render_opts)
Map.put(response, :target, target_render)
end end
end end

View file

@ -13,6 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserRelationship
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.AccountView
@ -71,10 +72,41 @@ defp reblogged?(activity, user) do
end end
def render("index.json", opts) do def render("index.json", opts) do
replied_to_activities = get_replied_to_activities(opts.activities) # To do: check AdminAPIControllerTest on the reasons behind nil activities in the list
opts = Map.put(opts, :replied_to_activities, replied_to_activities) activities = Enum.filter(opts.activities, & &1)
replied_to_activities = get_replied_to_activities(activities)
safe_render_many(opts.activities, StatusView, "show.json", opts) parent_activities =
activities
|> Enum.filter(&(&1.data["type"] == "Announce" && &1.data["object"]))
|> Enum.map(&Object.normalize(&1).data["id"])
|> Activity.create_by_object_ap_id()
|> Activity.with_preloaded_object(:left)
|> Activity.with_preloaded_bookmark(opts[:for])
|> Activity.with_set_thread_muted_field(opts[:for])
|> Repo.all()
relationships_opt =
cond do
Map.has_key?(opts, :relationships) ->
opts[:relationships]
is_nil(opts[:for]) ->
UserRelationship.view_relationships_option(nil, [])
true ->
actors = Enum.map(activities ++ parent_activities, &get_user(&1.data["actor"]))
UserRelationship.view_relationships_option(opts[:for], actors)
end
opts =
opts
|> Map.put(:replied_to_activities, replied_to_activities)
|> Map.put(:parent_activities, parent_activities)
|> Map.put(:relationships, relationships_opt)
safe_render_many(activities, StatusView, "show.json", opts)
end end
def render( def render(
@ -85,17 +117,25 @@ def render(
created_at = Utils.to_masto_date(activity.data["published"]) created_at = Utils.to_masto_date(activity.data["published"])
activity_object = Object.normalize(activity) activity_object = Object.normalize(activity)
reblogged_activity = reblogged_parent_activity =
if opts[:parent_activities] do
Activity.Queries.find_by_object_ap_id(
opts[:parent_activities],
activity_object.data["id"]
)
else
Activity.create_by_object_ap_id(activity_object.data["id"]) Activity.create_by_object_ap_id(activity_object.data["id"])
|> Activity.with_preloaded_bookmark(opts[:for]) |> Activity.with_preloaded_bookmark(opts[:for])
|> Activity.with_set_thread_muted_field(opts[:for]) |> Activity.with_set_thread_muted_field(opts[:for])
|> Repo.one() |> Repo.one()
end
reblogged = render("show.json", Map.put(opts, :activity, reblogged_activity)) reblog_rendering_opts = Map.put(opts, :activity, reblogged_parent_activity)
reblogged = render("show.json", reblog_rendering_opts)
favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || []) favorited = opts[:for] && opts[:for].ap_id in (activity_object.data["likes"] || [])
bookmarked = Activity.get_bookmark(reblogged_activity, opts[:for]) != nil bookmarked = Activity.get_bookmark(reblogged_parent_activity, opts[:for]) != nil
mentions = mentions =
activity.recipients activity.recipients
@ -107,7 +147,12 @@ def render(
id: to_string(activity.id), id: to_string(activity.id),
uri: activity_object.data["id"], uri: activity_object.data["id"],
url: activity_object.data["id"], url: activity_object.data["id"],
account: AccountView.render("show.json", %{user: user, for: opts[:for]}), account:
AccountView.render("show.json", %{
user: user,
for: opts[:for],
relationships: opts[:relationships]
}),
in_reply_to_id: nil, in_reply_to_id: nil,
in_reply_to_account_id: nil, in_reply_to_account_id: nil,
reblog: reblogged, reblog: reblogged,
@ -116,7 +161,7 @@ def render(
reblogs_count: 0, reblogs_count: 0,
replies_count: 0, replies_count: 0,
favourites_count: 0, favourites_count: 0,
reblogged: reblogged?(reblogged_activity, opts[:for]), reblogged: reblogged?(reblogged_parent_activity, opts[:for]),
favourited: present?(favorited), favourited: present?(favorited),
bookmarked: present?(bookmarked), bookmarked: present?(bookmarked),
muted: false, muted: false,
@ -183,9 +228,10 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
end end
thread_muted? = thread_muted? =
case activity.thread_muted? do cond do
thread_muted? when is_boolean(thread_muted?) -> thread_muted? is_nil(opts[:for]) -> false
nil -> (opts[:for] && CommonAPI.thread_muted?(opts[:for], activity)) || false is_boolean(activity.thread_muted?) -> activity.thread_muted?
true -> CommonAPI.thread_muted?(opts[:for], activity)
end end
attachment_data = object.data["attachment"] || [] attachment_data = object.data["attachment"] || []
@ -253,11 +299,26 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
_ -> [] _ -> []
end end
muted =
thread_muted? ||
UserRelationship.exists?(
get_in(opts, [:relationships, :user_relationships]),
:mute,
opts[:for],
user,
fn for_user, user -> User.mutes?(for_user, user) end
)
%{ %{
id: to_string(activity.id), id: to_string(activity.id),
uri: object.data["id"], uri: object.data["id"],
url: url, url: url,
account: AccountView.render("show.json", %{user: user, for: opts[:for]}), account:
AccountView.render("show.json", %{
user: user,
for: opts[:for],
relationships: opts[:relationships]
}),
in_reply_to_id: reply_to && to_string(reply_to.id), in_reply_to_id: reply_to && to_string(reply_to.id),
in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id), in_reply_to_account_id: reply_to_user && to_string(reply_to_user.id),
reblog: nil, reblog: nil,
@ -270,7 +331,7 @@ def render("show.json", %{activity: %{data: %{"object" => _object}} = activity}
reblogged: reblogged?(activity, opts[:for]), reblogged: reblogged?(activity, opts[:for]),
favourited: present?(favorited), favourited: present?(favorited),
bookmarked: present?(bookmarked), bookmarked: present?(bookmarked),
muted: thread_muted? || User.mutes?(opts[:for], user), muted: muted,
pinned: pinned?(activity, user), pinned: pinned?(activity, user),
sensitive: sensitive, sensitive: sensitive,
spoiler_text: summary, spoiler_text: summary,
@ -421,7 +482,7 @@ def get_reply_to(%{data: %{"object" => _object}} = activity, _) do
end end
def render_content(%{data: %{"type" => object_type}} = object) def render_content(%{data: %{"type" => object_type}} = object)
when object_type in ["Video", "Event"] do when object_type in ["Video", "Event", "Audio"] do
with name when not is_nil(name) and name != "" <- object.data["name"] do with name when not is_nil(name) and name != "" <- object.data["name"] do
"<p><a href=\"#{object.data["id"]}\">#{name}</a></p>#{object.data["content"]}" "<p><a href=\"#{object.data["id"]}\">#{name}</a></p>#{object.data["content"]}"
else else

View file

@ -173,6 +173,8 @@ defmodule Pleroma.Web.Router do
get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset) get("/users/:nickname/password_reset", AdminAPIController, :get_password_reset)
patch("/users/force_password_reset", AdminAPIController, :force_password_reset) patch("/users/force_password_reset", AdminAPIController, :force_password_reset)
get("/users/:nickname/credentials", AdminAPIController, :show_user_credentials)
patch("/users/:nickname/credentials", AdminAPIController, :update_user_credentials)
get("/users", AdminAPIController, :list_users) get("/users", AdminAPIController, :list_users)
get("/users/:nickname", AdminAPIController, :user_show) get("/users/:nickname", AdminAPIController, :user_show)

View file

@ -60,7 +60,9 @@ defp represent(%Activity{object: %Object{data: data}} = activity, selected) do
content = content =
if data["content"] do if data["content"] do
Pleroma.HTML.filter_tags(data["content"]) data["content"]
|> Pleroma.HTML.filter_tags()
|> Pleroma.Emoji.Formatter.emojify(Map.get(data, "emoji", %{}))
else else
nil nil
end end

View file

@ -130,7 +130,7 @@ defp do_stream(%{topic: topic, item: item}) do
defp should_send?(%User{} = user, %Activity{} = item) do defp should_send?(%User{} = user, %Activity{} = item) do
%{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} = %{block: blocked_ap_ids, mute: muted_ap_ids, reblog_mute: reblog_muted_ap_ids} =
User.outgoing_relations_ap_ids(user, [:block, :mute, :reblog_mute]) User.outgoing_relationships_ap_ids(user, [:block, :mute, :reblog_mute])
recipient_blocks = MapSet.new(blocked_ap_ids ++ muted_ap_ids) recipient_blocks = MapSet.new(blocked_ap_ids ++ muted_ap_ids)
recipients = MapSet.new(item.recipients) recipients = MapSet.new(item.recipients)

View file

@ -63,7 +63,7 @@ def copy_nginx_config(%{path: target_path} = release) do
def application do def application do
[ [
mod: {Pleroma.Application, []}, mod: {Pleroma.Application, []},
extra_applications: [:logger, :runtime_tools, :comeonin, :quack, :fast_sanitize], extra_applications: [:logger, :runtime_tools, :comeonin, :quack, :fast_sanitize, :ssl],
included_applications: [:ex_syslogger] included_applications: [:ex_syslogger]
] ]
end end

View file

@ -174,3 +174,10 @@ .alert-info {
font-weight: 500; font-weight: 500;
font-size: 16px; font-size: 16px;
} }
img.emoji {
width: 32px;
height: 32px;
padding: 0;
vertical-align: middle;
}

View file

@ -0,0 +1,44 @@
{
"id": "https://channels.tests.funkwhale.audio/federation/music/uploads/42342395-0208-4fee-a38d-259a6dae0871",
"type": "Audio",
"name": "Compositions - Test Audio for Pleroma",
"attributedTo": "https://channels.tests.funkwhale.audio/federation/actors/compositions",
"published": "2020-03-11T10:01:52.714918+00:00",
"to": "https://www.w3.org/ns/activitystreams#Public",
"url": [
{
"type": "Link",
"mimeType": "audio/ogg",
"href": "https://channels.tests.funkwhale.audio/api/v1/listen/3901e5d8-0445-49d5-9711-e096cf32e515/?upload=42342395-0208-4fee-a38d-259a6dae0871&download=false"
},
{
"type": "Link",
"mimeType": "text/html",
"href": "https://channels.tests.funkwhale.audio/library/tracks/74"
}
],
"content": "<p>This is a test Audio for Pleroma.</p>",
"mediaType": "text/html",
"tag": [
{
"type": "Hashtag",
"name": "#funkwhale"
},
{
"type": "Hashtag",
"name": "#test"
},
{
"type": "Hashtag",
"name": "#tests"
}
],
"summary": "#funkwhale #test #tests",
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers"
}
]
}

View file

@ -0,0 +1,44 @@
{
"id": "https://channels.tests.funkwhale.audio/federation/actors/compositions",
"outbox": "https://channels.tests.funkwhale.audio/federation/actors/compositions/outbox",
"inbox": "https://channels.tests.funkwhale.audio/federation/actors/compositions/inbox",
"preferredUsername": "compositions",
"type": "Person",
"name": "Compositions",
"followers": "https://channels.tests.funkwhale.audio/federation/actors/compositions/followers",
"following": "https://channels.tests.funkwhale.audio/federation/actors/compositions/following",
"manuallyApprovesFollowers": false,
"url": [
{
"type": "Link",
"href": "https://channels.tests.funkwhale.audio/channels/compositions",
"mediaType": "text/html"
},
{
"type": "Link",
"href": "https://channels.tests.funkwhale.audio/api/v1/channels/compositions/rss",
"mediaType": "application/rss+xml"
}
],
"icon": {
"type": "Image",
"url": "https://channels.tests.funkwhale.audio/media/attachments/75/b4/f1/nosmile.jpeg",
"mediaType": "image/jpeg"
},
"summary": "<p>I'm testing federation with the fediverse :)</p>",
"@context": [
"https://www.w3.org/ns/activitystreams",
"https://w3id.org/security/v1",
{
"manuallyApprovesFollowers": "as:manuallyApprovesFollowers"
}
],
"publicKey": {
"owner": "https://channels.tests.funkwhale.audio/federation/actors/compositions",
"publicKeyPem": "-----BEGIN RSA PUBLIC KEY-----\nMIIBCgKCAQEAv25u57oZfVLV3KltS+HcsdSx9Op4MmzIes1J8Wu8s0KbdXf2zEwS\nsVqyHgs/XCbnzsR3FqyJTo46D2BVnvZcuU5srNcR2I2HMaqQ0oVdnATE4K6KdcgV\nN+98pMWo56B8LTgE1VpvqbsrXLi9jCTzjrkebVMOP+ZVu+64v1qdgddseblYMnBZ\nct0s7ONbHnqrWlTGf5wES1uIZTVdn5r4MduZG+Uenfi1opBS0lUUxfWdW9r0oF2b\nyneZUyaUCbEroeKbqsweXCWVgnMarUOsgqC42KM4cf95lySSwTSaUtZYIbTw7s9W\n2jveU/rVg8BYZu5JK5obgBoxtlUeUoSswwIDAQAB\n-----END RSA PUBLIC KEY-----\n",
"id": "https://channels.tests.funkwhale.audio/federation/actors/compositions#main-key"
},
"endpoints": {
"sharedInbox": "https://channels.tests.funkwhale.audio/federation/shared/inbox"
}
}

View file

@ -6,12 +6,14 @@ defmodule Pleroma.NotificationTest do
use Pleroma.DataCase use Pleroma.DataCase
import Pleroma.Factory import Pleroma.Factory
import Mock
alias Pleroma.Notification alias Pleroma.Notification
alias Pleroma.Tests.ObanHelpers alias Pleroma.Tests.ObanHelpers
alias Pleroma.User alias Pleroma.User
alias Pleroma.Web.ActivityPub.Transmogrifier alias Pleroma.Web.ActivityPub.Transmogrifier
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.Push
alias Pleroma.Web.Streamer alias Pleroma.Web.Streamer
describe "create_notifications" do describe "create_notifications" do
@ -80,6 +82,80 @@ test "does not create a notification for subscribed users if status is a reply"
end end
end end
describe "CommonApi.post/2 notification-related functionality" do
test_with_mock "creates but does NOT send notification to blocker user",
Push,
[:passthrough],
[] do
user = insert(:user)
blocker = insert(:user)
{:ok, _user_relationship} = User.block(blocker, user)
{:ok, _activity} = CommonAPI.post(user, %{"status" => "hey @#{blocker.nickname}!"})
blocker_id = blocker.id
assert [%Notification{user_id: ^blocker_id}] = Repo.all(Notification)
refute called(Push.send(:_))
end
test_with_mock "creates but does NOT send notification to notification-muter user",
Push,
[:passthrough],
[] do
user = insert(:user)
muter = insert(:user)
{:ok, _user_relationships} = User.mute(muter, user)
{:ok, _activity} = CommonAPI.post(user, %{"status" => "hey @#{muter.nickname}!"})
muter_id = muter.id
assert [%Notification{user_id: ^muter_id}] = Repo.all(Notification)
refute called(Push.send(:_))
end
test_with_mock "creates but does NOT send notification to thread-muter user",
Push,
[:passthrough],
[] do
user = insert(:user)
thread_muter = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{thread_muter.nickname}!"})
{:ok, _} = CommonAPI.add_mute(thread_muter, activity)
{:ok, _same_context_activity} =
CommonAPI.post(user, %{
"status" => "hey-hey-hey @#{thread_muter.nickname}!",
"in_reply_to_status_id" => activity.id
})
[pre_mute_notification, post_mute_notification] =
Repo.all(from(n in Notification, where: n.user_id == ^thread_muter.id, order_by: n.id))
pre_mute_notification_id = pre_mute_notification.id
post_mute_notification_id = post_mute_notification.id
assert called(
Push.send(
:meck.is(fn
%Notification{id: ^pre_mute_notification_id} -> true
_ -> false
end)
)
)
refute called(
Push.send(
:meck.is(fn
%Notification{id: ^post_mute_notification_id} -> true
_ -> false
end)
)
)
end
end
describe "create_notification" do describe "create_notification" do
@tag needs_streamer: true @tag needs_streamer: true
test "it creates a notification for user and send to the 'user' and the 'user:notification' stream" do test "it creates a notification for user and send to the 'user' and the 'user:notification' stream" do
@ -382,7 +458,7 @@ test "Returns recent notifications" do
end end
end end
describe "notification target determination" do describe "notification target determination / get_notified_from_activity/2" do
test "it sends notifications to addressed users in new messages" do test "it sends notifications to addressed users in new messages" do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
@ -392,7 +468,9 @@ test "it sends notifications to addressed users in new messages" do
"status" => "hey @#{other_user.nickname}!" "status" => "hey @#{other_user.nickname}!"
}) })
assert other_user in Notification.get_notified_from_activity(activity) {enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(activity)
assert other_user in enabled_receivers
end end
test "it sends notifications to mentioned users in new messages" do test "it sends notifications to mentioned users in new messages" do
@ -420,7 +498,9 @@ test "it sends notifications to mentioned users in new messages" do
{:ok, activity} = Transmogrifier.handle_incoming(create_activity) {:ok, activity} = Transmogrifier.handle_incoming(create_activity)
assert other_user in Notification.get_notified_from_activity(activity) {enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(activity)
assert other_user in enabled_receivers
end end
test "it does not send notifications to users who are only cc in new messages" do test "it does not send notifications to users who are only cc in new messages" do
@ -442,7 +522,9 @@ test "it does not send notifications to users who are only cc in new messages" d
{:ok, activity} = Transmogrifier.handle_incoming(create_activity) {:ok, activity} = Transmogrifier.handle_incoming(create_activity)
assert other_user not in Notification.get_notified_from_activity(activity) {enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(activity)
assert other_user not in enabled_receivers
end end
test "it does not send notification to mentioned users in likes" do test "it does not send notification to mentioned users in likes" do
@ -457,7 +539,10 @@ test "it does not send notification to mentioned users in likes" do
{:ok, activity_two, _} = CommonAPI.favorite(activity_one.id, third_user) {:ok, activity_two, _} = CommonAPI.favorite(activity_one.id, third_user)
assert other_user not in Notification.get_notified_from_activity(activity_two) {enabled_receivers, _disabled_receivers} =
Notification.get_notified_from_activity(activity_two)
assert other_user not in enabled_receivers
end end
test "it does not send notification to mentioned users in announces" do test "it does not send notification to mentioned users in announces" do
@ -472,7 +557,57 @@ test "it does not send notification to mentioned users in announces" do
{:ok, activity_two, _} = CommonAPI.repeat(activity_one.id, third_user) {:ok, activity_two, _} = CommonAPI.repeat(activity_one.id, third_user)
assert other_user not in Notification.get_notified_from_activity(activity_two) {enabled_receivers, _disabled_receivers} =
Notification.get_notified_from_activity(activity_two)
assert other_user not in enabled_receivers
end
test "it returns blocking recipient in disabled recipients list" do
user = insert(:user)
other_user = insert(:user)
{:ok, _user_relationship} = User.block(other_user, user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}!"})
{enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity)
assert [] == enabled_receivers
assert [other_user] == disabled_receivers
end
test "it returns notification-muting recipient in disabled recipients list" do
user = insert(:user)
other_user = insert(:user)
{:ok, _user_relationships} = User.mute(other_user, user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}!"})
{enabled_receivers, disabled_receivers} = Notification.get_notified_from_activity(activity)
assert [] == enabled_receivers
assert [other_user] == disabled_receivers
end
test "it returns thread-muting recipient in disabled recipients list" do
user = insert(:user)
other_user = insert(:user)
{:ok, activity} = CommonAPI.post(user, %{"status" => "hey @#{other_user.nickname}!"})
{:ok, _} = CommonAPI.add_mute(other_user, activity)
{:ok, same_context_activity} =
CommonAPI.post(user, %{
"status" => "hey-hey-hey @#{other_user.nickname}!",
"in_reply_to_status_id" => activity.id
})
{enabled_receivers, disabled_receivers} =
Notification.get_notified_from_activity(same_context_activity)
assert [other_user] == disabled_receivers
refute other_user in enabled_receivers
end end
end end
@ -736,7 +871,7 @@ test "it doesn't return notifications for blocked user" do
assert Notification.for_user(user) == [] assert Notification.for_user(user) == []
end end
test "it doesn't return notificatitons for blocked domain" do test "it doesn't return notifications for blocked domain" do
user = insert(:user) user = insert(:user)
blocked = insert(:user, ap_id: "http://some-domain.com") blocked = insert(:user, ap_id: "http://some-domain.com")
{:ok, user} = User.block_domain(user, "some-domain.com") {:ok, user} = User.block_domain(user, "some-domain.com")

View file

@ -1283,6 +1283,21 @@ def get("https://patch.cx/users/rin", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/rin.json")}} {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/rin.json")}}
end end
def get(
"https://channels.tests.funkwhale.audio/federation/music/uploads/42342395-0208-4fee-a38d-259a6dae0871",
_,
_,
_
) do
{:ok,
%Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/funkwhale_audio.json")}}
end
def get("https://channels.tests.funkwhale.audio/federation/actors/compositions", _, _, _) do
{:ok,
%Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/funkwhale_channel.json")}}
end
def get("http://example.com/rel_me/error", _, _, _) do def get("http://example.com/rel_me/error", _, _, _) do
{:ok, %Tesla.Env{status: 404, body: ""}} {:ok, %Tesla.Env{status: 404, body: ""}}
end end

View file

@ -86,7 +86,7 @@ test "returns invisible actor" do
{:ok, user: insert(:user)} {:ok, user: insert(:user)}
end end
test "outgoing_relations_ap_ids/1", %{user: user} do test "outgoing_relationships_ap_ids/1", %{user: user} do
rel_types = [:block, :mute, :notification_mute, :reblog_mute, :inverse_subscription] rel_types = [:block, :mute, :notification_mute, :reblog_mute, :inverse_subscription]
ap_ids_by_rel = ap_ids_by_rel =
@ -124,10 +124,10 @@ test "outgoing_relations_ap_ids/1", %{user: user} do
assert ap_ids_by_rel[:inverse_subscription] == assert ap_ids_by_rel[:inverse_subscription] ==
Enum.sort(Enum.map(User.subscriber_users(user), & &1.ap_id)) Enum.sort(Enum.map(User.subscriber_users(user), & &1.ap_id))
outgoing_relations_ap_ids = User.outgoing_relations_ap_ids(user, rel_types) outgoing_relationships_ap_ids = User.outgoing_relationships_ap_ids(user, rel_types)
assert ap_ids_by_rel == assert ap_ids_by_rel ==
Enum.into(outgoing_relations_ap_ids, %{}, fn {k, v} -> {k, Enum.sort(v)} end) Enum.into(outgoing_relationships_ap_ids, %{}, fn {k, v} -> {k, Enum.sort(v)} end)
end end
end end

View file

@ -1425,6 +1425,12 @@ test "it creates a delete activity and deletes the original object" do
assert Repo.get(Object, object.id).data["type"] == "Tombstone" assert Repo.get(Object, object.id).data["type"] == "Tombstone"
end end
test "it doesn't fail when an activity was already deleted" do
{:ok, delete} = insert(:note_activity) |> Object.normalize() |> ActivityPub.delete()
assert {:ok, ^delete} = delete |> Object.normalize() |> ActivityPub.delete()
end
test "decrements user note count only for public activities" do test "decrements user note count only for public activities" do
user = insert(:user, note_count: 10) user = insert(:user, note_count: 10)

View file

@ -3356,6 +3356,75 @@ test "returns log filtered by search", %{conn: conn, moderator: moderator} do
end end
end end
describe "GET /users/:nickname/credentials" do
test "gets the user credentials", %{conn: conn} do
user = insert(:user)
conn = get(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials")
response = assert json_response(conn, 200)
assert response["email"] == user.email
end
test "returns 403 if requested by a non-admin" do
user = insert(:user)
conn =
build_conn()
|> assign(:user, user)
|> get("/api/pleroma/admin/users/#{user.nickname}/credentials")
assert json_response(conn, :forbidden)
end
end
describe "PATCH /users/:nickname/credentials" do
test "changes password and email", %{conn: conn, admin: admin} do
user = insert(:user)
assert user.password_reset_pending == false
conn =
patch(conn, "/api/pleroma/admin/users/#{user.nickname}/credentials", %{
"password" => "new_password",
"email" => "new_email@example.com",
"name" => "new_name"
})
assert json_response(conn, 200) == %{"status" => "success"}
ObanHelpers.perform_all()
updated_user = User.get_by_id(user.id)
assert updated_user.email == "new_email@example.com"
assert updated_user.name == "new_name"
assert updated_user.password_hash != user.password_hash
assert updated_user.password_reset_pending == true
[log_entry2, log_entry1] = ModerationLog |> Repo.all() |> Enum.sort()
assert ModerationLog.get_log_entry_message(log_entry1) ==
"@#{admin.nickname} updated users: @#{user.nickname}"
assert ModerationLog.get_log_entry_message(log_entry2) ==
"@#{admin.nickname} forced password reset for users: @#{user.nickname}"
end
test "returns 403 if requested by a non-admin" do
user = insert(:user)
conn =
build_conn()
|> assign(:user, user)
|> patch("/api/pleroma/admin/users/#{user.nickname}/credentials", %{
"password" => "new_password",
"email" => "new_email@example.com",
"name" => "new_name"
})
assert json_response(conn, :forbidden)
end
end
describe "PATCH /users/:nickname/force_password_reset" do describe "PATCH /users/:nickname/force_password_reset" do
test "sets password_reset_pending to true", %{conn: conn} do test "sets password_reset_pending to true", %{conn: conn} do
user = insert(:user) user = insert(:user)

View file

@ -76,7 +76,7 @@ test "updates the user's bio", %{conn: conn} do
conn = conn =
patch(conn, "/api/v1/accounts/update_credentials", %{ patch(conn, "/api/v1/accounts/update_credentials", %{
"note" => "I drink #cofe with @#{user2.nickname}" "note" => "I drink #cofe with @#{user2.nickname}\n\nsuya.."
}) })
assert user_data = json_response(conn, 200) assert user_data = json_response(conn, 200)
@ -84,7 +84,7 @@ test "updates the user's bio", %{conn: conn} do
assert user_data["note"] == assert user_data["note"] ==
~s(I drink <a class="hashtag" data-tag="cofe" href="http://localhost:4001/tag/cofe">#cofe</a> with <span class="h-card"><a data-user="#{ ~s(I drink <a class="hashtag" data-tag="cofe" href="http://localhost:4001/tag/cofe">#cofe</a> with <span class="h-card"><a data-user="#{
user2.id user2.id
}" class="u-url mention" href="#{user2.ap_id}" rel="ugc">@<span>#{user2.nickname}</span></a></span>) }" class="u-url mention" href="#{user2.ap_id}" rel="ugc">@<span>#{user2.nickname}</span></a></span><br/><br/>suya..)
end end
test "updates the user's locking status", %{conn: conn} do test "updates the user's locking status", %{conn: conn} do
@ -118,6 +118,18 @@ test "updates the user's hide_followers status", %{conn: conn} do
assert user_data["pleroma"]["hide_followers"] == true assert user_data["pleroma"]["hide_followers"] == true
end end
test "updates the user's discoverable status", %{conn: conn} do
assert %{"source" => %{"pleroma" => %{"discoverable" => true}}} =
conn
|> patch("/api/v1/accounts/update_credentials", %{discoverable: "true"})
|> json_response(:ok)
assert %{"source" => %{"pleroma" => %{"discoverable" => false}}} =
conn
|> patch("/api/v1/accounts/update_credentials", %{discoverable: "false"})
|> json_response(:ok)
end
test "updates the user's hide_followers_count and hide_follows_count", %{conn: conn} do test "updates the user's hide_followers_count and hide_follows_count", %{conn: conn} do
conn = conn =
patch(conn, "/api/v1/accounts/update_credentials", %{ patch(conn, "/api/v1/accounts/update_credentials", %{

View file

@ -21,9 +21,12 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
setup do: oauth_access(["read:statuses"]) setup do: oauth_access(["read:statuses"])
test "the home timeline", %{user: user, conn: conn} do test "the home timeline", %{user: user, conn: conn} do
following = insert(:user) following = insert(:user, nickname: "followed")
third_user = insert(:user, nickname: "repeated")
{:ok, _activity} = CommonAPI.post(following, %{"status" => "test"}) {:ok, _activity} = CommonAPI.post(following, %{"status" => "post"})
{:ok, activity} = CommonAPI.post(third_user, %{"status" => "repeated post"})
{:ok, _, _} = CommonAPI.repeat(activity.id, following)
ret_conn = get(conn, "/api/v1/timelines/home") ret_conn = get(conn, "/api/v1/timelines/home")
@ -31,9 +34,54 @@ test "the home timeline", %{user: user, conn: conn} do
{:ok, _user} = User.follow(user, following) {:ok, _user} = User.follow(user, following)
conn = get(conn, "/api/v1/timelines/home") ret_conn = get(conn, "/api/v1/timelines/home")
assert [%{"content" => "test"}] = json_response(conn, :ok) assert [
%{
"reblog" => %{
"content" => "repeated post",
"account" => %{
"pleroma" => %{
"relationship" => %{"following" => false, "followed_by" => false}
}
}
},
"account" => %{"pleroma" => %{"relationship" => %{"following" => true}}}
},
%{
"content" => "post",
"account" => %{
"acct" => "followed",
"pleroma" => %{"relationship" => %{"following" => true}}
}
}
] = json_response(ret_conn, :ok)
{:ok, _user} = User.follow(third_user, user)
ret_conn = get(conn, "/api/v1/timelines/home")
assert [
%{
"reblog" => %{
"content" => "repeated post",
"account" => %{
"acct" => "repeated",
"pleroma" => %{
"relationship" => %{"following" => false, "followed_by" => true}
}
}
},
"account" => %{"pleroma" => %{"relationship" => %{"following" => true}}}
},
%{
"content" => "post",
"account" => %{
"acct" => "followed",
"pleroma" => %{"relationship" => %{"following" => true}}
}
}
] = json_response(ret_conn, :ok)
end end
test "the home timeline when the direct messages are excluded", %{user: user, conn: conn} do test "the home timeline when the direct messages are excluded", %{user: user, conn: conn} do

View file

@ -4,8 +4,11 @@
defmodule Pleroma.Web.MastodonAPI.AccountViewTest do defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
use Pleroma.DataCase use Pleroma.DataCase
import Pleroma.Factory import Pleroma.Factory
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserRelationship
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.AccountView
@ -32,7 +35,8 @@ test "Represent a user account" do
background: background_image, background: background_image,
nickname: "shp@shitposter.club", nickname: "shp@shitposter.club",
name: ":karjalanpiirakka: shp", name: ":karjalanpiirakka: shp",
bio: "<script src=\"invalid-html\"></script><span>valid html</span>", bio:
"<script src=\"invalid-html\"></script><span>valid html</span>. a<br>b<br/>c<br >d<br />f",
inserted_at: ~N[2017-08-15 15:47:06.597036] inserted_at: ~N[2017-08-15 15:47:06.597036]
}) })
@ -46,7 +50,7 @@ test "Represent a user account" do
followers_count: 3, followers_count: 3,
following_count: 0, following_count: 0,
statuses_count: 5, statuses_count: 5,
note: "<span>valid html</span>", note: "<span>valid html</span>. a<br/>b<br/>c<br/>d<br/>f",
url: user.ap_id, url: user.ap_id,
avatar: "http://localhost:4001/images/avi.png", avatar: "http://localhost:4001/images/avi.png",
avatar_static: "http://localhost:4001/images/avi.png", avatar_static: "http://localhost:4001/images/avi.png",
@ -63,7 +67,7 @@ test "Represent a user account" do
fields: [], fields: [],
bot: false, bot: false,
source: %{ source: %{
note: "valid html", note: "valid html. a\nb\nc\nd\nf",
sensitive: false, sensitive: false,
pleroma: %{ pleroma: %{
actor_type: "Person", actor_type: "Person",
@ -181,6 +185,29 @@ test "Represent a smaller mention" do
end end
describe "relationship" do describe "relationship" do
defp test_relationship_rendering(user, other_user, expected_result) do
opts = %{user: user, target: other_user, relationships: nil}
assert expected_result == AccountView.render("relationship.json", opts)
relationships_opt = UserRelationship.view_relationships_option(user, [other_user])
opts = Map.put(opts, :relationships, relationships_opt)
assert expected_result == AccountView.render("relationship.json", opts)
end
@blank_response %{
following: false,
followed_by: false,
blocking: false,
blocked_by: false,
muting: false,
muting_notifications: false,
subscribing: false,
requested: false,
domain_blocking: false,
showing_reblogs: true,
endorsed: false
}
test "represent a relationship for the following and followed user" do test "represent a relationship for the following and followed user" do
user = insert(:user) user = insert(:user)
other_user = insert(:user) other_user = insert(:user)
@ -191,23 +218,21 @@ test "represent a relationship for the following and followed user" do
{:ok, _user_relationships} = User.mute(user, other_user, true) {:ok, _user_relationships} = User.mute(user, other_user, true)
{:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, other_user) {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, other_user)
expected = %{ expected =
id: to_string(other_user.id), Map.merge(
@blank_response,
%{
following: true, following: true,
followed_by: true, followed_by: true,
blocking: false,
blocked_by: false,
muting: true, muting: true,
muting_notifications: true, muting_notifications: true,
subscribing: true, subscribing: true,
requested: false,
domain_blocking: false,
showing_reblogs: false, showing_reblogs: false,
endorsed: false id: to_string(other_user.id)
} }
)
assert expected == test_relationship_rendering(user, other_user, expected)
AccountView.render("relationship.json", %{user: user, target: other_user})
end end
test "represent a relationship for the blocking and blocked user" do test "represent a relationship for the blocking and blocked user" do
@ -219,23 +244,13 @@ test "represent a relationship for the blocking and blocked user" do
{:ok, _user_relationship} = User.block(user, other_user) {:ok, _user_relationship} = User.block(user, other_user)
{:ok, _user_relationship} = User.block(other_user, user) {:ok, _user_relationship} = User.block(other_user, user)
expected = %{ expected =
id: to_string(other_user.id), Map.merge(
following: false, @blank_response,
followed_by: false, %{following: false, blocking: true, blocked_by: true, id: to_string(other_user.id)}
blocking: true, )
blocked_by: true,
muting: false,
muting_notifications: false,
subscribing: false,
requested: false,
domain_blocking: false,
showing_reblogs: true,
endorsed: false
}
assert expected == test_relationship_rendering(user, other_user, expected)
AccountView.render("relationship.json", %{user: user, target: other_user})
end end
test "represent a relationship for the user blocking a domain" do test "represent a relationship for the user blocking a domain" do
@ -244,8 +259,13 @@ test "represent a relationship for the user blocking a domain" do
{:ok, user} = User.block_domain(user, "bad.site") {:ok, user} = User.block_domain(user, "bad.site")
assert %{domain_blocking: true, blocking: false} = expected =
AccountView.render("relationship.json", %{user: user, target: other_user}) Map.merge(
@blank_response,
%{domain_blocking: true, blocking: false, id: to_string(other_user.id)}
)
test_relationship_rendering(user, other_user, expected)
end end
test "represent a relationship for the user with a pending follow request" do test "represent a relationship for the user with a pending follow request" do
@ -256,23 +276,13 @@ test "represent a relationship for the user with a pending follow request" do
user = User.get_cached_by_id(user.id) user = User.get_cached_by_id(user.id)
other_user = User.get_cached_by_id(other_user.id) other_user = User.get_cached_by_id(other_user.id)
expected = %{ expected =
id: to_string(other_user.id), Map.merge(
following: false, @blank_response,
followed_by: false, %{requested: true, following: false, id: to_string(other_user.id)}
blocking: false, )
blocked_by: false,
muting: false,
muting_notifications: false,
subscribing: false,
requested: true,
domain_blocking: false,
showing_reblogs: true,
endorsed: false
}
assert expected == test_relationship_rendering(user, other_user, expected)
AccountView.render("relationship.json", %{user: user, target: other_user})
end end
end end

View file

@ -16,6 +16,21 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
import Pleroma.Factory import Pleroma.Factory
defp test_notifications_rendering(notifications, user, expected_result) do
result = NotificationView.render("index.json", %{notifications: notifications, for: user})
assert expected_result == result
result =
NotificationView.render("index.json", %{
notifications: notifications,
for: user,
relationships: nil
})
assert expected_result == result
end
test "Mention notification" do test "Mention notification" do
user = insert(:user) user = insert(:user)
mentioned_user = insert(:user) mentioned_user = insert(:user)
@ -32,10 +47,7 @@ test "Mention notification" do
created_at: Utils.to_masto_date(notification.inserted_at) created_at: Utils.to_masto_date(notification.inserted_at)
} }
result = test_notifications_rendering([notification], mentioned_user, [expected])
NotificationView.render("index.json", %{notifications: [notification], for: mentioned_user})
assert [expected] == result
end end
test "Favourite notification" do test "Favourite notification" do
@ -55,9 +67,7 @@ test "Favourite notification" do
created_at: Utils.to_masto_date(notification.inserted_at) created_at: Utils.to_masto_date(notification.inserted_at)
} }
result = NotificationView.render("index.json", %{notifications: [notification], for: user}) test_notifications_rendering([notification], user, [expected])
assert [expected] == result
end end
test "Reblog notification" do test "Reblog notification" do
@ -77,9 +87,7 @@ test "Reblog notification" do
created_at: Utils.to_masto_date(notification.inserted_at) created_at: Utils.to_masto_date(notification.inserted_at)
} }
result = NotificationView.render("index.json", %{notifications: [notification], for: user}) test_notifications_rendering([notification], user, [expected])
assert [expected] == result
end end
test "Follow notification" do test "Follow notification" do
@ -96,16 +104,12 @@ test "Follow notification" do
created_at: Utils.to_masto_date(notification.inserted_at) created_at: Utils.to_masto_date(notification.inserted_at)
} }
result = test_notifications_rendering([notification], followed, [expected])
NotificationView.render("index.json", %{notifications: [notification], for: followed})
assert [expected] == result
User.perform(:delete, follower) User.perform(:delete, follower)
notification = Notification |> Repo.one() |> Repo.preload(:activity) notification = Notification |> Repo.one() |> Repo.preload(:activity)
assert [] == test_notifications_rendering([notification], followed, [])
NotificationView.render("index.json", %{notifications: [notification], for: followed})
end end
@tag capture_log: true @tag capture_log: true
@ -144,8 +148,7 @@ test "Move notification" do
created_at: Utils.to_masto_date(notification.inserted_at) created_at: Utils.to_masto_date(notification.inserted_at)
} }
assert [expected] == test_notifications_rendering([notification], follower, [expected])
NotificationView.render("index.json", %{notifications: [notification], for: follower})
end end
test "EmojiReact notification" do test "EmojiReact notification" do
@ -171,7 +174,6 @@ test "EmojiReact notification" do
created_at: Utils.to_masto_date(notification.inserted_at) created_at: Utils.to_masto_date(notification.inserted_at)
} }
assert expected == test_notifications_rendering([notification], user, [expected])
NotificationView.render("show.json", %{notification: notification, for: user})
end end
end end

View file

@ -12,10 +12,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
alias Pleroma.Object alias Pleroma.Object
alias Pleroma.Repo alias Pleroma.Repo
alias Pleroma.User alias Pleroma.User
alias Pleroma.UserRelationship
alias Pleroma.Web.CommonAPI alias Pleroma.Web.CommonAPI
alias Pleroma.Web.CommonAPI.Utils alias Pleroma.Web.CommonAPI.Utils
alias Pleroma.Web.MastodonAPI.AccountView alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.StatusView alias Pleroma.Web.MastodonAPI.StatusView
import Pleroma.Factory import Pleroma.Factory
import Tesla.Mock import Tesla.Mock
@ -229,12 +231,21 @@ test "tells if the message is muted for some reason" do
{:ok, _user_relationships} = User.mute(user, other_user) {:ok, _user_relationships} = User.mute(user, other_user)
{:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"}) {:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"})
status = StatusView.render("show.json", %{activity: activity})
relationships_opt = UserRelationship.view_relationships_option(user, [other_user])
opts = %{activity: activity}
status = StatusView.render("show.json", opts)
assert status.muted == false assert status.muted == false
status = StatusView.render("show.json", %{activity: activity, for: user}) status = StatusView.render("show.json", Map.put(opts, :relationships, relationships_opt))
assert status.muted == false
for_opts = %{activity: activity, for: user}
status = StatusView.render("show.json", for_opts)
assert status.muted == true
status = StatusView.render("show.json", Map.put(for_opts, :relationships, relationships_opt))
assert status.muted == true assert status.muted == true
end end
@ -437,6 +448,22 @@ test "a peertube video" do
assert length(represented[:media_attachments]) == 1 assert length(represented[:media_attachments]) == 1
end end
test "funkwhale audio" do
user = insert(:user)
{:ok, object} =
Pleroma.Object.Fetcher.fetch_object_from_id(
"https://channels.tests.funkwhale.audio/federation/music/uploads/42342395-0208-4fee-a38d-259a6dae0871"
)
%Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"])
represented = StatusView.render("show.json", %{for: user, activity: activity})
assert represented[:id] == to_string(activity.id)
assert length(represented[:media_attachments]) == 1
end
test "a Mobilizon event" do test "a Mobilizon event" do
user = insert(:user) user = insert(:user)

View file

@ -575,7 +575,7 @@ test "redirects with oauth authorization, " <>
# In case scope param is missing, expecting _all_ app-supported scopes to be granted # In case scope param is missing, expecting _all_ app-supported scopes to be granted
for user <- [non_admin, admin], for user <- [non_admin, admin],
{requested_scopes, expected_scopes} <- {requested_scopes, expected_scopes} <-
%{scopes_subset => scopes_subset, nil => app_scopes} do %{scopes_subset => scopes_subset, nil: app_scopes} do
conn = conn =
post( post(
build_conn(), build_conn(),