Merge branch 'develop' into feature/admin-api-status-count-per-instance
This commit is contained in:
		
						commit
						942093683a
					
				
					 37 changed files with 647 additions and 216 deletions
				
			
		
							
								
								
									
										27
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										27
									
								
								CHANGELOG.md
									
									
									
									
									
								
							| 
						 | 
				
			
			@ -40,24 +40,44 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 | 
			
		|||
- Filtering of push notifications on activities from blocked domains
 | 
			
		||||
- Resolving Peertube accounts with Webfinger
 | 
			
		||||
 | 
			
		||||
## [unreleased-patch]
 | 
			
		||||
## [Unreleased (patch)]
 | 
			
		||||
 | 
			
		||||
### Fixed
 | 
			
		||||
- Healthcheck reporting the number of memory currently used, rather than allocated in total
 | 
			
		||||
 | 
			
		||||
## [2.0.3] - 2020-05-02
 | 
			
		||||
 | 
			
		||||
### Security
 | 
			
		||||
- Disallow re-registration of previously deleted users, which allowed viewing direct messages addressed to them
 | 
			
		||||
- Mastodon API: Fix `POST /api/v1/follow_requests/:id/authorize` allowing to force a follow from a local user even if they didn't request to follow
 | 
			
		||||
- CSP: Sandbox uploads
 | 
			
		||||
 | 
			
		||||
### Fixed
 | 
			
		||||
- Logger configuration through AdminFE
 | 
			
		||||
- Notifications from blocked domains
 | 
			
		||||
- Potential federation issues with Mastodon versions before 3.0.0
 | 
			
		||||
- HTTP Basic Authentication permissions issue
 | 
			
		||||
- Follow/Block imports not being able to find the user if the nickname started with an `@`
 | 
			
		||||
- Instance stats counting internal users
 | 
			
		||||
- Inability to run a From Source release without git
 | 
			
		||||
- ObjectAgePolicy didn't filter out old messages
 | 
			
		||||
- Transmogrifier: Keep object sensitive settings for outgoing representation (AP C2S)
 | 
			
		||||
- `blob:` urls not being allowed by CSP
 | 
			
		||||
 | 
			
		||||
### Added
 | 
			
		||||
- NodeInfo: ObjectAgePolicy settings to the `federation` list.
 | 
			
		||||
- Follow request notifications
 | 
			
		||||
<details>
 | 
			
		||||
  <summary>API Changes</summary>
 | 
			
		||||
- Admin API: `GET /api/pleroma/admin/need_reboot`.
 | 
			
		||||
</details>
 | 
			
		||||
 | 
			
		||||
### Upgrade notes
 | 
			
		||||
 | 
			
		||||
1. Restart Pleroma
 | 
			
		||||
2. Run database migrations (inside Pleroma directory):
 | 
			
		||||
  - OTP: `./bin/pleroma_ctl migrate`
 | 
			
		||||
  - From Source: `mix ecto.migrate`
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## [2.0.2] - 2020-04-08
 | 
			
		||||
### Added
 | 
			
		||||
- Support for Funkwhale's `Audio` activity
 | 
			
		||||
| 
						 | 
				
			
			@ -156,6 +176,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 | 
			
		|||
- 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.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -712,7 +712,7 @@
 | 
			
		|||
        key: :quarantined_instances,
 | 
			
		||||
        type: {:list, :string},
 | 
			
		||||
        description:
 | 
			
		||||
          "List of ActivityPub instances where private (DMs, followers-only) activities will not be send",
 | 
			
		||||
          "List of ActivityPub instances where private (DMs, followers-only) activities will not be sent",
 | 
			
		||||
        suggestions: [
 | 
			
		||||
          "quarantined.com",
 | 
			
		||||
          "*.quarantined.com"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -61,6 +61,7 @@ Has these additional fields under the `pleroma` object:
 | 
			
		|||
- `deactivated`: boolean, true when the user is deactivated
 | 
			
		||||
- `allow_following_move`: boolean, true when the user allows automatically follow moved following accounts
 | 
			
		||||
- `unread_conversation_count`: The count of unread conversations. Only returned to the account owner.
 | 
			
		||||
- `unread_notifications_count`: The count of unread notifications. Only returned to the account owner.
 | 
			
		||||
 | 
			
		||||
### Source
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -218,3 +219,9 @@ Has theses additional parameters (which are the same as in Pleroma-API):
 | 
			
		|||
- `pleroma.metadata.features`: A list of supported features
 | 
			
		||||
- `pleroma.metadata.federation`: The federation restrictions of this instance
 | 
			
		||||
- `vapid_public_key`: The public key needed for push messages
 | 
			
		||||
 | 
			
		||||
## Markers
 | 
			
		||||
 | 
			
		||||
Has these additional fields under the `pleroma` object:
 | 
			
		||||
 | 
			
		||||
- `unread_count`: contains number unread notifications
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,7 +29,7 @@ defmodule Pleroma.Healthcheck do
 | 
			
		|||
  @spec system_info() :: t()
 | 
			
		||||
  def system_info do
 | 
			
		||||
    %Healthcheck{
 | 
			
		||||
      memory_used: Float.round(:erlang.memory(:total) / 1024 / 1024, 2)
 | 
			
		||||
      memory_used: Float.round(:recon_alloc.memory(:allocated) / 1024 / 1024, 2)
 | 
			
		||||
    }
 | 
			
		||||
    |> assign_db_info()
 | 
			
		||||
    |> assign_job_queue_stats()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -9,24 +9,34 @@ defmodule Pleroma.Marker do
 | 
			
		|||
  import Ecto.Query
 | 
			
		||||
 | 
			
		||||
  alias Ecto.Multi
 | 
			
		||||
  alias Pleroma.Notification
 | 
			
		||||
  alias Pleroma.Repo
 | 
			
		||||
  alias Pleroma.User
 | 
			
		||||
  alias __MODULE__
 | 
			
		||||
 | 
			
		||||
  @timelines ["notifications"]
 | 
			
		||||
  @type t :: %__MODULE__{}
 | 
			
		||||
 | 
			
		||||
  schema "markers" do
 | 
			
		||||
    field(:last_read_id, :string, default: "")
 | 
			
		||||
    field(:timeline, :string, default: "")
 | 
			
		||||
    field(:lock_version, :integer, default: 0)
 | 
			
		||||
    field(:unread_count, :integer, default: 0, virtual: true)
 | 
			
		||||
 | 
			
		||||
    belongs_to(:user, User, type: FlakeId.Ecto.CompatType)
 | 
			
		||||
    timestamps()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc "Gets markers by user and timeline."
 | 
			
		||||
  @spec get_markers(User.t(), list(String)) :: list(t())
 | 
			
		||||
  def get_markers(user, timelines \\ []) do
 | 
			
		||||
    Repo.all(get_query(user, timelines))
 | 
			
		||||
    user
 | 
			
		||||
    |> get_query(timelines)
 | 
			
		||||
    |> unread_count_query()
 | 
			
		||||
    |> Repo.all()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @spec upsert(User.t(), map()) :: {:ok | :error, any()}
 | 
			
		||||
  def upsert(%User{} = user, attrs) do
 | 
			
		||||
    attrs
 | 
			
		||||
    |> Map.take(@timelines)
 | 
			
		||||
| 
						 | 
				
			
			@ -45,6 +55,27 @@ def upsert(%User{} = user, attrs) do
 | 
			
		|||
    |> Repo.transaction()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @spec multi_set_last_read_id(Multi.t(), User.t(), String.t()) :: Multi.t()
 | 
			
		||||
  def multi_set_last_read_id(multi, %User{} = user, "notifications") do
 | 
			
		||||
    multi
 | 
			
		||||
    |> Multi.run(:counters, fn _repo, _changes ->
 | 
			
		||||
      {:ok, %{last_read_id: Repo.one(Notification.last_read_query(user))}}
 | 
			
		||||
    end)
 | 
			
		||||
    |> Multi.insert(
 | 
			
		||||
      :marker,
 | 
			
		||||
      fn %{counters: attrs} ->
 | 
			
		||||
        %Marker{timeline: "notifications", user_id: user.id}
 | 
			
		||||
        |> struct(attrs)
 | 
			
		||||
        |> Ecto.Changeset.change()
 | 
			
		||||
      end,
 | 
			
		||||
      returning: true,
 | 
			
		||||
      on_conflict: {:replace, [:last_read_id]},
 | 
			
		||||
      conflict_target: [:user_id, :timeline]
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def multi_set_last_read_id(multi, _, _), do: multi
 | 
			
		||||
 | 
			
		||||
  defp get_marker(user, timeline) do
 | 
			
		||||
    case Repo.find_resource(get_query(user, timeline)) do
 | 
			
		||||
      {:ok, marker} -> %__MODULE__{marker | user: user}
 | 
			
		||||
| 
						 | 
				
			
			@ -71,4 +102,16 @@ defp get_query(user, timelines) do
 | 
			
		|||
    |> by_user_id(user.id)
 | 
			
		||||
    |> by_timeline(timelines)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp unread_count_query(query) do
 | 
			
		||||
    from(
 | 
			
		||||
      q in query,
 | 
			
		||||
      left_join: n in "notifications",
 | 
			
		||||
      on: n.user_id == q.user_id and n.seen == false,
 | 
			
		||||
      group_by: [:id],
 | 
			
		||||
      select_merge: %{
 | 
			
		||||
        unread_count: fragment("count(?)", n.id)
 | 
			
		||||
      }
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -5,8 +5,10 @@
 | 
			
		|||
defmodule Pleroma.Notification do
 | 
			
		||||
  use Ecto.Schema
 | 
			
		||||
 | 
			
		||||
  alias Ecto.Multi
 | 
			
		||||
  alias Pleroma.Activity
 | 
			
		||||
  alias Pleroma.FollowingRelationship
 | 
			
		||||
  alias Pleroma.Marker
 | 
			
		||||
  alias Pleroma.Notification
 | 
			
		||||
  alias Pleroma.Object
 | 
			
		||||
  alias Pleroma.Pagination
 | 
			
		||||
| 
						 | 
				
			
			@ -34,11 +36,30 @@ defmodule Pleroma.Notification do
 | 
			
		|||
    timestamps()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @spec unread_notifications_count(User.t()) :: integer()
 | 
			
		||||
  def unread_notifications_count(%User{id: user_id}) do
 | 
			
		||||
    from(q in __MODULE__,
 | 
			
		||||
      where: q.user_id == ^user_id and q.seen == false
 | 
			
		||||
    )
 | 
			
		||||
    |> Repo.aggregate(:count, :id)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def changeset(%Notification{} = notification, attrs) do
 | 
			
		||||
    notification
 | 
			
		||||
    |> cast(attrs, [:seen])
 | 
			
		||||
  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_relationships =
 | 
			
		||||
      [:block] ++
 | 
			
		||||
| 
						 | 
				
			
			@ -185,25 +206,23 @@ def for_user_since(user, date) do
 | 
			
		|||
    |> 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_last_read_id(user, "notifications")
 | 
			
		||||
      |> Repo.transaction()
 | 
			
		||||
 | 
			
		||||
    Notification
 | 
			
		||||
    |> where([n], n.id in ^notification_ids)
 | 
			
		||||
| 
						 | 
				
			
			@ -220,11 +239,18 @@ def set_read_up_to(%{id: user_id} = _user, id) do
 | 
			
		|||
    |> 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_last_read_id(user, "notifications")
 | 
			
		||||
      |> Repo.transaction()
 | 
			
		||||
      |> case do
 | 
			
		||||
        {:ok, %{update: notification}} -> {:ok, notification}
 | 
			
		||||
        {:error, :update, changeset, _} -> {:error, changeset}
 | 
			
		||||
      end
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -316,8 +342,11 @@ defp do_create_notifications(%Activity{} = activity) do
 | 
			
		|||
  # TODO move to sql, too.
 | 
			
		||||
  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)
 | 
			
		||||
      {:ok, %{notification: notification}} =
 | 
			
		||||
        Multi.new()
 | 
			
		||||
        |> Multi.insert(:notification, %Notification{user_id: user.id, activity: activity})
 | 
			
		||||
        |> Marker.multi_set_last_read_id(user, "notifications")
 | 
			
		||||
        |> Repo.transaction()
 | 
			
		||||
 | 
			
		||||
      if do_send do
 | 
			
		||||
        Streamer.stream(["user", "user:notification"], notification)
 | 
			
		||||
| 
						 | 
				
			
			@ -339,13 +368,7 @@ 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
 | 
			
		||||
    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()
 | 
			
		||||
    potential_receiver_ap_ids = get_potential_receiver_ap_ids(activity)
 | 
			
		||||
 | 
			
		||||
    potential_receivers = User.get_users_from_set(potential_receiver_ap_ids, local_only)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -363,6 +386,27 @@ def get_notified_from_activity(%Activity{data: %{"type" => type}} = activity, lo
 | 
			
		|||
 | 
			
		||||
  def get_notified_from_activity(_, _local_only), do: {[], []}
 | 
			
		||||
 | 
			
		||||
  # For some activities, only notify the author of the object
 | 
			
		||||
  def get_potential_receiver_ap_ids(%{data: %{"type" => type, "object" => object_id}})
 | 
			
		||||
      when type in ~w{Like Announce EmojiReact} do
 | 
			
		||||
    case Object.get_cached_by_ap_id(object_id) do
 | 
			
		||||
      %Object{data: %{"actor" => actor}} ->
 | 
			
		||||
        [actor]
 | 
			
		||||
 | 
			
		||||
      _ ->
 | 
			
		||||
        []
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def get_potential_receiver_ap_ids(activity) 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()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @doc "Filters out AP IDs domain-blocking and not following the activity's actor"
 | 
			
		||||
  def exclude_domain_blocker_ap_ids(ap_ids, activity, preloaded_users \\ [])
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -356,31 +356,6 @@ def update(%{to: to, cc: cc, actor: actor, object: object} = params) do
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @spec react_with_emoji(User.t(), Object.t(), String.t(), keyword()) ::
 | 
			
		||||
          {:ok, Activity.t(), Object.t()} | {:error, any()}
 | 
			
		||||
  def react_with_emoji(user, object, emoji, options \\ []) do
 | 
			
		||||
    with {:ok, result} <-
 | 
			
		||||
           Repo.transaction(fn -> do_react_with_emoji(user, object, emoji, options) end) do
 | 
			
		||||
      result
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp do_react_with_emoji(user, object, emoji, options) do
 | 
			
		||||
    with local <- Keyword.get(options, :local, true),
 | 
			
		||||
         activity_id <- Keyword.get(options, :activity_id, nil),
 | 
			
		||||
         true <- Pleroma.Emoji.is_unicode_emoji?(emoji),
 | 
			
		||||
         reaction_data <- make_emoji_reaction_data(user, object, emoji, activity_id),
 | 
			
		||||
         {:ok, activity} <- insert(reaction_data, local),
 | 
			
		||||
         {:ok, object} <- add_emoji_reaction_to_object(activity, object),
 | 
			
		||||
         _ <- notify_and_stream(activity),
 | 
			
		||||
         :ok <- maybe_federate(activity) do
 | 
			
		||||
      {:ok, activity, object}
 | 
			
		||||
    else
 | 
			
		||||
      false -> {:error, false}
 | 
			
		||||
      {:error, error} -> Repo.rollback(error)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @spec announce(User.t(), Object.t(), String.t() | nil, boolean(), boolean()) ::
 | 
			
		||||
          {:ok, Activity.t(), Object.t()} | {:error, any()}
 | 
			
		||||
  def announce(
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,6 +10,18 @@ defmodule Pleroma.Web.ActivityPub.Builder do
 | 
			
		|||
  alias Pleroma.Web.ActivityPub.Utils
 | 
			
		||||
  alias Pleroma.Web.ActivityPub.Visibility
 | 
			
		||||
 | 
			
		||||
  @spec emoji_react(User.t(), Object.t(), String.t()) :: {:ok, map(), keyword()}
 | 
			
		||||
  def emoji_react(actor, object, emoji) do
 | 
			
		||||
    with {:ok, data, meta} <- object_action(actor, object) do
 | 
			
		||||
      data =
 | 
			
		||||
        data
 | 
			
		||||
        |> Map.put("content", emoji)
 | 
			
		||||
        |> Map.put("type", "EmojiReact")
 | 
			
		||||
 | 
			
		||||
      {:ok, data, meta}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @spec undo(User.t(), Activity.t()) :: {:ok, map(), keyword()}
 | 
			
		||||
  def undo(actor, object) do
 | 
			
		||||
    {:ok,
 | 
			
		||||
| 
						 | 
				
			
			@ -52,6 +64,17 @@ def delete(actor, object_id) do
 | 
			
		|||
 | 
			
		||||
  @spec like(User.t(), Object.t()) :: {:ok, map(), keyword()}
 | 
			
		||||
  def like(actor, object) do
 | 
			
		||||
    with {:ok, data, meta} <- object_action(actor, object) do
 | 
			
		||||
      data =
 | 
			
		||||
        data
 | 
			
		||||
        |> Map.put("type", "Like")
 | 
			
		||||
 | 
			
		||||
      {:ok, data, meta}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  @spec object_action(User.t(), Object.t()) :: {:ok, map(), keyword()}
 | 
			
		||||
  defp object_action(actor, object) do
 | 
			
		||||
    object_actor = User.get_cached_by_ap_id(object.data["actor"])
 | 
			
		||||
 | 
			
		||||
    # Address the actor of the object, and our actor's follower collection if the post is public.
 | 
			
		||||
| 
						 | 
				
			
			@ -73,7 +96,6 @@ def like(actor, object) do
 | 
			
		|||
     %{
 | 
			
		||||
       "id" => Utils.generate_activity_id(),
 | 
			
		||||
       "actor" => actor.ap_id,
 | 
			
		||||
       "type" => "Like",
 | 
			
		||||
       "object" => object.data["id"],
 | 
			
		||||
       "to" => to,
 | 
			
		||||
       "cc" => cc,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,6 +12,7 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
 | 
			
		|||
  alias Pleroma.Object
 | 
			
		||||
  alias Pleroma.User
 | 
			
		||||
  alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
 | 
			
		||||
  alias Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator
 | 
			
		||||
  alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
 | 
			
		||||
  alias Pleroma.Web.ActivityPub.ObjectValidators.Types
 | 
			
		||||
  alias Pleroma.Web.ActivityPub.ObjectValidators.UndoValidator
 | 
			
		||||
| 
						 | 
				
			
			@ -47,6 +48,16 @@ def validate(%{"type" => "Like"} = object, meta) do
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def validate(%{"type" => "EmojiReact"} = object, meta) do
 | 
			
		||||
    with {:ok, object} <-
 | 
			
		||||
           object
 | 
			
		||||
           |> EmojiReactValidator.cast_and_validate()
 | 
			
		||||
           |> Ecto.Changeset.apply_action(:insert) do
 | 
			
		||||
      object = stringify_keys(object |> Map.from_struct())
 | 
			
		||||
      {:ok, object, meta}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def stringify_keys(%{__struct__: _} = object) do
 | 
			
		||||
    object
 | 
			
		||||
    |> Map.from_struct()
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,81 @@
 | 
			
		|||
# Pleroma: A lightweight social networking server
 | 
			
		||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
 | 
			
		||||
# SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
defmodule Pleroma.Web.ActivityPub.ObjectValidators.EmojiReactValidator do
 | 
			
		||||
  use Ecto.Schema
 | 
			
		||||
 | 
			
		||||
  alias Pleroma.Object
 | 
			
		||||
  alias Pleroma.Web.ActivityPub.ObjectValidators.Types
 | 
			
		||||
 | 
			
		||||
  import Ecto.Changeset
 | 
			
		||||
  import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
 | 
			
		||||
 | 
			
		||||
  @primary_key false
 | 
			
		||||
 | 
			
		||||
  embedded_schema do
 | 
			
		||||
    field(:id, Types.ObjectID, primary_key: true)
 | 
			
		||||
    field(:type, :string)
 | 
			
		||||
    field(:object, Types.ObjectID)
 | 
			
		||||
    field(:actor, Types.ObjectID)
 | 
			
		||||
    field(:context, :string)
 | 
			
		||||
    field(:content, :string)
 | 
			
		||||
    field(:to, {:array, :string}, default: [])
 | 
			
		||||
    field(:cc, {:array, :string}, default: [])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def cast_and_validate(data) do
 | 
			
		||||
    data
 | 
			
		||||
    |> cast_data()
 | 
			
		||||
    |> validate_data()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def cast_data(data) do
 | 
			
		||||
    %__MODULE__{}
 | 
			
		||||
    |> changeset(data)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def changeset(struct, data) do
 | 
			
		||||
    struct
 | 
			
		||||
    |> cast(data, __schema__(:fields))
 | 
			
		||||
    |> fix_after_cast()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def fix_after_cast(cng) do
 | 
			
		||||
    cng
 | 
			
		||||
    |> fix_context()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def fix_context(cng) do
 | 
			
		||||
    object = get_field(cng, :object)
 | 
			
		||||
 | 
			
		||||
    with nil <- get_field(cng, :context),
 | 
			
		||||
         %Object{data: %{"context" => context}} <- Object.get_cached_by_ap_id(object) do
 | 
			
		||||
      cng
 | 
			
		||||
      |> put_change(:context, context)
 | 
			
		||||
    else
 | 
			
		||||
      _ ->
 | 
			
		||||
        cng
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def validate_emoji(cng) do
 | 
			
		||||
    content = get_field(cng, :content)
 | 
			
		||||
 | 
			
		||||
    if Pleroma.Emoji.is_unicode_emoji?(content) do
 | 
			
		||||
      cng
 | 
			
		||||
    else
 | 
			
		||||
      cng
 | 
			
		||||
      |> add_error(:content, "must be a single character emoji")
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def validate_data(data_cng) do
 | 
			
		||||
    data_cng
 | 
			
		||||
    |> validate_inclusion(:type, ["EmojiReact"])
 | 
			
		||||
    |> validate_required([:id, :type, :object, :actor, :context, :to, :cc, :content])
 | 
			
		||||
    |> validate_actor_presence()
 | 
			
		||||
    |> validate_object_presence()
 | 
			
		||||
    |> validate_emoji()
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -34,6 +34,18 @@ def handle(%{data: %{"type" => "Undo", "object" => undone_object}} = object, met
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Tasks this handles:
 | 
			
		||||
  # - Add reaction to object
 | 
			
		||||
  # - Set up notification
 | 
			
		||||
  def handle(%{data: %{"type" => "EmojiReact"}} = object, meta) do
 | 
			
		||||
    reacted_object = Object.get_by_ap_id(object.data["object"])
 | 
			
		||||
    Utils.add_emoji_reaction_to_object(object, reacted_object)
 | 
			
		||||
 | 
			
		||||
    Notification.create_notifications(object)
 | 
			
		||||
 | 
			
		||||
    {:ok, object, meta}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  # Tasks this handles:
 | 
			
		||||
  # - Delete and unpins the create activity
 | 
			
		||||
  # - Replace object with Tombstone
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -656,7 +656,7 @@ def handle_incoming(
 | 
			
		|||
    |> handle_incoming(options)
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_incoming(%{"type" => "Like"} = data, _options) do
 | 
			
		||||
  def handle_incoming(%{"type" => type} = data, _options) when type in ["Like", "EmojiReact"] do
 | 
			
		||||
    with :ok <- ObjectValidator.fetch_actor_and_object(data),
 | 
			
		||||
         {:ok, activity, _meta} <-
 | 
			
		||||
           Pipeline.common_pipeline(data, local: false) do
 | 
			
		||||
| 
						 | 
				
			
			@ -666,27 +666,6 @@ def handle_incoming(%{"type" => "Like"} = data, _options) do
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_incoming(
 | 
			
		||||
        %{
 | 
			
		||||
          "type" => "EmojiReact",
 | 
			
		||||
          "object" => object_id,
 | 
			
		||||
          "actor" => _actor,
 | 
			
		||||
          "id" => id,
 | 
			
		||||
          "content" => emoji
 | 
			
		||||
        } = data,
 | 
			
		||||
        _options
 | 
			
		||||
      ) do
 | 
			
		||||
    with actor <- Containment.get_actor(data),
 | 
			
		||||
         {:ok, %User{} = actor} <- User.get_or_fetch_by_ap_id(actor),
 | 
			
		||||
         {:ok, object} <- get_obj_helper(object_id),
 | 
			
		||||
         {:ok, activity, _object} <-
 | 
			
		||||
           ActivityPub.react_with_emoji(actor, object, emoji, activity_id: id, local: false) do
 | 
			
		||||
      {:ok, activity}
 | 
			
		||||
    else
 | 
			
		||||
      _e -> :error
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def handle_incoming(
 | 
			
		||||
        %{"type" => "Announce", "object" => object_id, "actor" => _actor, "id" => id} = data,
 | 
			
		||||
        _options
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -191,8 +191,10 @@ def unfavorite(id, user) do
 | 
			
		|||
 | 
			
		||||
  def react_with_emoji(id, user, emoji) do
 | 
			
		||||
    with %Activity{} = activity <- Activity.get_by_id(id),
 | 
			
		||||
         object <- Object.normalize(activity) do
 | 
			
		||||
      ActivityPub.react_with_emoji(user, object, emoji)
 | 
			
		||||
         object <- Object.normalize(activity),
 | 
			
		||||
         {:ok, emoji_react, _} <- Builder.emoji_react(user, object, emoji),
 | 
			
		||||
         {:ok, activity, _} <- Pipeline.common_pipeline(emoji_react, local: true) do
 | 
			
		||||
      {:ok, activity}
 | 
			
		||||
    else
 | 
			
		||||
      _ ->
 | 
			
		||||
        {:error, dgettext("errors", "Could not add reaction emoji")}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -36,9 +36,11 @@ def render("index.json", %{users: users} = opts) do
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def render("show.json", %{user: user} = opts) do
 | 
			
		||||
    if User.visible_for?(user, opts[:for]),
 | 
			
		||||
      do: do_render("show.json", opts),
 | 
			
		||||
      else: %{}
 | 
			
		||||
    if User.visible_for?(user, opts[:for]) do
 | 
			
		||||
      do_render("show.json", opts)
 | 
			
		||||
    else
 | 
			
		||||
      %{}
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def render("mention.json", %{user: user}) do
 | 
			
		||||
| 
						 | 
				
			
			@ -221,7 +223,7 @@ defp do_render("show.json", %{user: user} = opts) do
 | 
			
		|||
      fields: user.fields,
 | 
			
		||||
      bot: bot,
 | 
			
		||||
      source: %{
 | 
			
		||||
        note: (user.bio || "") |> String.replace(~r(<br */?>), "\n") |> Pleroma.HTML.strip_tags(),
 | 
			
		||||
        note: prepare_user_bio(user),
 | 
			
		||||
        sensitive: false,
 | 
			
		||||
        fields: user.raw_fields,
 | 
			
		||||
        pleroma: %{
 | 
			
		||||
| 
						 | 
				
			
			@ -253,8 +255,17 @@ defp do_render("show.json", %{user: user} = opts) do
 | 
			
		|||
    |> maybe_put_follow_requests_count(user, opts[:for])
 | 
			
		||||
    |> maybe_put_allow_following_move(user, opts[:for])
 | 
			
		||||
    |> maybe_put_unread_conversation_count(user, opts[:for])
 | 
			
		||||
    |> maybe_put_unread_notification_count(user, opts[:for])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp prepare_user_bio(%User{bio: ""}), do: ""
 | 
			
		||||
 | 
			
		||||
  defp prepare_user_bio(%User{bio: bio}) when is_binary(bio) do
 | 
			
		||||
    bio |> String.replace(~r(<br */?>), "\n") |> Pleroma.HTML.strip_tags()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp prepare_user_bio(_), do: ""
 | 
			
		||||
 | 
			
		||||
  defp username_from_nickname(string) when is_binary(string) do
 | 
			
		||||
    hd(String.split(string, "@"))
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			@ -350,6 +361,16 @@ defp maybe_put_unread_conversation_count(data, %User{id: user_id} = user, %User{
 | 
			
		|||
 | 
			
		||||
  defp maybe_put_unread_conversation_count(data, _, _), do: data
 | 
			
		||||
 | 
			
		||||
  defp maybe_put_unread_notification_count(data, %User{id: user_id}, %User{id: user_id} = user) do
 | 
			
		||||
    Kernel.put_in(
 | 
			
		||||
      data,
 | 
			
		||||
      [:pleroma, :unread_notifications_count],
 | 
			
		||||
      Pleroma.Notification.unread_notifications_count(user)
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp maybe_put_unread_notification_count(data, _, _), do: data
 | 
			
		||||
 | 
			
		||||
  defp image_url(%{"url" => [%{"href" => href} | _]}), do: href
 | 
			
		||||
  defp image_url(_), do: nil
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,7 +11,10 @@ def render("markers.json", %{markers: markers}) do
 | 
			
		|||
       %{
 | 
			
		||||
         last_read_id: m.last_read_id,
 | 
			
		||||
         version: m.lock_version,
 | 
			
		||||
         updated_at: NaiveDateTime.to_iso8601(m.updated_at)
 | 
			
		||||
         updated_at: NaiveDateTime.to_iso8601(m.updated_at),
 | 
			
		||||
         pleroma: %{
 | 
			
		||||
           unread_count: m.unread_count
 | 
			
		||||
         }
 | 
			
		||||
       }}
 | 
			
		||||
    end)
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -86,7 +86,7 @@ def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id}
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  def react_with_emoji(%{assigns: %{user: user}} = conn, %{"id" => activity_id, "emoji" => emoji}) do
 | 
			
		||||
    with {:ok, _activity, _object} <- CommonAPI.react_with_emoji(activity_id, user, emoji),
 | 
			
		||||
    with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji),
 | 
			
		||||
         activity <- Activity.get_by_id(activity_id) do
 | 
			
		||||
      conn
 | 
			
		||||
      |> put_view(StatusView)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -106,14 +106,13 @@ def build_content(notification, actor, object, mastodon_type \\ nil)
 | 
			
		|||
 | 
			
		||||
  def build_content(
 | 
			
		||||
        %{
 | 
			
		||||
          activity: %{data: %{"directMessage" => true}},
 | 
			
		||||
          user: %{notification_settings: %{privacy_option: true}}
 | 
			
		||||
        },
 | 
			
		||||
        actor,
 | 
			
		||||
        } = notification,
 | 
			
		||||
        _actor,
 | 
			
		||||
        _object,
 | 
			
		||||
        _mastodon_type
 | 
			
		||||
        mastodon_type
 | 
			
		||||
      ) do
 | 
			
		||||
    %{title: "New Direct Message", body: "@#{actor.nickname}"}
 | 
			
		||||
    %{body: format_title(notification, mastodon_type)}
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def build_content(notification, actor, object, mastodon_type) do
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										40
									
								
								priv/repo/migrations/20200415181818_update_markers.exs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								priv/repo/migrations/20200415181818_update_markers.exs
									
									
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,40 @@
 | 
			
		|||
defmodule Pleroma.Repo.Migrations.UpdateMarkers do
 | 
			
		||||
  use Ecto.Migration
 | 
			
		||||
  import Ecto.Query
 | 
			
		||||
  alias Pleroma.Repo
 | 
			
		||||
 | 
			
		||||
  def up do
 | 
			
		||||
    update_markers()
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  def down do
 | 
			
		||||
    :ok
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  defp update_markers do
 | 
			
		||||
    now = NaiveDateTime.utc_now()
 | 
			
		||||
 | 
			
		||||
    markers_attrs =
 | 
			
		||||
      from(q in "notifications",
 | 
			
		||||
        select: %{
 | 
			
		||||
          timeline: "notifications",
 | 
			
		||||
          user_id: q.user_id,
 | 
			
		||||
          last_read_id:
 | 
			
		||||
            type(fragment("MAX( CASE WHEN seen = true THEN id ELSE null END )"), :string)
 | 
			
		||||
        },
 | 
			
		||||
        group_by: [q.user_id]
 | 
			
		||||
      )
 | 
			
		||||
      |> Repo.all()
 | 
			
		||||
      |> Enum.map(fn %{last_read_id: last_read_id} = attrs ->
 | 
			
		||||
        attrs
 | 
			
		||||
        |> Map.put(:last_read_id, last_read_id || "")
 | 
			
		||||
        |> Map.put_new(:inserted_at, now)
 | 
			
		||||
        |> Map.put_new(:updated_at, now)
 | 
			
		||||
      end)
 | 
			
		||||
 | 
			
		||||
    Repo.insert_all("markers", markers_attrs,
 | 
			
		||||
      on_conflict: {:replace, [:last_read_id]},
 | 
			
		||||
      conflict_target: [:user_id, :timeline]
 | 
			
		||||
    )
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								priv/static/adminfe/static/fonts/element-icons.535877f.woff
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								priv/static/adminfe/static/fonts/element-icons.535877f.woff
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								priv/static/adminfe/static/fonts/element-icons.732389d.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								priv/static/adminfe/static/fonts/element-icons.732389d.ttf
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
						 | 
				
			
			@ -8,12 +8,39 @@ defmodule Pleroma.MarkerTest do
 | 
			
		|||
 | 
			
		||||
  import Pleroma.Factory
 | 
			
		||||
 | 
			
		||||
  describe "multi_set_unread_count/3" do
 | 
			
		||||
    test "returns multi" do
 | 
			
		||||
      user = insert(:user)
 | 
			
		||||
 | 
			
		||||
      assert %Ecto.Multi{
 | 
			
		||||
               operations: [marker: {:run, _}, counters: {:run, _}]
 | 
			
		||||
             } =
 | 
			
		||||
               Marker.multi_set_last_read_id(
 | 
			
		||||
                 Ecto.Multi.new(),
 | 
			
		||||
                 user,
 | 
			
		||||
                 "notifications"
 | 
			
		||||
               )
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "return empty multi" do
 | 
			
		||||
      user = insert(:user)
 | 
			
		||||
      multi = Ecto.Multi.new()
 | 
			
		||||
      assert Marker.multi_set_last_read_id(multi, user, "home") == multi
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "get_markers/2" do
 | 
			
		||||
    test "returns user markers" do
 | 
			
		||||
      user = insert(:user)
 | 
			
		||||
      marker = insert(:marker, user: user)
 | 
			
		||||
      insert(:notification, user: user)
 | 
			
		||||
      insert(:notification, user: user)
 | 
			
		||||
      insert(:marker, timeline: "home", user: user)
 | 
			
		||||
      assert Marker.get_markers(user, ["notifications"]) == [refresh_record(marker)]
 | 
			
		||||
 | 
			
		||||
      assert Marker.get_markers(
 | 
			
		||||
               user,
 | 
			
		||||
               ["notifications"]
 | 
			
		||||
             ) == [%Marker{refresh_record(marker) | unread_count: 2}]
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,6 +12,8 @@ defmodule Pleroma.NotificationTest do
 | 
			
		|||
  alias Pleroma.Notification
 | 
			
		||||
  alias Pleroma.Tests.ObanHelpers
 | 
			
		||||
  alias Pleroma.User
 | 
			
		||||
  alias Pleroma.Web.ActivityPub.ActivityPub
 | 
			
		||||
  alias Pleroma.Web.ActivityPub.Builder
 | 
			
		||||
  alias Pleroma.Web.ActivityPub.Transmogrifier
 | 
			
		||||
  alias Pleroma.Web.CommonAPI
 | 
			
		||||
  alias Pleroma.Web.MastodonAPI.NotificationView
 | 
			
		||||
| 
						 | 
				
			
			@ -24,7 +26,7 @@ test "creates a notification for an emoji reaction" do
 | 
			
		|||
      other_user = insert(:user)
 | 
			
		||||
 | 
			
		||||
      {:ok, activity} = CommonAPI.post(user, %{"status" => "yeah"})
 | 
			
		||||
      {:ok, activity, _object} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
 | 
			
		||||
      {:ok, activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
 | 
			
		||||
 | 
			
		||||
      {:ok, [notification]} = Notification.create_notifications(activity)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -47,6 +49,9 @@ test "notifies someone when they are directly addressed" do
 | 
			
		|||
      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
 | 
			
		||||
| 
						 | 
				
			
			@ -466,6 +471,16 @@ test "it sets all notifications as read up to a specified notification ID" do
 | 
			
		|||
      assert n1.seen == true
 | 
			
		||||
      assert n2.seen == true
 | 
			
		||||
      assert n3.seen == false
 | 
			
		||||
 | 
			
		||||
      assert %Pleroma.Marker{} =
 | 
			
		||||
               m =
 | 
			
		||||
               Pleroma.Repo.get_by(
 | 
			
		||||
                 Pleroma.Marker,
 | 
			
		||||
                 user_id: other_user.id,
 | 
			
		||||
                 timeline: "notifications"
 | 
			
		||||
               )
 | 
			
		||||
 | 
			
		||||
      assert m.last_read_id == to_string(n2.id)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -601,6 +616,28 @@ test "it does not send notification to mentioned users in likes" do
 | 
			
		|||
      assert other_user not in enabled_receivers
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "it only notifies the post's author in likes" do
 | 
			
		||||
      user = insert(:user)
 | 
			
		||||
      other_user = insert(:user)
 | 
			
		||||
      third_user = insert(:user)
 | 
			
		||||
 | 
			
		||||
      {:ok, activity_one} =
 | 
			
		||||
        CommonAPI.post(user, %{
 | 
			
		||||
          "status" => "hey @#{other_user.nickname}!"
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
      {:ok, like_data, _} = Builder.like(third_user, activity_one.object)
 | 
			
		||||
 | 
			
		||||
      {:ok, like, _} =
 | 
			
		||||
        like_data
 | 
			
		||||
        |> Map.put("to", [other_user.ap_id | like_data["to"]])
 | 
			
		||||
        |> ActivityPub.persist(local: true)
 | 
			
		||||
 | 
			
		||||
      {enabled_receivers, _disabled_receivers} = Notification.get_notified_from_activity(like)
 | 
			
		||||
 | 
			
		||||
      assert other_user not in enabled_receivers
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "it does not send notification to mentioned users in announces" do
 | 
			
		||||
      user = insert(:user)
 | 
			
		||||
      other_user = insert(:user)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -16,7 +16,6 @@ defmodule Pleroma.Web.ActivityPub.ActivityPubTest do
 | 
			
		|||
  alias Pleroma.Web.ActivityPub.Utils
 | 
			
		||||
  alias Pleroma.Web.AdminAPI.AccountView
 | 
			
		||||
  alias Pleroma.Web.CommonAPI
 | 
			
		||||
  alias Pleroma.Web.Federator
 | 
			
		||||
 | 
			
		||||
  import ExUnit.CaptureLog
 | 
			
		||||
  import Mock
 | 
			
		||||
| 
						 | 
				
			
			@ -874,71 +873,6 @@ test "returns reblogs for users for whom reblogs have not been muted" do
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "react to an object" do
 | 
			
		||||
    test_with_mock "sends an activity to federation", Federator, [:passthrough], [] do
 | 
			
		||||
      Config.put([:instance, :federating], true)
 | 
			
		||||
      user = insert(:user)
 | 
			
		||||
      reactor = insert(:user)
 | 
			
		||||
      {:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
 | 
			
		||||
      assert object = Object.normalize(activity)
 | 
			
		||||
 | 
			
		||||
      {:ok, reaction_activity, _object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
 | 
			
		||||
 | 
			
		||||
      assert called(Federator.publish(reaction_activity))
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "adds an emoji reaction activity to the db" do
 | 
			
		||||
      user = insert(:user)
 | 
			
		||||
      reactor = insert(:user)
 | 
			
		||||
      third_user = insert(:user)
 | 
			
		||||
      fourth_user = insert(:user)
 | 
			
		||||
      {:ok, activity} = CommonAPI.post(user, %{"status" => "YASSSS queen slay"})
 | 
			
		||||
      assert object = Object.normalize(activity)
 | 
			
		||||
 | 
			
		||||
      {:ok, reaction_activity, object} = ActivityPub.react_with_emoji(reactor, object, "🔥")
 | 
			
		||||
 | 
			
		||||
      assert reaction_activity
 | 
			
		||||
 | 
			
		||||
      assert reaction_activity.data["actor"] == reactor.ap_id
 | 
			
		||||
      assert reaction_activity.data["type"] == "EmojiReact"
 | 
			
		||||
      assert reaction_activity.data["content"] == "🔥"
 | 
			
		||||
      assert reaction_activity.data["object"] == object.data["id"]
 | 
			
		||||
      assert reaction_activity.data["to"] == [User.ap_followers(reactor), activity.data["actor"]]
 | 
			
		||||
      assert reaction_activity.data["context"] == object.data["context"]
 | 
			
		||||
      assert object.data["reaction_count"] == 1
 | 
			
		||||
      assert object.data["reactions"] == [["🔥", [reactor.ap_id]]]
 | 
			
		||||
 | 
			
		||||
      {:ok, _reaction_activity, object} = ActivityPub.react_with_emoji(third_user, object, "☕")
 | 
			
		||||
 | 
			
		||||
      assert object.data["reaction_count"] == 2
 | 
			
		||||
      assert object.data["reactions"] == [["🔥", [reactor.ap_id]], ["☕", [third_user.ap_id]]]
 | 
			
		||||
 | 
			
		||||
      {:ok, _reaction_activity, object} = ActivityPub.react_with_emoji(fourth_user, object, "🔥")
 | 
			
		||||
 | 
			
		||||
      assert object.data["reaction_count"] == 3
 | 
			
		||||
 | 
			
		||||
      assert object.data["reactions"] == [
 | 
			
		||||
               ["🔥", [fourth_user.ap_id, reactor.ap_id]],
 | 
			
		||||
               ["☕", [third_user.ap_id]]
 | 
			
		||||
             ]
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "reverts emoji reaction on error" do
 | 
			
		||||
      [user, reactor] = insert_list(2, :user)
 | 
			
		||||
 | 
			
		||||
      {:ok, activity} = CommonAPI.post(user, %{"status" => "Status"})
 | 
			
		||||
      object = Object.normalize(activity)
 | 
			
		||||
 | 
			
		||||
      with_mock(Utils, [:passthrough], maybe_federate: fn _ -> {:error, :reverted} end) do
 | 
			
		||||
        assert {:error, :reverted} = ActivityPub.react_with_emoji(reactor, object, "😀")
 | 
			
		||||
      end
 | 
			
		||||
 | 
			
		||||
      object = Object.get_by_ap_id(object.data["id"])
 | 
			
		||||
      refute object.data["reaction_count"]
 | 
			
		||||
      refute object.data["reactions"]
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "announcing an object" do
 | 
			
		||||
    test "adds an announce activity to the db" do
 | 
			
		||||
      note_activity = insert(:note_activity)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -10,6 +10,46 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do
 | 
			
		|||
 | 
			
		||||
  import Pleroma.Factory
 | 
			
		||||
 | 
			
		||||
  describe "EmojiReacts" do
 | 
			
		||||
    setup do
 | 
			
		||||
      user = insert(:user)
 | 
			
		||||
      {:ok, post_activity} = CommonAPI.post(user, %{"status" => "uguu"})
 | 
			
		||||
 | 
			
		||||
      object = Pleroma.Object.get_by_ap_id(post_activity.data["object"])
 | 
			
		||||
 | 
			
		||||
      {:ok, valid_emoji_react, []} = Builder.emoji_react(user, object, "👌")
 | 
			
		||||
 | 
			
		||||
      %{user: user, post_activity: post_activity, valid_emoji_react: valid_emoji_react}
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "it validates a valid EmojiReact", %{valid_emoji_react: valid_emoji_react} do
 | 
			
		||||
      assert {:ok, _, _} = ObjectValidator.validate(valid_emoji_react, [])
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "it is not valid without a 'content' field", %{valid_emoji_react: valid_emoji_react} do
 | 
			
		||||
      without_content =
 | 
			
		||||
        valid_emoji_react
 | 
			
		||||
        |> Map.delete("content")
 | 
			
		||||
 | 
			
		||||
      {:error, cng} = ObjectValidator.validate(without_content, [])
 | 
			
		||||
 | 
			
		||||
      refute cng.valid?
 | 
			
		||||
      assert {:content, {"can't be blank", [validation: :required]}} in cng.errors
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "it is not valid with a non-emoji content field", %{valid_emoji_react: valid_emoji_react} do
 | 
			
		||||
      without_emoji_content =
 | 
			
		||||
        valid_emoji_react
 | 
			
		||||
        |> Map.put("content", "x")
 | 
			
		||||
 | 
			
		||||
      {:error, cng} = ObjectValidator.validate(without_emoji_content, [])
 | 
			
		||||
 | 
			
		||||
      refute cng.valid?
 | 
			
		||||
 | 
			
		||||
      assert {:content, {"must be a single character emoji", []}} in cng.errors
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "Undos" do
 | 
			
		||||
    setup do
 | 
			
		||||
      user = insert(:user)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -72,13 +72,40 @@ test "it handles user deletions", %{delete_user: delete, user: user} do
 | 
			
		|||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "EmojiReact objects" do
 | 
			
		||||
    setup do
 | 
			
		||||
      poster = insert(:user)
 | 
			
		||||
      user = insert(:user)
 | 
			
		||||
 | 
			
		||||
      {:ok, post} = CommonAPI.post(poster, %{"status" => "hey"})
 | 
			
		||||
 | 
			
		||||
      {:ok, emoji_react_data, []} = Builder.emoji_react(user, post.object, "👌")
 | 
			
		||||
      {:ok, emoji_react, _meta} = ActivityPub.persist(emoji_react_data, local: true)
 | 
			
		||||
 | 
			
		||||
      %{emoji_react: emoji_react, user: user, poster: poster}
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "adds the reaction to the object", %{emoji_react: emoji_react, user: user} do
 | 
			
		||||
      {:ok, emoji_react, _} = SideEffects.handle(emoji_react)
 | 
			
		||||
      object = Object.get_by_ap_id(emoji_react.data["object"])
 | 
			
		||||
 | 
			
		||||
      assert object.data["reaction_count"] == 1
 | 
			
		||||
      assert ["👌", [user.ap_id]] in object.data["reactions"]
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "creates a notification", %{emoji_react: emoji_react, poster: poster} do
 | 
			
		||||
      {:ok, emoji_react, _} = SideEffects.handle(emoji_react)
 | 
			
		||||
      assert Repo.get_by(Notification, user_id: poster.id, activity_id: emoji_react.id)
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "Undo objects" do
 | 
			
		||||
    setup do
 | 
			
		||||
      poster = insert(:user)
 | 
			
		||||
      user = insert(:user)
 | 
			
		||||
      {:ok, post} = CommonAPI.post(poster, %{"status" => "hey"})
 | 
			
		||||
      {:ok, like} = CommonAPI.favorite(user, post.id)
 | 
			
		||||
      {:ok, reaction, _} = CommonAPI.react_with_emoji(post.id, user, "👍")
 | 
			
		||||
      {:ok, reaction} = CommonAPI.react_with_emoji(post.id, user, "👍")
 | 
			
		||||
      {:ok, announce, _} = CommonAPI.repeat(post.id, user)
 | 
			
		||||
      {:ok, block} = ActivityPub.block(user, poster)
 | 
			
		||||
      User.block(user, poster)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,61 @@
 | 
			
		|||
# Pleroma: A lightweight social networking server
 | 
			
		||||
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
 | 
			
		||||
# SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
defmodule Pleroma.Web.ActivityPub.Transmogrifier.EmojiReactHandlingTest do
 | 
			
		||||
  use Pleroma.DataCase
 | 
			
		||||
 | 
			
		||||
  alias Pleroma.Activity
 | 
			
		||||
  alias Pleroma.Object
 | 
			
		||||
  alias Pleroma.Web.ActivityPub.Transmogrifier
 | 
			
		||||
  alias Pleroma.Web.CommonAPI
 | 
			
		||||
 | 
			
		||||
  import Pleroma.Factory
 | 
			
		||||
 | 
			
		||||
  test "it works for incoming emoji reactions" do
 | 
			
		||||
    user = insert(:user)
 | 
			
		||||
    other_user = insert(:user, local: false)
 | 
			
		||||
    {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
 | 
			
		||||
 | 
			
		||||
    data =
 | 
			
		||||
      File.read!("test/fixtures/emoji-reaction.json")
 | 
			
		||||
      |> Poison.decode!()
 | 
			
		||||
      |> Map.put("object", activity.data["object"])
 | 
			
		||||
      |> Map.put("actor", other_user.ap_id)
 | 
			
		||||
 | 
			
		||||
    {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
 | 
			
		||||
 | 
			
		||||
    assert data["actor"] == other_user.ap_id
 | 
			
		||||
    assert data["type"] == "EmojiReact"
 | 
			
		||||
    assert data["id"] == "http://mastodon.example.org/users/admin#reactions/2"
 | 
			
		||||
    assert data["object"] == activity.data["object"]
 | 
			
		||||
    assert data["content"] == "👌"
 | 
			
		||||
 | 
			
		||||
    object = Object.get_by_ap_id(data["object"])
 | 
			
		||||
 | 
			
		||||
    assert object.data["reaction_count"] == 1
 | 
			
		||||
    assert match?([["👌", _]], object.data["reactions"])
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  test "it reject invalid emoji reactions" do
 | 
			
		||||
    user = insert(:user)
 | 
			
		||||
    other_user = insert(:user, local: false)
 | 
			
		||||
    {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
 | 
			
		||||
 | 
			
		||||
    data =
 | 
			
		||||
      File.read!("test/fixtures/emoji-reaction-too-long.json")
 | 
			
		||||
      |> Poison.decode!()
 | 
			
		||||
      |> Map.put("object", activity.data["object"])
 | 
			
		||||
      |> Map.put("actor", other_user.ap_id)
 | 
			
		||||
 | 
			
		||||
    assert {:error, _} = Transmogrifier.handle_incoming(data)
 | 
			
		||||
 | 
			
		||||
    data =
 | 
			
		||||
      File.read!("test/fixtures/emoji-reaction-no-emoji.json")
 | 
			
		||||
      |> Poison.decode!()
 | 
			
		||||
      |> Map.put("object", activity.data["object"])
 | 
			
		||||
      |> Map.put("actor", other_user.ap_id)
 | 
			
		||||
 | 
			
		||||
    assert {:error, _} = Transmogrifier.handle_incoming(data)
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			@ -17,7 +17,7 @@ test "it works for incoming emoji reaction undos" do
 | 
			
		|||
    user = insert(:user)
 | 
			
		||||
 | 
			
		||||
    {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
 | 
			
		||||
    {:ok, reaction_activity, _object} = CommonAPI.react_with_emoji(activity.id, user, "👌")
 | 
			
		||||
    {:ok, reaction_activity} = CommonAPI.react_with_emoji(activity.id, user, "👌")
 | 
			
		||||
 | 
			
		||||
    data =
 | 
			
		||||
      File.read!("test/fixtures/mastodon-undo-like.json")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -325,43 +325,6 @@ test "it cleans up incoming notices which are not really DMs" do
 | 
			
		|||
      assert object_data["cc"] == to
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "it works for incoming emoji reactions" do
 | 
			
		||||
      user = insert(:user)
 | 
			
		||||
      {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
 | 
			
		||||
 | 
			
		||||
      data =
 | 
			
		||||
        File.read!("test/fixtures/emoji-reaction.json")
 | 
			
		||||
        |> Poison.decode!()
 | 
			
		||||
        |> Map.put("object", activity.data["object"])
 | 
			
		||||
 | 
			
		||||
      {:ok, %Activity{data: data, local: false}} = Transmogrifier.handle_incoming(data)
 | 
			
		||||
 | 
			
		||||
      assert data["actor"] == "http://mastodon.example.org/users/admin"
 | 
			
		||||
      assert data["type"] == "EmojiReact"
 | 
			
		||||
      assert data["id"] == "http://mastodon.example.org/users/admin#reactions/2"
 | 
			
		||||
      assert data["object"] == activity.data["object"]
 | 
			
		||||
      assert data["content"] == "👌"
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "it reject invalid emoji reactions" do
 | 
			
		||||
      user = insert(:user)
 | 
			
		||||
      {:ok, activity} = CommonAPI.post(user, %{"status" => "hello"})
 | 
			
		||||
 | 
			
		||||
      data =
 | 
			
		||||
        File.read!("test/fixtures/emoji-reaction-too-long.json")
 | 
			
		||||
        |> Poison.decode!()
 | 
			
		||||
        |> Map.put("object", activity.data["object"])
 | 
			
		||||
 | 
			
		||||
      assert :error = Transmogrifier.handle_incoming(data)
 | 
			
		||||
 | 
			
		||||
      data =
 | 
			
		||||
        File.read!("test/fixtures/emoji-reaction-no-emoji.json")
 | 
			
		||||
        |> Poison.decode!()
 | 
			
		||||
        |> Map.put("object", activity.data["object"])
 | 
			
		||||
 | 
			
		||||
      assert :error = Transmogrifier.handle_incoming(data)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "it works for incoming announces" do
 | 
			
		||||
      data = File.read!("test/fixtures/mastodon-announce.json") |> Poison.decode!()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -518,7 +481,7 @@ test "it strips internal likes" do
 | 
			
		|||
    test "it strips internal reactions" do
 | 
			
		||||
      user = insert(:user)
 | 
			
		||||
      {:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"})
 | 
			
		||||
      {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, user, "📢")
 | 
			
		||||
      {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "📢")
 | 
			
		||||
 | 
			
		||||
      %{object: object} = Activity.get_by_id_with_object(activity.id)
 | 
			
		||||
      assert Map.has_key?(object.data, "reactions")
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -358,7 +358,7 @@ test "reacting to a status with an emoji" do
 | 
			
		|||
 | 
			
		||||
      {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
 | 
			
		||||
 | 
			
		||||
      {:ok, reaction, _} = CommonAPI.react_with_emoji(activity.id, user, "👍")
 | 
			
		||||
      {:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍")
 | 
			
		||||
 | 
			
		||||
      assert reaction.data["actor"] == user.ap_id
 | 
			
		||||
      assert reaction.data["content"] == "👍"
 | 
			
		||||
| 
						 | 
				
			
			@ -373,7 +373,7 @@ test "unreacting to a status with an emoji" do
 | 
			
		|||
      other_user = insert(:user)
 | 
			
		||||
 | 
			
		||||
      {:ok, activity} = CommonAPI.post(other_user, %{"status" => "cofe"})
 | 
			
		||||
      {:ok, reaction, _} = CommonAPI.react_with_emoji(activity.id, user, "👍")
 | 
			
		||||
      {:ok, reaction} = CommonAPI.react_with_emoji(activity.id, user, "👍")
 | 
			
		||||
 | 
			
		||||
      {:ok, unreaction} = CommonAPI.unreact_with_emoji(activity.id, user, "👍")
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1196,12 +1196,15 @@ test "returns lists to which the account belongs" do
 | 
			
		|||
  describe "verify_credentials" do
 | 
			
		||||
    test "verify_credentials" do
 | 
			
		||||
      %{user: user, conn: conn} = oauth_access(["read:accounts"])
 | 
			
		||||
      [notification | _] = insert_list(7, :notification, user: user)
 | 
			
		||||
      Pleroma.Notification.set_read_up_to(user, notification.id)
 | 
			
		||||
      conn = get(conn, "/api/v1/accounts/verify_credentials")
 | 
			
		||||
 | 
			
		||||
      response = json_response_and_validate_schema(conn, 200)
 | 
			
		||||
 | 
			
		||||
      assert %{"id" => id, "source" => %{"privacy" => "public"}} = response
 | 
			
		||||
      assert response["pleroma"]["chat_token"]
 | 
			
		||||
      assert response["pleroma"]["unread_notifications_count"] == 6
 | 
			
		||||
      assert id == to_string(user.id)
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -11,6 +11,7 @@ defmodule Pleroma.Web.MastodonAPI.MarkerControllerTest do
 | 
			
		|||
    test "gets markers with correct scopes", %{conn: conn} do
 | 
			
		||||
      user = insert(:user)
 | 
			
		||||
      token = insert(:oauth_token, user: user, scopes: ["read:statuses"])
 | 
			
		||||
      insert_list(7, :notification, user: user)
 | 
			
		||||
 | 
			
		||||
      {:ok, %{"notifications" => marker}} =
 | 
			
		||||
        Pleroma.Marker.upsert(
 | 
			
		||||
| 
						 | 
				
			
			@ -29,7 +30,8 @@ test "gets markers with correct scopes", %{conn: conn} do
 | 
			
		|||
               "notifications" => %{
 | 
			
		||||
                 "last_read_id" => "69420",
 | 
			
		||||
                 "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at),
 | 
			
		||||
                 "version" => 0
 | 
			
		||||
                 "version" => 0,
 | 
			
		||||
                 "pleroma" => %{"unread_count" => 7}
 | 
			
		||||
               }
 | 
			
		||||
             }
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			@ -71,7 +73,8 @@ test "creates a marker with correct scopes", %{conn: conn} do
 | 
			
		|||
               "notifications" => %{
 | 
			
		||||
                 "last_read_id" => "69420",
 | 
			
		||||
                 "updated_at" => _,
 | 
			
		||||
                 "version" => 0
 | 
			
		||||
                 "version" => 0,
 | 
			
		||||
                 "pleroma" => %{"unread_count" => 0}
 | 
			
		||||
               }
 | 
			
		||||
             } = response
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			@ -101,7 +104,8 @@ test "updates exist marker", %{conn: conn} do
 | 
			
		|||
               "notifications" => %{
 | 
			
		||||
                 "last_read_id" => "69888",
 | 
			
		||||
                 "updated_at" => NaiveDateTime.to_iso8601(marker.updated_at),
 | 
			
		||||
                 "version" => 0
 | 
			
		||||
                 "version" => 0,
 | 
			
		||||
                 "pleroma" => %{"unread_count" => 0}
 | 
			
		||||
               }
 | 
			
		||||
             }
 | 
			
		||||
    end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -466,6 +466,24 @@ test "shows unread_conversation_count only to the account owner" do
 | 
			
		|||
               :unread_conversation_count
 | 
			
		||||
             ] == 1
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "shows unread_count only to the account owner" do
 | 
			
		||||
      user = insert(:user)
 | 
			
		||||
      insert_list(7, :notification, user: user)
 | 
			
		||||
      other_user = insert(:user)
 | 
			
		||||
 | 
			
		||||
      user = User.get_cached_by_ap_id(user.ap_id)
 | 
			
		||||
 | 
			
		||||
      assert AccountView.render(
 | 
			
		||||
               "show.json",
 | 
			
		||||
               %{user: user, for: other_user}
 | 
			
		||||
             )[:pleroma][:unread_notifications_count] == nil
 | 
			
		||||
 | 
			
		||||
      assert AccountView.render(
 | 
			
		||||
               "show.json",
 | 
			
		||||
               %{user: user, for: user}
 | 
			
		||||
             )[:pleroma][:unread_notifications_count] == 7
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
 | 
			
		||||
  describe "follow requests counter" do
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -8,19 +8,21 @@ defmodule Pleroma.Web.MastodonAPI.MarkerViewTest do
 | 
			
		|||
  import Pleroma.Factory
 | 
			
		||||
 | 
			
		||||
  test "returns markers" do
 | 
			
		||||
    marker1 = insert(:marker, timeline: "notifications", last_read_id: "17")
 | 
			
		||||
    marker1 = insert(:marker, timeline: "notifications", last_read_id: "17", unread_count: 5)
 | 
			
		||||
    marker2 = insert(:marker, timeline: "home", last_read_id: "42")
 | 
			
		||||
 | 
			
		||||
    assert MarkerView.render("markers.json", %{markers: [marker1, marker2]}) == %{
 | 
			
		||||
             "home" => %{
 | 
			
		||||
               last_read_id: "42",
 | 
			
		||||
               updated_at: NaiveDateTime.to_iso8601(marker2.updated_at),
 | 
			
		||||
               version: 0
 | 
			
		||||
               version: 0,
 | 
			
		||||
               pleroma: %{unread_count: 0}
 | 
			
		||||
             },
 | 
			
		||||
             "notifications" => %{
 | 
			
		||||
               last_read_id: "17",
 | 
			
		||||
               updated_at: NaiveDateTime.to_iso8601(marker1.updated_at),
 | 
			
		||||
               version: 0
 | 
			
		||||
               version: 0,
 | 
			
		||||
               pleroma: %{unread_count: 5}
 | 
			
		||||
             }
 | 
			
		||||
           }
 | 
			
		||||
  end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -156,7 +156,7 @@ test "EmojiReact notification" do
 | 
			
		|||
    other_user = insert(:user)
 | 
			
		||||
 | 
			
		||||
    {:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"})
 | 
			
		||||
    {:ok, _activity, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
 | 
			
		||||
    {:ok, _activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
 | 
			
		||||
 | 
			
		||||
    activity = Repo.get(Activity, activity.id)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -32,9 +32,9 @@ test "has an emoji reaction list" do
 | 
			
		|||
    third_user = insert(:user)
 | 
			
		||||
    {:ok, activity} = CommonAPI.post(user, %{"status" => "dae cofe??"})
 | 
			
		||||
 | 
			
		||||
    {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, user, "☕")
 | 
			
		||||
    {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, third_user, "🍵")
 | 
			
		||||
    {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
 | 
			
		||||
    {:ok, _} = CommonAPI.react_with_emoji(activity.id, user, "☕")
 | 
			
		||||
    {:ok, _} = CommonAPI.react_with_emoji(activity.id, third_user, "🍵")
 | 
			
		||||
    {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
 | 
			
		||||
    activity = Repo.get(Activity, activity.id)
 | 
			
		||||
    status = StatusView.render("show.json", activity: activity)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,7 +43,7 @@ test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
 | 
			
		|||
    other_user = insert(:user)
 | 
			
		||||
 | 
			
		||||
    {:ok, activity} = CommonAPI.post(user, %{"status" => "#cofe"})
 | 
			
		||||
    {:ok, _reaction, _object} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
 | 
			
		||||
    {:ok, _reaction_activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
 | 
			
		||||
 | 
			
		||||
    ObanHelpers.perform_all()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -77,8 +77,8 @@ test "GET /api/v1/pleroma/statuses/:id/reactions", %{conn: conn} do
 | 
			
		|||
 | 
			
		||||
    assert result == []
 | 
			
		||||
 | 
			
		||||
    {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
 | 
			
		||||
    {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, doomed_user, "🎅")
 | 
			
		||||
    {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
 | 
			
		||||
    {:ok, _} = CommonAPI.react_with_emoji(activity.id, doomed_user, "🎅")
 | 
			
		||||
 | 
			
		||||
    User.perform(:delete, doomed_user)
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -115,8 +115,8 @@ test "GET /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
 | 
			
		|||
 | 
			
		||||
    assert result == []
 | 
			
		||||
 | 
			
		||||
    {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
 | 
			
		||||
    {:ok, _, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
 | 
			
		||||
    {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
 | 
			
		||||
    {:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")
 | 
			
		||||
 | 
			
		||||
    result =
 | 
			
		||||
      conn
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -193,7 +193,7 @@ test "renders title for create activity with direct visibility" do
 | 
			
		|||
  end
 | 
			
		||||
 | 
			
		||||
  describe "build_content/3" do
 | 
			
		||||
    test "returns info content for direct message with enabled privacy option" do
 | 
			
		||||
    test "hides details for notifications when privacy option enabled" do
 | 
			
		||||
      user = insert(:user, nickname: "Bob")
 | 
			
		||||
      user2 = insert(:user, nickname: "Rob", notification_settings: %{privacy_option: true})
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -209,12 +209,37 @@ test "returns info content for direct message with enabled privacy option" do
 | 
			
		|||
      object = Object.normalize(activity)
 | 
			
		||||
 | 
			
		||||
      assert Impl.build_content(notif, actor, object) == %{
 | 
			
		||||
               body: "@Bob",
 | 
			
		||||
               title: "New Direct Message"
 | 
			
		||||
               body: "New Direct Message"
 | 
			
		||||
             }
 | 
			
		||||
 | 
			
		||||
      {:ok, activity} =
 | 
			
		||||
        CommonAPI.post(user, %{
 | 
			
		||||
          "visibility" => "public",
 | 
			
		||||
          "status" => "<Lorem ipsum dolor sit amet."
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
      notif = insert(:notification, user: user2, activity: activity)
 | 
			
		||||
 | 
			
		||||
      actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
 | 
			
		||||
      object = Object.normalize(activity)
 | 
			
		||||
 | 
			
		||||
      assert Impl.build_content(notif, actor, object) == %{
 | 
			
		||||
               body: "New Mention"
 | 
			
		||||
             }
 | 
			
		||||
 | 
			
		||||
      {:ok, activity} = CommonAPI.favorite(user, activity.id)
 | 
			
		||||
 | 
			
		||||
      notif = insert(:notification, user: user2, activity: activity)
 | 
			
		||||
 | 
			
		||||
      actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
 | 
			
		||||
      object = Object.normalize(activity)
 | 
			
		||||
 | 
			
		||||
      assert Impl.build_content(notif, actor, object) == %{
 | 
			
		||||
               body: "New Favorite"
 | 
			
		||||
             }
 | 
			
		||||
    end
 | 
			
		||||
 | 
			
		||||
    test "returns regular content for direct message with disabled privacy option" do
 | 
			
		||||
    test "returns regular content for notifications with privacy option disabled" do
 | 
			
		||||
      user = insert(:user, nickname: "Bob")
 | 
			
		||||
      user2 = insert(:user, nickname: "Rob", notification_settings: %{privacy_option: false})
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -235,6 +260,36 @@ test "returns regular content for direct message with disabled privacy option" d
 | 
			
		|||
                 "@Bob: Lorem ipsum dolor sit amet, consectetur  adipiscing elit. Fusce sagittis fini...",
 | 
			
		||||
               title: "New Direct Message"
 | 
			
		||||
             }
 | 
			
		||||
 | 
			
		||||
      {:ok, activity} =
 | 
			
		||||
        CommonAPI.post(user, %{
 | 
			
		||||
          "visibility" => "public",
 | 
			
		||||
          "status" =>
 | 
			
		||||
            "<span>Lorem ipsum dolor sit amet</span>, consectetur :firefox: adipiscing elit. Fusce sagittis finibus turpis."
 | 
			
		||||
        })
 | 
			
		||||
 | 
			
		||||
      notif = insert(:notification, user: user2, activity: activity)
 | 
			
		||||
 | 
			
		||||
      actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
 | 
			
		||||
      object = Object.normalize(activity)
 | 
			
		||||
 | 
			
		||||
      assert Impl.build_content(notif, actor, object) == %{
 | 
			
		||||
               body:
 | 
			
		||||
                 "@Bob: Lorem ipsum dolor sit amet, consectetur  adipiscing elit. Fusce sagittis fini...",
 | 
			
		||||
               title: "New Mention"
 | 
			
		||||
             }
 | 
			
		||||
 | 
			
		||||
      {:ok, activity} = CommonAPI.favorite(user, activity.id)
 | 
			
		||||
 | 
			
		||||
      notif = insert(:notification, user: user2, activity: activity)
 | 
			
		||||
 | 
			
		||||
      actor = User.get_cached_by_ap_id(notif.activity.data["actor"])
 | 
			
		||||
      object = Object.normalize(activity)
 | 
			
		||||
 | 
			
		||||
      assert Impl.build_content(notif, actor, object) == %{
 | 
			
		||||
               body: "@Bob has favorited your post",
 | 
			
		||||
               title: "New Favorite"
 | 
			
		||||
             }
 | 
			
		||||
    end
 | 
			
		||||
  end
 | 
			
		||||
end
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in a new issue