Merge branch 'develop' into issue/1276
authorMaksim Pechnikov <parallel588@gmail.com>
Mon, 27 Jan 2020 12:20:47 +0000 (15:20 +0300)
committerMaksim Pechnikov <parallel588@gmail.com>
Mon, 27 Jan 2020 12:20:47 +0000 (15:20 +0300)
1  2 
CHANGELOG.md
docs/API/differences_in_mastoapi_responses.md
lib/pleroma/notification.ex
test/notification_test.exs

diff --combined CHANGELOG.md
index 4a2296cdd071107c308b4a842494d497c3d2ea2d,4d626a6836e4660592b266e194a7f0aec979ccf5..bd18073932d894161e86192c7c10639fe06d03ee
@@@ -7,10 -7,15 +7,15 @@@ The format is based on [Keep a Changelo
  ### Removed
  - **Breaking**: Removed 1.0+ deprecated configurations `Pleroma.Upload, :strip_exif` and `:instance, :dedupe_media`
  - **Breaking**: OStatus protocol support
+ - **Breaking**: MDII uploader
  
  ### Changed
+ - **Breaking:** Pleroma won't start if it detects unapplied migrations
+ - **Breaking:** attachments are removed along with statuses when there are no other references to it
  - **Breaking:** Elixir >=1.8 is now required (was >= 1.7)
  - **Breaking:** attachment links (`config :pleroma, :instance, no_attachment_links` and `config :pleroma, Pleroma.Upload, link_name`) disabled by default
+ - **Breaking:** OAuth: defaulted `[:auth, :enforce_oauth_admin_scope_usage]` setting to `true` which demands `admin` OAuth scope to perform admin actions (in addition to `is_admin` flag on User); make sure to use bundled or newer versions of AdminFE & PleromaFE to access admin / moderator features.
+ - **Breaking:** Dynamic configuration has been rearchitected. The `:pleroma, :instance, dynamic_configuration` setting has been replaced with `config :pleroma, configurable_from_database`. Please backup your configuration to a file and run the migration task to ensure consistency with the new schema.
  - Replaced [pleroma_job_queue](https://git.pleroma.social/pleroma/pleroma_job_queue) and `Pleroma.Web.Federator.RetryQueue` with [Oban](https://github.com/sorentwo/oban) (see [`docs/config.md`](docs/config.md) on migrating customized worker / retry settings)
  - Introduced [quantum](https://github.com/quantum-elixir/quantum-core) job scheduler
  - Enabled `:instance, extended_nickname_format` in the default config
@@@ -21,6 -26,8 +26,8 @@@
  - Deprecated `User.Info` embedded schema (fields moved to `User`)
  - Store status data inside Flag activity
  - Deprecated (reorganized as `UserRelationship` entity) User fields with user AP IDs (`blocks`, `mutes`, `muted_reblogs`, `muted_notifications`, `subscribers`).
+ - Logger: default log level changed from `warn` to `info`.
+ - Config mix task `migrate_to_db` truncates `config` table before migrating the config file.
  <details>
    <summary>API Changes</summary>
  
@@@ -28,6 -35,7 +35,7 @@@
  - **Breaking:** Admin API: Return link alongside with token on password reset
  - **Breaking:** Admin API: `PUT /api/pleroma/admin/reports/:id` is now `PATCH /api/pleroma/admin/reports`, see admin_api.md for details
  - **Breaking:** `/api/pleroma/admin/users/invite_token` now uses `POST`, changed accepted params and returns full invite in json instead of only token string.
+ - **Breaking** replying to reports is now "report notes", enpoint changed from `POST /api/pleroma/admin/reports/:id/respond` to `POST /api/pleroma/admin/reports/:id/notes`
  - Admin API: Return `total` when querying for reports
  - Mastodon API: Return `pleroma.direct_conversation_id` when creating a direct message (`POST /api/v1/statuses`)
  - Admin API: Return link alongside with token on password reset
  - Mastodon API: `pleroma.thread_muted` to the Status entity
  - Mastodon API: Mark the direct conversation as read for the author when they send a new direct message
  - Mastodon API, streaming: Add `pleroma.direct_conversation_id` to the `conversation` stream event payload.
 +- Mastodon API: Add `pleroma.unread_count` to the Marker entity
  - Admin API: Render whole status in grouped reports
+ - 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.
  </details>
  
  ### Added
  - Mix task to list all users (`mix pleroma.user list`)
  - Support for `X-Forwarded-For` and similar HTTP headers which used by reverse proxies to pass a real user IP address to the backend. Must not be enabled unless your instance is behind at least one reverse proxy (such as Nginx, Apache HTTPD or Varnish Cache).
  - MRF: New module which handles incoming posts based on their age. By default, all incoming posts that are older than 2 days will be unlisted and not shown to their followers.
+ - User notification settings: Add `privacy_option` option.
+ - Support for custom Elixir modules (such as MRF policies)
+ - User settings: Add _This account is a_ option.
+ - OAuth: admin scopes support (relevant setting: `[:auth, :enforce_oauth_admin_scope_usage]`).
  <details>
    <summary>API Changes</summary>
  
  - Pleroma API: Add Emoji reactions
  - Admin API: Add `/api/pleroma/admin/instances/:instance/statuses` - lists all statuses from a given instance
  - Admin API: `PATCH /api/pleroma/users/confirm_email` to confirm email for multiple users, `PATCH /api/pleroma/users/resend_confirmation_email` to resend confirmation email for multiple users
+ - ActivityPub: Configurable `type` field of the actors.
+ - Mastodon API: `/api/v1/accounts/:id` has `source/pleroma/actor_type` field.
+ - Mastodon API: `/api/v1/update_credentials` accepts `actor_type` field.
+ - Captcha: Support native provider
+ - Captcha: Enable by default
+ - Mastodon API: Add support for `account_id` param to filter notifications by the account
+ - Mastodon API: Add `emoji_reactions` property to Statuses
+ - Mastodon API: Change emoji reaction reply format
+ - Notifications: Added `pleroma:emoji_reaction` notification type
+ - Mastodon API: Change emoji reaction reply format once more
  </details>
  
  ### Fixed
  - Report emails now include functional links to profiles of remote user accounts
  - Not being able to log in to some third-party apps when logged in to MastoFE
  - MRF: `Delete` activities being exempt from MRF policies
+ - OTP releases: Not being able to configure OAuth expired token cleanup interval
+ - OTP releases: Not being able to configure HTML sanitization policy
+ - Favorites timeline now ordered by favorite date instead of post date
  <details>
    <summary>API Changes</summary>
  
  - Mastodon API: Inability to get some local users by nickname in `/api/v1/accounts/:id_or_nickname`
  - AdminAPI: If some status received reports both in the "new" format and "old" format it was considered reports on two different statuses (in the context of grouped reports)
  - Admin API: Error when trying to update reports in the "old" format
+ - Mastodon API: Marking a conversation as read (`POST /api/v1/conversations/:id/read`) now no longer brings it to the top in the user's direct conversation list
  </details>
  
  ## [1.1.6] - 2019-11-19
index eea7f07070e15e1a1aa7147d88e09324c6768b73,030660b346b401c84317bb1840a1313b2b6242d6..40cac158e8cdde3f0b56e9e3c50ed0438c7bbcdb
@@@ -29,6 -29,7 +29,7 @@@ Has these additional fields under the `
  - `spoiler_text`: a map consisting of alternate representations of the `spoiler_text` property with the key being it's mimetype. Currently the only alternate representation supported is `text/plain`
  - `expires_at`: a datetime (iso8601) that states when the post will expire (be deleted automatically), or empty if the post won't expire
  - `thread_muted`: true if the thread the post belongs to is muted
+ - `emoji_reactions`: A list with emoji / reaction maps. The format is {emoji: "☕", count: 1}. Contains no information about the reacting users, for that use the `emoji_reactions_by` endpoint.
  
  ## Attachments
  
@@@ -46,7 -47,7 +47,7 @@@ The `id` parameter can also be the `nic
  Has these additional fields under the `pleroma` object:
  
  - `tags`: Lists an array of tags for the user
- - `relationship{}`: Includes fields as documented for Mastodon API https://docs.joinmastodon.org/api/entities/#relationship
+ - `relationship{}`: Includes fields as documented for Mastodon API https://docs.joinmastodon.org/entities/relationship/
  - `is_moderator`: boolean, nullable,  true if user is a moderator
  - `is_admin`: boolean, nullable, true if user is an admin
  - `confirmation_pending`: boolean, true if a new user account is waiting on email confirmation to be activated
@@@ -66,6 -67,8 +67,8 @@@ Has these additional fields under the `
  
  - `show_role`: boolean, nullable, true when the user wants his role (e.g admin, moderator) to be shown
  - `no_rich_text` - boolean, nullable, true when html tags are stripped from all statuses requested from the API
+ - `discoverable`: boolean, true when the user allows discovery of the account in search results and other services.
+ - `actor_type`: string, the type of this account.
  
  ## Conversations
  
@@@ -98,11 -101,20 +101,20 @@@ The `type` value is `move`. Has an addi
  
  - `target`: new account
  
+ ### EmojiReaction Notification
+ The `type` value is `pleroma:emoji_reaction`. Has these fields:
+ - `emoji`: The used emoji
+ - `account`: The account of the user who reacted
+ - `status`: The status that was reacted on
  ## GET `/api/v1/notifications`
  
  Accepts additional parameters:
  
  - `exclude_visibilities`: will exclude the notifications for activities with the given visibilities. The parameter accepts an array of visibility types (`public`, `unlisted`, `private`, `direct`). Usage example: `GET /api/v1/notifications?exclude_visibilities[]=direct&exclude_visibilities[]=private`.
+ - `with_move`: boolean, when set to `true` will include Move notifications. `false` by default.
  
  ## POST `/api/v1/statuses`
  
@@@ -145,6 -157,8 +157,8 @@@ Additional parameters can be added to t
  - `skip_thread_containment` - if true, skip filtering out broken threads
  - `allow_following_move` - if true, allows automatically follow moved following accounts
  - `pleroma_background_image` - sets the background image of the user.
+ - `discoverable` - if true, discovery of this account in search results and other services is allowed.
+ - `actor_type` - the type of this account.
  
  ### Pleroma Settings Store
  Pleroma has mechanism that allows frontends to save blobs of json for each user on the backend. This can be used to save frontend-specific settings for a user that the backend does not need to know about.
@@@ -169,9 -183,3 +183,9 @@@ Has theses additionnal parameters (whic
      * `captcha_solution`: optional, contains provider-specific captcha solution,
      * `captcha_token`: optional, contains provider-specific captcha token
      * `token`: invite token required when the registerations aren't public.
 +
 +## Markers
 +
 +Has these additional fields under the `pleroma` object:
 +
 +- `unread_count`: contains number unread notifications
index 11adbb77bf7e9278cb959878a6b79e65450ea851,d04a65a1e6c011828c41686d6ff87a027bbbd569..e2b75054e767b6ac302a852817eecf0c6a61ed71
@@@ -5,9 -5,7 +5,9 @@@
  defmodule Pleroma.Notification do
    use Ecto.Schema
  
 +  alias Ecto.Multi
    alias Pleroma.Activity
 +  alias Pleroma.Marker
    alias Pleroma.Notification
    alias Pleroma.Object
    alias Pleroma.Pagination
      |> cast(attrs, [:seen])
    end
  
 +  @spec unread_count_query(User.t()) :: Ecto.Queryable.t()
 +  def unread_count_query(user) do
 +    from(q in Pleroma.Notification,
 +      where: q.user_id == ^user.id,
 +      where: q.seen == false
 +    )
 +  end
 +
 +  @spec last_read_query(User.t()) :: Ecto.Queryable.t()
 +  def last_read_query(user) do
 +    from(q in Pleroma.Notification,
 +      where: q.user_id == ^user.id,
 +      where: q.seen == true,
 +      select: type(q.id, :string),
 +      limit: 1,
 +      order_by: [desc: :id]
 +    )
 +  end
 +
    defp for_user_query_ap_id_opts(user, opts) do
      ap_id_relations =
        [:block] ++
         when is_list(visibility) do
      if Enum.all?(visibility, &(&1 in @valid_visibilities)) do
        query
+       |> join(:left, [n, a], mutated_activity in Pleroma.Activity,
+         on:
+           fragment("?->>'context'", a.data) ==
+             fragment("?->>'context'", mutated_activity.data) and
+             fragment("(?->>'type' = 'Like' or ?->>'type' = 'Announce')", a.data, a.data) and
+             fragment("?->>'type'", mutated_activity.data) == "Create",
+         as: :mutated_activity
+       )
        |> where(
-         [n, a],
+         [n, a, mutated_activity: mutated_activity],
          not fragment(
-           "activity_visibility(?, ?, ?) = ANY (?)",
+           """
+           CASE WHEN (?->>'type') = 'Like' or (?->>'type') = 'Announce'
+             THEN (activity_visibility(?, ?, ?) = ANY (?))
+             ELSE (activity_visibility(?, ?, ?) = ANY (?)) END
+           """,
+           a.data,
+           a.data,
+           mutated_activity.actor,
+           mutated_activity.recipients,
+           mutated_activity.data,
+           ^visibility,
            a.actor,
            a.recipients,
            a.data,
  
    defp exclude_visibility(query, %{exclude_visibilities: visibility})
         when visibility in @valid_visibilities do
-     query
-     |> where(
-       [n, a],
-       not fragment(
-         "activity_visibility(?, ?, ?) = (?)",
-         a.actor,
-         a.recipients,
-         a.data,
-         ^visibility
-       )
-     )
+     exclude_visibility(query, [visibility])
    end
  
    defp exclude_visibility(query, %{exclude_visibilities: visibility})
      |> Repo.all()
    end
  
 -  def set_read_up_to(%{id: user_id} = _user, id) do
 +  def set_read_up_to(%{id: user_id} = user, id) do
      query =
        from(
          n in Notification,
          where: n.user_id == ^user_id,
          where: n.id <= ^id,
          where: n.seen == false,
 -        update: [
 -          set: [
 -            seen: true,
 -            updated_at: ^NaiveDateTime.utc_now()
 -          ]
 -        ],
          # Ideally we would preload object and activities here
          # but Ecto does not support preloads in update_all
          select: n.id
        )
  
 -    {_, notification_ids} = Repo.update_all(query, [])
 +    {:ok, %{ids: {_, notification_ids}}} =
 +      Multi.new()
 +      |> Multi.update_all(:ids, query, set: [seen: true, updated_at: NaiveDateTime.utc_now()])
 +      |> Marker.multi_set_unread_count(user, "notifications")
 +      |> Repo.transaction()
  
      Notification
      |> where([n], n.id in ^notification_ids)
      |> Repo.all()
    end
  
 +  @spec read_one(User.t(), String.t()) ::
 +          {:ok, Notification.t()} | {:error, Ecto.Changeset.t()} | nil
    def read_one(%User{} = user, notification_id) do
      with {:ok, %Notification{} = notification} <- get(user, notification_id) do
 -      notification
 -      |> changeset(%{seen: true})
 -      |> Repo.update()
 +      Multi.new()
 +      |> Multi.update(:update, changeset(notification, %{seen: true}))
 +      |> Marker.multi_set_unread_count(user, "notifications")
 +      |> Repo.transaction()
 +      |> case do
 +        {:ok, %{update: notification}} -> {:ok, notification}
 +        {:error, :update, changeset, _} -> {:error, changeset}
 +      end
      end
    end
  
      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)
 +      notifications =
 +        activity
 +        |> get_notified_from_activity()
 +        |> Enum.map(&create_notification(activity, &1))
 +
        {:ok, notifications}
      else
        {:ok, []}
    end
  
    def create_notifications(%Activity{data: %{"type" => type}} = activity)
-       when type in ["Like", "Announce", "Follow", "Move"] do
+       when type in ["Like", "Announce", "Follow", "Move", "EmojiReaction"] do
      notifications =
        activity
        |> get_notified_from_activity()
    # TODO move to sql, too.
    def create_notification(%Activity{} = activity, %User{} = user) do
      unless skip?(activity, user) do
 -      notification = %Notification{user_id: user.id, activity: activity}
 -      {:ok, notification} = Repo.insert(notification)
 +      {:ok, %{notification: notification}} =
 +        Multi.new()
 +        |> Multi.insert(:notification, %Notification{user_id: user.id, activity: activity})
 +        |> Marker.multi_set_unread_count(user, "notifications")
 +        |> Repo.transaction()
  
        ["user", "user:notification"]
        |> Streamer.stream(notification)
    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"] do
+       when type in ["Create", "Like", "Announce", "Follow", "Move", "EmojiReaction"] do
      []
      |> Utils.maybe_notify_to_recipients(activity)
      |> Utils.maybe_notify_mentioned_recipients(activity)
    def skip?(
          :followers,
          activity,
-         %{notification_settings: %{"followers" => false}} = user
+         %{notification_settings: %{followers: false}} = user
        ) do
      actor = activity.data["actor"]
      follower = User.get_cached_by_ap_id(actor)
    def skip?(
          :non_followers,
          activity,
-         %{notification_settings: %{"non_followers" => false}} = 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, %{notification_settings: %{follows: false}} = user) do
      actor = activity.data["actor"]
      followed = User.get_cached_by_ap_id(actor)
      User.following?(user, followed)
    def skip?(
          :non_follows,
          activity,
-         %{notification_settings: %{"non_follows" => false}} = user
+         %{notification_settings: %{non_follows: false}} = user
        ) do
      actor = activity.data["actor"]
      followed = User.get_cached_by_ap_id(actor)
index 80a69d4df7a8f74d6a28d065a58f5e1b870eac42,04bf5b41aaf62883934167a3cca48978c7b658c8..c9b35209720f027b0c1c884f01ad29c4546eae0c
@@@ -15,6 -15,18 +15,18 @@@ defmodule Pleroma.NotificationTest d
    alias Pleroma.Web.Streamer
  
    describe "create_notifications" do
+     test "creates a notification for an emoji reaction" do
+       user = insert(:user)
+       other_user = insert(:user)
+       {:ok, activity} = CommonAPI.post(user, %{"status" => "yeah"})
+       {:ok, activity, _object} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
+       {:ok, [notification]} = Notification.create_notifications(activity)
+       assert notification.user_id == user.id
+     end
      test "notifies someone when they are directly addressed" do
        user = insert(:user)
        other_user = insert(:user)
@@@ -31,9 -43,6 +43,9 @@@
        assert notified_ids == [other_user.id, third_user.id]
        assert notification.activity_id == activity.id
        assert other_notification.activity_id == activity.id
 +
 +      assert [%Pleroma.Marker{unread_count: 2}] =
 +               Pleroma.Marker.get_markers(other_user, ["notifications"])
      end
  
      test "it creates a notification for subscribed users" do
        assert Notification.create_notification(activity, user)
      end
  
-     test "it creates a notificatin for the user if the user mutes the activity author" do
+     test "it creates a notification for the user if the user mutes the activity author" do
        muter = insert(:user)
        muted = insert(:user)
        {:ok, _} = User.mute(muter, muted)
  
      test "it disables notifications from followers" do
        follower = insert(:user)
-       followed = insert(:user, notification_settings: %{"followers" => false})
+       followed =
+         insert(:user, notification_settings: %Pleroma.User.NotificationSetting{followers: false})
        User.follow(follower, followed)
        {:ok, activity} = CommonAPI.post(follower, %{"status" => "hey @#{followed.nickname}"})
        refute Notification.create_notification(activity, followed)
  
      test "it disables notifications from non-followers" do
        follower = insert(:user)
-       followed = insert(:user, notification_settings: %{"non_followers" => false})
+       followed =
+         insert(:user,
+           notification_settings: %Pleroma.User.NotificationSetting{non_followers: false}
+         )
        {:ok, activity} = CommonAPI.post(follower, %{"status" => "hey @#{followed.nickname}"})
        refute Notification.create_notification(activity, followed)
      end
  
      test "it disables notifications from people the user follows" do
-       follower = insert(:user, notification_settings: %{"follows" => false})
+       follower =
+         insert(:user, notification_settings: %Pleroma.User.NotificationSetting{follows: false})
        followed = insert(:user)
        User.follow(follower, followed)
        follower = Repo.get(User, follower.id)
      end
  
      test "it disables notifications from people the user does not follow" do
-       follower = insert(:user, notification_settings: %{"non_follows" => false})
+       follower =
+         insert(:user, notification_settings: %Pleroma.User.NotificationSetting{non_follows: false})
        followed = insert(:user)
        {:ok, activity} = CommonAPI.post(followed, %{"status" => "hey @#{follower.nickname}"})
        refute Notification.create_notification(activity, follower)
        assert n1.seen == true
        assert n2.seen == true
        assert n3.seen == false
 +
 +      assert %Pleroma.Marker{unread_count: 1} =
 +               Pleroma.Repo.get_by(
 +                 Pleroma.Marker,
 +                 user_id: other_user.id,
 +                 timeline: "notifications"
 +               )
      end
    end
  
  
        {:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"})
  
-       assert length(Notification.for_user(user, %{with_muted: true})) == 0
+       assert Enum.empty?(Notification.for_user(user, %{with_muted: true}))
      end
  
      test "it doesn't return notifications from a domain-blocked user when with_muted is set" do
  
        {:ok, _activity} = CommonAPI.post(blocked, %{"status" => "hey @#{user.nickname}"})
  
-       assert length(Notification.for_user(user, %{with_muted: true})) == 0
+       assert Enum.empty?(Notification.for_user(user, %{with_muted: true}))
      end
  
      test "it returns notifications from muted threads when with_muted is set" do