Merge branch 'develop' into gun
authorAlexander Strizhakov <alex.strizhakov@gmail.com>
Mon, 30 Mar 2020 09:15:23 +0000 (12:15 +0300)
committerAlexander Strizhakov <alex.strizhakov@gmail.com>
Mon, 30 Mar 2020 09:15:23 +0000 (12:15 +0300)
43 files changed:
CHANGELOG.md
benchmarks/load_testing/generator.ex
benchmarks/mix/tasks/pleroma/benchmarks/timelines.ex [new file with mode: 0644]
config/description.exs
docs/API/admin_api.md
lib/pleroma/activity.ex
lib/pleroma/activity/queries.ex
lib/pleroma/conversation/participation.ex
lib/pleroma/following_relationship.ex
lib/pleroma/moderation_log.ex
lib/pleroma/notification.ex
lib/pleroma/thread_mute.ex
lib/pleroma/user.ex
lib/pleroma/user_relationship.ex
lib/pleroma/web/activity_pub/activity_pub.ex
lib/pleroma/web/activity_pub/transmogrifier.ex
lib/pleroma/web/activity_pub/utils.ex
lib/pleroma/web/admin_api/admin_api_controller.ex
lib/pleroma/web/admin_api/views/account_view.ex
lib/pleroma/web/common_api/common_api.ex
lib/pleroma/web/controller_helper.ex
lib/pleroma/web/mastodon_api/controllers/account_controller.ex
lib/pleroma/web/mastodon_api/views/account_view.ex
lib/pleroma/web/mastodon_api/views/notification_view.ex
lib/pleroma/web/mastodon_api/views/status_view.ex
lib/pleroma/web/router.ex
lib/pleroma/web/static_fe/static_fe_controller.ex
lib/pleroma/web/streamer/worker.ex
mix.exs
priv/static/static/static-fe.css
test/fixtures/tesla_mock/funkwhale_audio.json [new file with mode: 0644]
test/fixtures/tesla_mock/funkwhale_channel.json [new file with mode: 0644]
test/notification_test.exs
test/support/http_request_mock.ex
test/user_test.exs
test/web/activity_pub/activity_pub_test.exs
test/web/admin_api/admin_api_controller_test.exs
test/web/mastodon_api/controllers/account_controller/update_credentials_test.exs
test/web/mastodon_api/controllers/timeline_controller_test.exs
test/web/mastodon_api/views/account_view_test.exs
test/web/mastodon_api/views/notification_view_test.exs
test/web/mastodon_api/views/status_view_test.exs
test/web/oauth/oauth_controller_test.exs

index 841dd8afaea7580d2d93d5bb082541703015d0b2..f393ea8ebeab57fd9cd8d5913a3e684ade347df6 100644 (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: 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.
+- Admin API: `PATCH /api/pleroma/admin/users/:nickname/credentials` and `GET /api/pleroma/admin/users/:nickname/credentials`
 </details>
 
 ### Added
index 3f88fefd7001b6e98030b14486637b916bbb8ebf..e4673757cdfa728f7d4289b75a8edde2419edd1c 100644 (file)
@@ -22,9 +22,10 @@ defmodule Pleroma.LoadTesting.Generator do
 
   def generate_users(opts) do
     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
 
   defp do_generate_users(opts) do
diff --git a/benchmarks/mix/tasks/pleroma/benchmarks/timelines.ex b/benchmarks/mix/tasks/pleroma/benchmarks/timelines.ex
new file mode 100644 (file)
index 0000000..dc6f3d3
--- /dev/null
@@ -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
index 056f5971d121c1a4ff6db10d05cbf61098c2985a..642f1a3ce9d2be466a15fe24495dfb189bf8ed0b 100644 (file)
@@ -2442,7 +2442,7 @@ config :pleroma, :config_description, [
       %{
         key: :relations_actions,
         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}]]
       },
       %{
index be6617e49a8667fb29245c7fc90aea3ab91199fc..58d7023472d0202aa023b7e40790423346d48761 100644 (file)
@@ -414,6 +414,83 @@ Note: Available `:permission_group` is currently moderator and admin. 404 is ret
   - `nicknames`
 - 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 a list of reports
index 6ca05f74e061ce4739b52fe592db979cc92408b5..5a8329e69368de5c03e1ed4fc7ad22de7f7e6ad2 100644 (file)
@@ -95,6 +95,17 @@ defmodule Pleroma.Activity do
     |> preload([activity, object: object], object: object)
   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
     join(query, join_type, [activity], u in User,
       on: u.ap_id == activity.actor,
index 04593b9fb70f32dcbaf16d291c8a2e56b7827023..a34c20343471582e7c0dbb8e0bebd647d5cd83e0 100644 (file)
@@ -35,6 +35,13 @@ defmodule Pleroma.Activity.Queries do
     from(a in query, where: a.actor == ^ap_id)
   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
   def by_object_id(query \\ Activity, object_id)
 
index 693825cf5fbef554bffda87bc7bb20757afb7e12..215265fc90a201c8c24539e9c0cc632bbea62699 100644 (file)
@@ -129,21 +129,18 @@ defmodule Pleroma.Conversation.Participation do
   end
 
   def restrict_recipients(query, user, %{"recipients" => user_ids}) do
-    user_ids =
+    user_binary_ids =
       [user.id | user_ids]
       |> Enum.uniq()
-      |> Enum.reduce([], fn user_id, acc ->
-        {:ok, user_id} = FlakeId.Ecto.CompatType.dump(user_id)
-        [user_id | acc]
-      end)
+      |> User.binary_id()
 
     conversation_subquery =
       __MODULE__
       |> group_by([p], p.conversation_id)
       |> having(
         [p],
-        count(p.user_id) == ^length(user_ids) and
-          fragment("array_agg(?) @> ?", p.user_id, ^user_ids)
+        count(p.user_id) == ^length(user_binary_ids) and
+          fragment("array_agg(?) @> ?", p.user_id, ^user_binary_ids)
       )
       |> select([p], %{id: p.conversation_id})
 
index a6d28115137b32cb5ec30026f87461c20971930b..a9538ea4e4d613b8020b835781268890ca229b5a 100644 (file)
@@ -129,4 +129,32 @@ defmodule Pleroma.FollowingRelationship do
         move_following(origin, target)
     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
index e32895f70d5d269bcae8f0ed73b8b650fa358651..7aacd9d80984edab08d4f38d408af1d5904e8846 100644 (file)
@@ -605,6 +605,17 @@ defmodule Pleroma.ModerationLog do
     }"
   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
     nicknames
     |> Enum.map(&"@#{&1}")
index 3ef3b3f58bf94f9752d806038b63e0c058896c20..04ee510b9f13fa7d346064988dacaea2d95babc1 100644 (file)
@@ -10,6 +10,7 @@ defmodule Pleroma.Notification do
   alias Pleroma.Object
   alias Pleroma.Pagination
   alias Pleroma.Repo
+  alias Pleroma.ThreadMute
   alias Pleroma.User
   alias Pleroma.Web.CommonAPI.Utils
   alias Pleroma.Web.Push
@@ -17,6 +18,7 @@ defmodule Pleroma.Notification do
 
   import Ecto.Query
   import Ecto.Changeset
+
   require Logger
 
   @type t :: %__MODULE__{}
@@ -37,11 +39,11 @@ defmodule Pleroma.Notification do
   end
 
   defp for_user_query_ap_id_opts(user, opts) do
-    ap_id_relations =
+    ap_id_relationships =
       [:block] ++
         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)
 
@@ -100,7 +102,7 @@ defmodule Pleroma.Notification do
 
     query
     |> 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)
     )
     |> where([n, a, o, tm], is_nil(tm.user_id))
@@ -275,58 +277,111 @@ defmodule Pleroma.Notification do
   def create_notifications(%Activity{data: %{"to" => _, "type" => "Create"}} = activity) do
     object = Object.normalize(activity)
 
-    unless 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
+    if object && object.data["type"] == "Answer" do
       {:ok, []}
+    else
+      do_create_notifications(activity)
     end
   end
 
   def create_notifications(%Activity{data: %{"type" => type}} = activity)
       when type in ["Like", "Announce", "Follow", "Move", "EmojiReact"] do
+    do_create_notifications(activity)
+  end
+
+  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 =
-      activity
-      |> get_notified_from_activity()
-      |> Enum.map(&create_notification(activity, &1))
+      Enum.map(potential_receivers, fn user ->
+        do_send = user in enabled_receivers
+        create_notification(activity, user, do_send)
+      end)
 
     {:ok, notifications}
   end
 
-  def create_notifications(_), do: {:ok, []}
-
   # 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
       notification = %Notification{user_id: user.id, activity: activity}
       {:ok, notification} = Repo.insert(notification)
 
-      ["user", "user:notification"]
-      |> Streamer.stream(notification)
+      if do_send do
+        Streamer.stream(["user", "user:notification"], notification)
+        Push.send(notification)
+      end
 
-      Push.send(notification)
       notification
     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{data: %{"type" => type}} = activity, local_only)
       when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReact"] do
-    []
-    |> Utils.maybe_notify_to_recipients(activity)
-    |> Utils.maybe_notify_mentioned_recipients(activity)
-    |> Utils.maybe_notify_subscribers(activity)
-    |> Utils.maybe_notify_followers(activity)
-    |> Enum.uniq()
-    |> User.get_users_from_set(local_only)
+    potential_receiver_ap_ids =
+      []
+      |> Utils.maybe_notify_to_recipients(activity)
+      |> Utils.maybe_notify_mentioned_recipients(activity)
+      |> Utils.maybe_notify_subscribers(activity)
+      |> Utils.maybe_notify_followers(activity)
+      |> 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)
+
+    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
+
+  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
 
-  def get_notified_from_activity(_, _local_only), do: []
+  @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()
-  def skip?(activity, user) do
+  def skip?(%Activity{} = activity, %User{} = user) do
     [
       :self,
       :followers,
@@ -335,18 +390,20 @@ defmodule Pleroma.Notification do
       :non_follows,
       :recently_followed
     ]
-    |> Enum.any?(&skip?(&1, activity, user))
+    |> Enum.find(&skip?(&1, activity, user))
   end
 
+  def skip?(_, _), do: false
+
   @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
   end
 
   def skip?(
         :followers,
-        activity,
-        %{notification_settings: %{followers: false}} = user
+        %Activity{} = activity,
+        %User{notification_settings: %{followers: false}} = user
       ) do
     actor = activity.data["actor"]
     follower = User.get_cached_by_ap_id(actor)
@@ -355,15 +412,19 @@ defmodule Pleroma.Notification do
 
   def skip?(
         :non_followers,
-        activity,
-        %{notification_settings: %{non_followers: false}} = user
+        %Activity{} = activity,
+        %User{notification_settings: %{non_followers: false}} = user
       ) do
     actor = activity.data["actor"]
     follower = User.get_cached_by_ap_id(actor)
     !User.following?(follower, user)
   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"]
     followed = User.get_cached_by_ap_id(actor)
     User.following?(user, followed)
@@ -371,15 +432,16 @@ defmodule Pleroma.Notification do
 
   def skip?(
         :non_follows,
-        activity,
-        %{notification_settings: %{non_follows: false}} = user
+        %Activity{} = activity,
+        %User{notification_settings: %{non_follows: false}} = user
       ) do
     actor = activity.data["actor"]
     followed = User.get_cached_by_ap_id(actor)
     !User.following?(user, followed)
   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"]
 
     Notification.for_user(user)
index cc815430a39f576530a3f23d9b6f0ff5cd1accd5..be01d541dab5ef977e1950fd08f63b33834c631e 100644 (file)
@@ -9,7 +9,8 @@ defmodule Pleroma.ThreadMute do
   alias Pleroma.ThreadMute
   alias Pleroma.User
 
-  require Ecto.Query
+  import Ecto.Changeset
+  import Ecto.Query
 
   schema "thread_mutes" do
     belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
@@ -18,19 +19,44 @@ defmodule Pleroma.ThreadMute do
 
   def changeset(mute, params \\ %{}) do
     mute
-    |> Ecto.Changeset.cast(params, [:user_id, :context])
-    |> Ecto.Changeset.foreign_key_constraint(:user_id)
-    |> Ecto.Changeset.unique_constraint(:user_id, name: :unique_index)
+    |> cast(params, [:user_id, :context])
+    |> foreign_key_constraint(:user_id)
+    |> unique_constraint(:user_id, name: :unique_index)
   end
 
   def query(user_id, context) do
-    {:ok, user_id} = FlakeId.Ecto.CompatType.dump(user_id)
+    user_binary_id = User.binary_id(user_id)
 
     ThreadMute
-    |> Ecto.Query.where(user_id: ^user_id)
-    |> Ecto.Query.where(context: ^context)
+    |> where(user_id: ^user_binary_id)
+    |> where(context: ^context)
   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
     %ThreadMute{}
     |> changeset(%{user_id: user_id, context: context})
@@ -42,8 +68,8 @@ defmodule Pleroma.ThreadMute do
     |> Repo.delete_all()
   end
 
-  def check_muted(user_id, context) do
+  def exists?(user_id, context) do
     query(user_id, context)
-    |> Repo.all()
+    |> Repo.exists?()
   end
 end
index 8693c0b8090c28c633a49d4110efdfba71c8d157..d9aa5405756f7af1dd808a1d4f3610edd472177c 100644 (file)
@@ -150,22 +150,26 @@ defmodule Pleroma.User do
            {outgoing_relation, outgoing_relation_target},
            {incoming_relation, incoming_relation_source}
          ]} <- @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,
         foreign_key: :source_id,
         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,
         foreign_key: :target_id,
         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])
 
-      # 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])
     end
 
@@ -185,7 +189,9 @@ defmodule Pleroma.User do
 
   for {_relationship_type, [{_outgoing_relation, outgoing_relation_target}, _]} <-
         @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
       target_users_query = assoc(user, unquote(outgoing_relation_target))
 
@@ -196,7 +202,8 @@ defmodule Pleroma.User do
       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
       __MODULE__
       |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
@@ -206,7 +213,8 @@ defmodule Pleroma.User do
       |> Repo.all()
     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
       __MODULE__
       |> apply(unquote(:"#{outgoing_relation_target}_relation"), [
@@ -218,6 +226,24 @@ defmodule Pleroma.User do
     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"
   @spec account_status(User.t()) :: account_status()
   def account_status(%User{deactivated: true}), do: :deactivated
@@ -292,24 +318,6 @@ defmodule Pleroma.User do
   def ap_following(%User{following_address: fa}) when is_binary(fa), do: fa
   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()
   def restrict_deactivated(query) do
     from(u in query, where: u.deactivated != ^true)
@@ -428,9 +436,55 @@ defmodule Pleroma.User do
     |> validate_format(:nickname, local_nickname_regex())
     |> validate_length(:bio, max: bio_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)
   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
     bio_limit = Pleroma.Config.get([:instance, :user_bio_length], 5000)
     name_limit = Pleroma.Config.get([:instance, :user_name_length], 100)
@@ -474,6 +528,27 @@ defmodule Pleroma.User do
     |> validate_fields(remote?)
   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
     struct
     |> cast(params, [:password, :password_confirmation])
@@ -484,10 +559,14 @@ defmodule Pleroma.User do
   end
 
   @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.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(:auth, OAuth.Authorization.delete_by_user_query(user))
 
@@ -692,7 +771,14 @@ defmodule Pleroma.User do
 
   def get_follow_state(%User{} = follower, %User{} = following) do
     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
       {nil, false} ->
         case Utils.fetch_latest_follow(follower, following) do
@@ -1225,13 +1311,15 @@ defmodule Pleroma.User do
   end
 
   @doc """
-  Returns map of outgoing (blocked, muted etc.) relations' user AP IDs by relation type.
-  E.g. `outgoing_relations_ap_ids(user, [:block])` -> `%{block: ["https://some.site/users/userapid"]}`
+  Returns map of outgoing (blocked, muted etc.) relationships' user AP IDs by relation type.
+  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())}
-  def outgoing_relations_ap_ids(_, []), do: %{}
+  @spec outgoing_relationships_ap_ids(User.t(), list(atom())) :: %{atom() => list(String.t())}
+  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
     db_result =
       user
@@ -1250,6 +1338,30 @@ defmodule Pleroma.User do
     )
   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
     BackgroundWorker.enqueue("deactivate_user", %{"user_id" => user.id, "status" => status})
   end
@@ -1660,8 +1772,12 @@ defmodule Pleroma.User do
     |> Repo.all()
   end
 
+  def muting_reblogs?(%User{} = user, %User{} = target) do
+    UserRelationship.reblog_mute_exists?(user, target)
+  end
+
   def showing_reblogs?(%User{} = user, %User{} = target) do
-    not UserRelationship.reblog_mute_exists?(user, target)
+    not muting_reblogs?(user, target)
   end
 
   @doc """
@@ -1867,6 +1983,17 @@ defmodule Pleroma.User do
 
   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
     limit_name = if remote?, do: :max_remote_account_fields, else: :max_account_fields
     limit = Pleroma.Config.get([:instance, limit_name], 0)
index 393947942304d0a4356e9599a9fc91bad1552b2a..18a5eec7262bc1816753dea928b64025ebe08deb 100644 (file)
@@ -8,6 +8,7 @@ defmodule Pleroma.UserRelationship do
   import Ecto.Changeset
   import Ecto.Query
 
+  alias Pleroma.FollowingRelationship
   alias Pleroma.Repo
   alias Pleroma.User
   alias Pleroma.UserRelationship
@@ -21,19 +22,26 @@ defmodule Pleroma.UserRelationship do
   end
 
   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),
       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),
       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),
       do: exists?(unquote(relationship_type), source, target)
   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
     user_relationship
     |> cast(params, [:relationship_type, :source_id, :target_id])
@@ -72,6 +80,73 @@ defmodule Pleroma.UserRelationship do
     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
     changeset
     |> validate_change(:target_id, fn _, target_id ->
index d9f74b6a4928004f074025369bd26183aa517253..9c0f5d77111670a33d2d2322175a15a336892a72 100644 (file)
@@ -503,8 +503,7 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
   defp do_follow(follower, followed, activity_id, local) do
     with data <- make_follow_data(follower, followed, activity_id),
          {:ok, activity} <- insert(data, local),
-         :ok <- maybe_federate(activity),
-         _ <- User.set_follow_state_cache(follower.ap_id, followed.ap_id, activity.data["state"]) do
+         :ok <- maybe_federate(activity) do
       {:ok, activity}
     else
       {:error, error} -> Repo.rollback(error)
@@ -584,6 +583,16 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
     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()) ::
           {:ok, Activity.t()} | {:error, any()}
   def block(blocker, blocked, activity_id \\ nil, local \\ true) do
@@ -1230,17 +1239,17 @@ defmodule Pleroma.Web.ActivityPub.ActivityPub do
 
   defp fetch_activities_query_ap_ids_ops(opts) do
     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_relations ++
+    ap_id_relationships =
+      ap_id_relationships ++
         if opts["blocking_user"] && opts["blocking_user"] == source_user do
           [:block]
         else
           []
         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_muted_opts = Map.merge(%{"muted_users_ap_ids" => preloaded_ap_ids[:mute]}, opts)
index 9cd3de7053ee9adc49459a8ee4ec073cd6c6f736..09bd9a44290679477778ffa6548e290632daa43a 100644 (file)
@@ -229,7 +229,8 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
     Map.put(object, "url", url["href"])
   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)
 
     link_element = Enum.find(url, fn x -> is_map(x) and x["mimeType"] == "text/html" end)
@@ -398,7 +399,7 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
         %{"type" => "Create", "object" => %{"type" => objtype} = object} = data,
         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)
 
     data =
@@ -1108,13 +1109,11 @@ defmodule Pleroma.Web.ActivityPub.Transmogrifier do
   end
 
   def add_mention_tags(object) do
-    mentions =
-      object
-      |> Utils.get_notified_from_object()
-      |> Enum.map(&build_mention_tag/1)
+    {enabled_receivers, disabled_receivers} = Utils.get_notified_from_object(object)
+    potential_receivers = enabled_receivers ++ disabled_receivers
+    mentions = Enum.map(potential_receivers, &build_mention_tag/1)
 
     tags = object["tag"] || []
-
     Map.put(object, "tag", tags ++ mentions)
   end
 
index 15dd2ed45f5f6ffaada313e32ad51559a5580302..c65bbed672dcca53da881d44ebb933129d6ff6d3 100644 (file)
@@ -440,22 +440,19 @@ defmodule Pleroma.Web.ActivityPub.Utils do
     |> update(set: [data: fragment("jsonb_set(data, '{state}', ?)", ^state)])
     |> Repo.update_all([])
 
-    User.set_follow_state_cache(actor, object, state)
-
     activity = Activity.get_by_id(activity.id)
 
     {:ok, activity}
   end
 
   def update_follow_state(
-        %Activity{data: %{"actor" => actor, "object" => object}} = activity,
+        %Activity{} = activity,
         state
       ) do
     new_data = Map.put(activity.data, "state", state)
     changeset = Changeset.change(activity, data: new_data)
 
     with {:ok, activity} <- Repo.update(changeset) do
-      User.set_follow_state_cache(actor, object, state)
       {:ok, activity}
     end
   end
index 175260bc2bcb8069aaa115722de136c975caef4d..0368df1e94b93ad5703da8913f352831f4dc0bd2 100644 (file)
@@ -38,7 +38,7 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
   plug(
     OAuthScopesPlug,
     %{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(
@@ -54,7 +54,8 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
            :tag_users,
            :untag_users,
            :right_add,
-           :right_delete
+           :right_delete,
+           :update_user_credentials
          ]
   )
 
@@ -658,6 +659,52 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIController do
     json_response(conn, :no_content, "")
   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
     {page, page_size} = page_params(params)
 
index 1e03849de035f950d73ee99bf91011b1201ba892..a16a3ebf042bd138825d4b46718847d769d8a6b0 100644 (file)
@@ -23,6 +23,43 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
     }
   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
     avatar = User.avatar_url(user) |> MediaProxy.url()
     display_name = Pleroma.HTML.strip_tags(user.name || user.nickname)
@@ -104,4 +141,7 @@ defmodule Pleroma.Web.AdminAPI.AccountView do
         ""
     end
   end
+
+  defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
+  defp image_url(_), do: nil
 end
index 091011c6b1082abf268221c2b80c2a4169e66cf8..2646b9f7b8887c31cc685e54c4c3a68be15da0c6 100644 (file)
@@ -358,7 +358,7 @@ defmodule Pleroma.Web.CommonAPI do
   def thread_muted?(%{id: nil} = _user, _activity), do: false
 
   def thread_muted?(user, activity) do
-    ThreadMute.check_muted(user.id, activity.data["context"]) != []
+    ThreadMute.exists?(user.id, activity.data["context"])
   end
 
   def report(user, %{"account_id" => account_id} = data) do
index ad293cda937e4c2f379013a55dc0f883bb992f5e..b49523ec38513455a20f88c5daea3ab2be812f94 100644 (file)
@@ -34,7 +34,12 @@ defmodule Pleroma.Web.ControllerHelper do
 
   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
       %{id: max_id} ->
         params =
index 6dbf11ac92ff080f470d7df30277ff52f0a945fc..21bc3d5a549d5fde33a633c00f1147df3968deb4 100644 (file)
@@ -8,7 +8,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
   import Pleroma.Web.ControllerHelper,
     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.RateLimiter
   alias Pleroma.User
@@ -63,11 +62,15 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
     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
 
-  plug(RateLimiter, [name: :relations_id_action, params: ["id", "uri"]] when action in @relations)
-  plug(RateLimiter, [name: :relations_actions] when action in @relations)
+  plug(
+    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(:assign_account_by_id when action in @needs_account)
 
@@ -140,17 +143,6 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
   def update_credentials(%{assigns: %{user: original_user}} = conn, params) do
     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 =
       [
         :no_rich_text,
@@ -169,46 +161,20 @@ defmodule Pleroma.Web.MastodonAPI.AccountController do
         add_if_present(acc, params, to_string(key), key, &{:ok, truthy_param?(&1)})
       end)
       |> 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, "avatar", :avatar, fn value ->
-        with %Plug.Upload{} <- value,
-             {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
-          {:ok, object.data}
-        end
-      end)
-      |> add_if_present(params, "header", :banner, fn value ->
-        with %Plug.Upload{} <- value,
-             {:ok, object} <- ActivityPub.upload(value, type: :banner) do
-          {:ok, object.data}
-        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, "note", :bio)
+      |> add_if_present(params, "avatar", :avatar)
+      |> add_if_present(params, "header", :banner)
+      |> add_if_present(params, "pleroma_background_image", :background)
+      |> add_if_present(
+        params,
+        "fields_attributes",
+        :raw_fields,
+        &{:ok, normalize_fields_attributes(&1)}
+      )
+      |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store)
       |> add_if_present(params, "default_scope", :default_scope)
       |> 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)
 
     with {:ok, user} <- User.update_and_set_cache(changeset) do
index 341dc2c91e2f059a7ced3c4c50d4aca387796138..0efcabc019b420f56c7b3a0e7f7c9f844b1ea07f 100644 (file)
@@ -5,12 +5,28 @@
 defmodule Pleroma.Web.MastodonAPI.AccountView do
   use Pleroma.Web, :view
 
+  alias Pleroma.FollowingRelationship
   alias Pleroma.User
+  alias Pleroma.UserRelationship
   alias Pleroma.Web.CommonAPI.Utils
   alias Pleroma.Web.MastodonAPI.AccountView
   alias Pleroma.Web.MediaProxy
 
   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
     |> render_many(AccountView, "show.json", opts)
     |> Enum.filter(&Enum.any?/1)
@@ -35,34 +51,107 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
     %{}
   end
 
-  def render("relationship.json", %{user: %User{} = user, target: %User{} = target}) do
-    follow_state = User.get_cached_follow_state(user, target)
+  def render(
+        "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])
+
+    follow_state =
+      if following_relationships do
+        user_to_target_following_relation =
+          FollowingRelationship.find(following_relationships, reading_user, target)
 
-    requested =
-      if follow_state && !User.following?(user, target) do
-        follow_state == "pending"
+        User.get_follow_state(reading_user, target, user_to_target_following_relation)
       else
-        false
+        User.get_follow_state(reading_user, target)
       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),
-      following: User.following?(user, target),
-      followed_by: User.following?(target, user),
-      blocking: User.blocks_user?(user, target),
-      blocked_by: User.blocks_user?(target, user),
-      muting: User.mutes?(user, target),
-      muting_notifications: User.muted_notifications?(user, target),
-      subscribing: User.subscribed_to?(user, target),
-      requested: requested,
-      domain_blocking: User.blocks_domain?(user, target),
-      showing_reblogs: User.showing_reblogs?(user, target),
+      following: follow_state == "accept",
+      followed_by: followed_by,
+      blocking:
+        UserRelationship.exists?(
+          user_relationships,
+          :block,
+          reading_user,
+          target,
+          &User.blocks_user?(&1, &2)
+        ),
+      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
     }
   end
 
-  def render("relationships.json", %{user: user, targets: targets}) do
-    render_many(targets, AccountView, "relationship.json", user: user, as: :target)
+  def render("relationships.json", %{user: user, targets: targets} = 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(user, targets)
+      end
+
+    render_opts = %{as: :target, user: user, relationships: relationships_opt}
+    render_many(targets, AccountView, "relationship.json", render_opts)
   end
 
   defp do_render("show.json", %{user: user} = opts) do
@@ -100,7 +189,12 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
         }
       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),
@@ -122,7 +216,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountView do
       fields: user.fields,
       bot: bot,
       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,
         fields: user.raw_fields,
         pleroma: %{
index 33145c484d6f74fe03e95a88292ebd9cb58d2bc1..89f5734ffaee84d618dd260fc18f5b5798b3ba12 100644 (file)
@@ -8,24 +8,86 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
   alias Pleroma.Activity
   alias Pleroma.Notification
   alias Pleroma.User
+  alias Pleroma.UserRelationship
   alias Pleroma.Web.CommonAPI
   alias Pleroma.Web.MastodonAPI.AccountView
   alias Pleroma.Web.MastodonAPI.NotificationView
   alias Pleroma.Web.MastodonAPI.StatusView
 
-  def render("index.json", %{notifications: notifications, for: user}) do
-    safe_render_many(notifications, NotificationView, "show.json", %{for: user})
+  def render("index.json", %{notifications: notifications, for: reading_user} = opts) do
+    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
+
+    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,
-        for: user
-      }) do
+  def render(
+        "show.json",
+        %{
+          notification: %Notification{activity: activity} = notification,
+          for: reading_user
+        } = opts
+      ) do
     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)
 
-    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 = %{
         id: to_string(notification.id),
         type: mastodon_type,
@@ -36,24 +98,28 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
         }
       }
 
+      render_opts = %{relationships: opts[:relationships]}
+
       case mastodon_type do
         "mention" ->
-          put_status(response, activity, user)
+          put_status(response, activity, reading_user, render_opts)
 
         "favourite" ->
-          put_status(response, parent_activity, user)
+          put_status(response, parent_activity_fn.(), reading_user, render_opts)
 
         "reblog" ->
-          put_status(response, parent_activity, user)
+          put_status(response, parent_activity_fn.(), reading_user, render_opts)
 
         "move" ->
-          put_target(response, activity, user)
+          put_target(response, activity, reading_user, render_opts)
 
         "follow" ->
           response
 
         "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
@@ -64,16 +130,21 @@ defmodule Pleroma.Web.MastodonAPI.NotificationView do
   end
 
   defp put_emoji(response, activity) do
-    response
-    |> Map.put(:emoji, activity.data["content"])
+    Map.put(response, :emoji, activity.data["content"])
   end
 
-  defp put_status(response, activity, user) do
-    Map.put(response, :status, StatusView.render("show.json", %{activity: activity, for: user}))
+  defp put_status(response, activity, reading_user, opts) do
+    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
 
-  defp put_target(response, activity, user) do
-    target = User.get_cached_by_ap_id(activity.data["target"])
-    Map.put(response, :target, AccountView.render("show.json", %{user: target, for: user}))
+  defp put_target(response, activity, reading_user, opts) do
+    target_user = User.get_cached_by_ap_id(activity.data["target"])
+    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
index f7469cdff69e6e7883cbf290395eebb52fe5bc3a..82326986ced11047de3a18206d5673af45605ff1 100644 (file)
@@ -13,6 +13,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
   alias Pleroma.Object
   alias Pleroma.Repo
   alias Pleroma.User
+  alias Pleroma.UserRelationship
   alias Pleroma.Web.CommonAPI
   alias Pleroma.Web.CommonAPI.Utils
   alias Pleroma.Web.MastodonAPI.AccountView
@@ -71,10 +72,41 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
   end
 
   def render("index.json", opts) do
-    replied_to_activities = get_replied_to_activities(opts.activities)
-    opts = Map.put(opts, :replied_to_activities, replied_to_activities)
+    # To do: check AdminAPIControllerTest on the reasons behind nil activities in the list
+    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
 
   def render(
@@ -85,17 +117,25 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
     created_at = Utils.to_masto_date(activity.data["published"])
     activity_object = Object.normalize(activity)
 
-    reblogged_activity =
-      Activity.create_by_object_ap_id(activity_object.data["id"])
-      |> Activity.with_preloaded_bookmark(opts[:for])
-      |> Activity.with_set_thread_muted_field(opts[:for])
-      |> Repo.one()
+    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.with_preloaded_bookmark(opts[:for])
+        |> Activity.with_set_thread_muted_field(opts[:for])
+        |> 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"] || [])
 
-    bookmarked = Activity.get_bookmark(reblogged_activity, opts[:for]) != nil
+    bookmarked = Activity.get_bookmark(reblogged_parent_activity, opts[:for]) != nil
 
     mentions =
       activity.recipients
@@ -107,7 +147,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
       id: to_string(activity.id),
       uri: 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_account_id: nil,
       reblog: reblogged,
@@ -116,7 +161,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
       reblogs_count: 0,
       replies_count: 0,
       favourites_count: 0,
-      reblogged: reblogged?(reblogged_activity, opts[:for]),
+      reblogged: reblogged?(reblogged_parent_activity, opts[:for]),
       favourited: present?(favorited),
       bookmarked: present?(bookmarked),
       muted: false,
@@ -183,9 +228,10 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
       end
 
     thread_muted? =
-      case activity.thread_muted? do
-        thread_muted? when is_boolean(thread_muted?) -> thread_muted?
-        nil -> (opts[:for] && CommonAPI.thread_muted?(opts[:for], activity)) || false
+      cond do
+        is_nil(opts[:for]) -> false
+        is_boolean(activity.thread_muted?) -> activity.thread_muted?
+        true -> CommonAPI.thread_muted?(opts[:for], activity)
       end
 
     attachment_data = object.data["attachment"] || []
@@ -253,11 +299,26 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
         _ -> []
       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),
       uri: object.data["id"],
       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_account_id: reply_to_user && to_string(reply_to_user.id),
       reblog: nil,
@@ -270,7 +331,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
       reblogged: reblogged?(activity, opts[:for]),
       favourited: present?(favorited),
       bookmarked: present?(bookmarked),
-      muted: thread_muted? || User.mutes?(opts[:for], user),
+      muted: muted,
       pinned: pinned?(activity, user),
       sensitive: sensitive,
       spoiler_text: summary,
@@ -421,7 +482,7 @@ defmodule Pleroma.Web.MastodonAPI.StatusView do
   end
 
   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
       "<p><a href=\"#{object.data["id"]}\">#{name}</a></p>#{object.data["content"]}"
     else
index 3f36f6c1a8d000b7e3f7de3f554a77dd02a2888d..a22f744c1ffed29d2f49c677752b5bd015206839 100644 (file)
@@ -173,6 +173,8 @@ defmodule Pleroma.Web.Router do
 
     get("/users/:nickname/password_reset", AdminAPIController, :get_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/:nickname", AdminAPIController, :user_show)
index 7f946426801ee1d6e69fe67eefd2e51538ec07a6..7a35238d7dd71657124d96a919485c7cd1c93c55 100644 (file)
@@ -60,7 +60,9 @@ defmodule Pleroma.Web.StaticFE.StaticFEController do
 
     content =
       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
         nil
       end
index 29f992a678cb6014552725a18b89e117b7ea9bc4..abfed21c8aa51eee4458848fcba33fdf4cd8e177 100644 (file)
@@ -130,7 +130,7 @@ defmodule Pleroma.Web.Streamer.Worker do
 
   defp should_send?(%User{} = user, %Activity{} = item) do
     %{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)
     recipients = MapSet.new(item.recipients)
diff --git a/mix.exs b/mix.exs
index f4f4596828e24951cde5331c3fa9c6710b8f2a9f..77d043d37d5ab5cc555384bb316f703b8ee759ad 100644 (file)
--- a/mix.exs
+++ b/mix.exs
@@ -63,7 +63,7 @@ defmodule Pleroma.Mixfile do
   def application do
     [
       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]
     ]
   end
index 19c56387b1ea9aa19f3d0f8d596c949f0b0c6485..db61ff2665ba2aef7f33dbfa8dbf29b7f6cdcba7 100644 (file)
@@ -174,3 +174,10 @@ button {
     font-weight: 500;
     font-size: 16px;
 }
+
+img.emoji {
+  width: 32px;
+  height: 32px;
+  padding: 0;
+  vertical-align: middle;
+}
diff --git a/test/fixtures/tesla_mock/funkwhale_audio.json b/test/fixtures/tesla_mock/funkwhale_audio.json
new file mode 100644 (file)
index 0000000..15736b1
--- /dev/null
@@ -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"
+    }
+  ]
+}
diff --git a/test/fixtures/tesla_mock/funkwhale_channel.json b/test/fixtures/tesla_mock/funkwhale_channel.json
new file mode 100644 (file)
index 0000000..cf9ee81
--- /dev/null
@@ -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"
+  }
+}
index 6ddd0ad35c3781773baedfd09dab4d953d861ced..7cfa40c5178d5e4798b4628ac25ebb83493b4de6 100644 (file)
@@ -6,12 +6,14 @@ defmodule Pleroma.NotificationTest do
   use Pleroma.DataCase
 
   import Pleroma.Factory
+  import Mock
 
   alias Pleroma.Notification
   alias Pleroma.Tests.ObanHelpers
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.Transmogrifier
   alias Pleroma.Web.CommonAPI
+  alias Pleroma.Web.Push
   alias Pleroma.Web.Streamer
 
   describe "create_notifications" do
@@ -80,6 +82,80 @@ defmodule Pleroma.NotificationTest do
     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
     @tag needs_streamer: true
     test "it creates a notification for user and send to the 'user' and the 'user:notification' stream" do
@@ -382,7 +458,7 @@ defmodule Pleroma.NotificationTest do
     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
       user = insert(:user)
       other_user = insert(:user)
@@ -392,7 +468,9 @@ defmodule Pleroma.NotificationTest do
           "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
 
     test "it sends notifications to mentioned users in new messages" do
@@ -420,7 +498,9 @@ defmodule Pleroma.NotificationTest do
 
       {: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
 
     test "it does not send notifications to users who are only cc in new messages" do
@@ -442,7 +522,9 @@ defmodule Pleroma.NotificationTest do
 
       {: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
 
     test "it does not send notification to mentioned users in likes" do
@@ -457,7 +539,10 @@ defmodule Pleroma.NotificationTest do
 
       {: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
 
     test "it does not send notification to mentioned users in announces" do
@@ -472,7 +557,57 @@ defmodule Pleroma.NotificationTest do
 
       {: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
 
@@ -736,7 +871,7 @@ defmodule Pleroma.NotificationTest do
       assert Notification.for_user(user) == []
     end
 
-    test "it doesn't return notificatitons for blocked domain" do
+    test "it doesn't return notifications for blocked domain" do
       user = insert(:user)
       blocked = insert(:user, ap_id: "http://some-domain.com")
       {:ok, user} = User.block_domain(user, "some-domain.com")
index 8a09e089bb0b4110d7f2d8c15c89ca0f2b021d6d..20cb2b3d180b5302284719aae6f12dc1e6280e18 100644 (file)
@@ -1283,6 +1283,21 @@ defmodule HttpRequestMock do
     {:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/rin.json")}}
   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
     {:ok, %Tesla.Env{status: 404, body: ""}}
   end
index 119a36ec1da2a026d7cea247f711c1cbbf5adbc9..8055ebd08ebb24edffd582f64802b043cd2a5fda 100644 (file)
@@ -86,7 +86,7 @@ defmodule Pleroma.UserTest do
       {:ok, user: insert(:user)}
     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]
 
       ap_ids_by_rel =
@@ -124,10 +124,10 @@ defmodule Pleroma.UserTest do
       assert ap_ids_by_rel[:inverse_subscription] ==
                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 ==
-               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
 
index a43dd34f01fa79ddb0889f05eb8d40243e9cfe58..049b14498ec6bd69c40e50160451bc96b9aaf249 100644 (file)
@@ -1425,6 +1425,12 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
       assert Repo.get(Object, object.id).data["type"] == "Tombstone"
     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
       user = insert(:user, note_count: 10)
 
index 4729ed9ed39c8700bc6c091b12269fdc724ebd37..06238becaf4d91a83ff129e2b0191273ed95cf1f 100644 (file)
@@ -3356,6 +3356,75 @@ defmodule Pleroma.Web.AdminAPI.AdminAPIControllerTest do
     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
     test "sets password_reset_pending to true", %{conn: conn} do
       user = insert(:user)
index 43538cb1715f0cbc4e04c61d2707f53db0a88844..b693c1a47603401240fee96c0d7a89191c13aaf8 100644 (file)
@@ -76,7 +76,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
 
       conn =
         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)
@@ -84,7 +84,7 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
       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="#{
                  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
 
     test "updates the user's locking status", %{conn: conn} do
@@ -118,6 +118,18 @@ defmodule Pleroma.Web.MastodonAPI.MastodonAPIController.UpdateCredentialsTest do
       assert user_data["pleroma"]["hide_followers"] == true
     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
       conn =
         patch(conn, "/api/v1/accounts/update_credentials", %{
index 6fedb4223d8963bbeb7b4251820e8b4f2960caf2..97b1c3e66c35b06d82d1488d1b3b89b0a6f19f6f 100644 (file)
@@ -21,9 +21,12 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
     setup do: oauth_access(["read:statuses"])
 
     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")
 
@@ -31,9 +34,54 @@ defmodule Pleroma.Web.MastodonAPI.TimelineControllerTest do
 
       {: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
 
     test "the home timeline when the direct messages are excluded", %{user: user, conn: conn} do
index d60ed7b6404d5998b899238af0640b6784d05221..0d1c3ecb3fc5cacaf1a58a66106e8ebe78873637 100644 (file)
@@ -4,8 +4,11 @@
 
 defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
   use Pleroma.DataCase
+
   import Pleroma.Factory
+
   alias Pleroma.User
+  alias Pleroma.UserRelationship
   alias Pleroma.Web.CommonAPI
   alias Pleroma.Web.MastodonAPI.AccountView
 
@@ -32,7 +35,8 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
         background: background_image,
         nickname: "shp@shitposter.club",
         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]
       })
 
@@ -46,7 +50,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
       followers_count: 3,
       following_count: 0,
       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,
       avatar: "http://localhost:4001/images/avi.png",
       avatar_static: "http://localhost:4001/images/avi.png",
@@ -63,7 +67,7 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
       fields: [],
       bot: false,
       source: %{
-        note: "valid html",
+        note: "valid html. a\nb\nc\nd\nf",
         sensitive: false,
         pleroma: %{
           actor_type: "Person",
@@ -181,6 +185,29 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
   end
 
   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
       user = insert(:user)
       other_user = insert(:user)
@@ -191,23 +218,21 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
       {:ok, _user_relationships} = User.mute(user, other_user, true)
       {:ok, _reblog_mute} = CommonAPI.hide_reblogs(user, other_user)
 
-      expected = %{
-        id: to_string(other_user.id),
-        following: true,
-        followed_by: true,
-        blocking: false,
-        blocked_by: false,
-        muting: true,
-        muting_notifications: true,
-        subscribing: true,
-        requested: false,
-        domain_blocking: false,
-        showing_reblogs: false,
-        endorsed: false
-      }
-
-      assert expected ==
-               AccountView.render("relationship.json", %{user: user, target: other_user})
+      expected =
+        Map.merge(
+          @blank_response,
+          %{
+            following: true,
+            followed_by: true,
+            muting: true,
+            muting_notifications: true,
+            subscribing: true,
+            showing_reblogs: false,
+            id: to_string(other_user.id)
+          }
+        )
+
+      test_relationship_rendering(user, other_user, expected)
     end
 
     test "represent a relationship for the blocking and blocked user" do
@@ -219,23 +244,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
       {:ok, _user_relationship} = User.block(user, other_user)
       {:ok, _user_relationship} = User.block(other_user, user)
 
-      expected = %{
-        id: to_string(other_user.id),
-        following: false,
-        followed_by: false,
-        blocking: true,
-        blocked_by: true,
-        muting: false,
-        muting_notifications: false,
-        subscribing: false,
-        requested: false,
-        domain_blocking: false,
-        showing_reblogs: true,
-        endorsed: false
-      }
+      expected =
+        Map.merge(
+          @blank_response,
+          %{following: false, blocking: true, blocked_by: true, id: to_string(other_user.id)}
+        )
 
-      assert expected ==
-               AccountView.render("relationship.json", %{user: user, target: other_user})
+      test_relationship_rendering(user, other_user, expected)
     end
 
     test "represent a relationship for the user blocking a domain" do
@@ -244,8 +259,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
 
       {:ok, user} = User.block_domain(user, "bad.site")
 
-      assert %{domain_blocking: true, blocking: false} =
-               AccountView.render("relationship.json", %{user: user, target: other_user})
+      expected =
+        Map.merge(
+          @blank_response,
+          %{domain_blocking: true, blocking: false, id: to_string(other_user.id)}
+        )
+
+      test_relationship_rendering(user, other_user, expected)
     end
 
     test "represent a relationship for the user with a pending follow request" do
@@ -256,23 +276,13 @@ defmodule Pleroma.Web.MastodonAPI.AccountViewTest do
       user = User.get_cached_by_id(user.id)
       other_user = User.get_cached_by_id(other_user.id)
 
-      expected = %{
-        id: to_string(other_user.id),
-        following: false,
-        followed_by: false,
-        blocking: false,
-        blocked_by: false,
-        muting: false,
-        muting_notifications: false,
-        subscribing: false,
-        requested: true,
-        domain_blocking: false,
-        showing_reblogs: true,
-        endorsed: false
-      }
+      expected =
+        Map.merge(
+          @blank_response,
+          %{requested: true, following: false, id: to_string(other_user.id)}
+        )
 
-      assert expected ==
-               AccountView.render("relationship.json", %{user: user, target: other_user})
+      test_relationship_rendering(user, other_user, expected)
     end
   end
 
index ac3e126179fae9fc1b3a1883e16755df672de02a..81eefd7350d839078ba09793eb127061a3f041b2 100644 (file)
@@ -16,6 +16,21 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
   alias Pleroma.Web.MastodonAPI.StatusView
   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
     user = insert(:user)
     mentioned_user = insert(:user)
@@ -32,10 +47,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
       created_at: Utils.to_masto_date(notification.inserted_at)
     }
 
-    result =
-      NotificationView.render("index.json", %{notifications: [notification], for: mentioned_user})
-
-    assert [expected] == result
+    test_notifications_rendering([notification], mentioned_user, [expected])
   end
 
   test "Favourite notification" do
@@ -55,9 +67,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
       created_at: Utils.to_masto_date(notification.inserted_at)
     }
 
-    result = NotificationView.render("index.json", %{notifications: [notification], for: user})
-
-    assert [expected] == result
+    test_notifications_rendering([notification], user, [expected])
   end
 
   test "Reblog notification" do
@@ -77,9 +87,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
       created_at: Utils.to_masto_date(notification.inserted_at)
     }
 
-    result = NotificationView.render("index.json", %{notifications: [notification], for: user})
-
-    assert [expected] == result
+    test_notifications_rendering([notification], user, [expected])
   end
 
   test "Follow notification" do
@@ -96,16 +104,12 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
       created_at: Utils.to_masto_date(notification.inserted_at)
     }
 
-    result =
-      NotificationView.render("index.json", %{notifications: [notification], for: followed})
-
-    assert [expected] == result
+    test_notifications_rendering([notification], followed, [expected])
 
     User.perform(:delete, follower)
     notification = Notification |> Repo.one() |> Repo.preload(:activity)
 
-    assert [] ==
-             NotificationView.render("index.json", %{notifications: [notification], for: followed})
+    test_notifications_rendering([notification], followed, [])
   end
 
   @tag capture_log: true
@@ -144,8 +148,7 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
       created_at: Utils.to_masto_date(notification.inserted_at)
     }
 
-    assert [expected] ==
-             NotificationView.render("index.json", %{notifications: [notification], for: follower})
+    test_notifications_rendering([notification], follower, [expected])
   end
 
   test "EmojiReact notification" do
@@ -171,7 +174,6 @@ defmodule Pleroma.Web.MastodonAPI.NotificationViewTest do
       created_at: Utils.to_masto_date(notification.inserted_at)
     }
 
-    assert expected ==
-             NotificationView.render("show.json", %{notification: notification, for: user})
+    test_notifications_rendering([notification], user, [expected])
   end
 end
index 7df72decbe6b0f5450f9503efade448512d73d8d..6791c2fb08b5fdebd1a95e37424f03da8bcd28ec 100644 (file)
@@ -12,10 +12,12 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
   alias Pleroma.Object
   alias Pleroma.Repo
   alias Pleroma.User
+  alias Pleroma.UserRelationship
   alias Pleroma.Web.CommonAPI
   alias Pleroma.Web.CommonAPI.Utils
   alias Pleroma.Web.MastodonAPI.AccountView
   alias Pleroma.Web.MastodonAPI.StatusView
+
   import Pleroma.Factory
   import Tesla.Mock
 
@@ -229,12 +231,21 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
     {:ok, _user_relationships} = User.mute(user, other_user)
 
     {: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
 
-    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
   end
 
@@ -437,6 +448,22 @@ defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
     assert length(represented[:media_attachments]) == 1
   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
     user = insert(:user)
 
index 0b0972b17979f9aaad3521633201660854ff11db..f2f98d768101b193bf9c7a8c132036ed59eb069c 100644 (file)
@@ -575,7 +575,7 @@ defmodule Pleroma.Web.OAuth.OAuthControllerTest do
       # In case scope param is missing, expecting _all_ app-supported scopes to be granted
       for user <- [non_admin, admin],
           {requested_scopes, expected_scopes} <-
-            %{scopes_subset => scopes_subset, nil => app_scopes} do
+            %{scopes_subset => scopes_subset, nil: app_scopes} do
         conn =
           post(
             build_conn(),