Browse Source

Merge branch 'develop' of git.pleroma.social:pleroma/pleroma into remake-remodel-dms

1570-levenshtein-distance-user-search
lain 4 years ago
parent
commit
578ed3a37f
25 changed files with 1020 additions and 581 deletions
  1. +1
    -0
      CHANGELOG.md
  2. +2
    -2
      docs/API/pleroma_api.md
  3. +9
    -10
      docs/administration/CLI_tasks/user.md
  4. +5
    -0
      docs/configuration/cheatsheet.md
  5. +5
    -15
      lib/mix/tasks/pleroma/user.ex
  6. +15
    -5
      lib/pleroma/user.ex
  7. +97
    -0
      lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex
  8. +102
    -0
      lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex
  9. +1
    -1
      lib/pleroma/web/api_spec/operations/notification_operation.ex
  10. +106
    -0
      lib/pleroma/web/api_spec/operations/pleroma_conversation_operation.ex
  11. +42
    -0
      lib/pleroma/web/api_spec/operations/pleroma_notification_operation.ex
  12. +95
    -0
      lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex
  13. +61
    -0
      lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex
  14. +36
    -0
      lib/pleroma/web/pleroma_api/controllers/notification_controller.ex
  15. +0
    -220
      lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex
  16. +33
    -0
      lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex
  17. +9
    -17
      lib/pleroma/web/router.ex
  18. +1
    -1
      test/support/api_spec_helpers.ex
  19. +4
    -0
      test/support/http_request_mock.ex
  20. +8
    -8
      test/tasks/user_test.exs
  21. +64
    -0
      test/web/activity_pub/mrf/steal_emoji_policy_test.exs
  22. +136
    -0
      test/web/pleroma_api/controllers/conversation_controller_test.exs
  23. +125
    -0
      test/web/pleroma_api/controllers/emoji_reaction_controller_test.exs
  24. +63
    -0
      test/web/pleroma_api/controllers/notification_controller_test.exs
  25. +0
    -302
      test/web/pleroma_api/controllers/pleroma_api_controller_test.exs

+ 1
- 0
CHANGELOG.md View File

@@ -25,6 +25,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Mix task to create trusted OAuth App.
- Notifications: Added `follow_request` notification type.
- Added `:reject_deletes` group to SimplePolicy
- MRF (`EmojiStealPolicy`): New MRF Policy which allows to automatically download emojis from remote instances
<details>
<summary>API Changes</summary>
- Mastodon API: Extended `/api/v1/instance`.


+ 2
- 2
docs/API/pleroma_api.md View File

@@ -358,7 +358,7 @@ The status posting endpoint takes an additional parameter, `in_reply_to_conversa
* `recipients`: A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though.
* Response: JSON, statuses (200 - healthy, 503 unhealthy)

## `GET /api/v1/pleroma/conversations/read`
## `POST /api/v1/pleroma/conversations/read`
### Marks all user's conversations as read.
* Method `POST`
* Authentication: required
@@ -536,7 +536,7 @@ Emoji reactions work a lot like favourites do. They make it possible to react to
```

## `GET /api/v1/pleroma/statuses/:id/reactions/:emoji`
### Get an object of emoji to account mappings with accounts that reacted to the post for a specific emoji`
### Get an object of emoji to account mappings with accounts that reacted to the post for a specific emoji
* Method: `GET`
* Authentication: optional
* Params: None


+ 9
- 10
docs/administration/CLI_tasks/user.md View File

@@ -95,33 +95,33 @@ mix pleroma.user sign_out <nickname>
```


## Deactivate or activate a user
## Deactivate or activate a user
```sh tab="OTP"
./bin/pleroma_ctl user toggle_activated <nickname>
./bin/pleroma_ctl user toggle_activated <nickname>
```

```sh tab="From Source"
mix pleroma.user toggle_activated <nickname>
mix pleroma.user toggle_activated <nickname>
```


## Unsubscribe local users from a user and deactivate the user
## Deactivate a user and unsubscribes local users from the user
```sh tab="OTP"
./bin/pleroma_ctl user unsubscribe NICKNAME
./bin/pleroma_ctl user deactivate NICKNAME
```

```sh tab="From Source"
mix pleroma.user unsubscribe NICKNAME
mix pleroma.user deactivate NICKNAME
```


## Unsubscribe local users from an instance and deactivate all accounts on it
## Deactivate all accounts from an instance and unsubscribe local users on it
```sh tab="OTP"
./bin/pleroma_ctl user unsubscribe_all_from_instance <instance>
./bin/pleroma_ctl user deactivate_all_from_instance <instance>
```

```sh tab="From Source"
mix pleroma.user unsubscribe_all_from_instance <instance>
mix pleroma.user deactivate_all_from_instance <instance>
```


@@ -177,4 +177,3 @@ mix pleroma.user untag <nickname> <tags>
```sh tab="From Source"
mix pleroma.user toggle_confirmed <nickname>
```


+ 5
- 0
docs/configuration/cheatsheet.md View File

@@ -149,6 +149,11 @@ config :pleroma, :mrf_user_allowlist,
* `:strip_followers` removes followers from the ActivityPub recipient list, ensuring they won't be delivered to home timelines
* `:reject` rejects the message entirely

#### mrf_steal_emoji
* `hosts`: List of hosts to steal emojis from
* `rejected_shortcodes`: Regex-list of shortcodes to reject
* `size_limit`: File size limit (in bytes), checked before an emoji is saved to the disk

### :activitypub
* `unfollow_blocked`: Whether blocks result in people getting unfollowed
* `outgoing_blocks`: Whether to federate blocks to other instances


+ 5
- 15
lib/mix/tasks/pleroma/user.ex View File

@@ -144,28 +144,18 @@ defmodule Mix.Tasks.Pleroma.User do
end
end

def run(["unsubscribe", nickname]) do
def run(["deactivate", nickname]) do
start_pleroma()

with %User{} = user <- User.get_cached_by_nickname(nickname) do
shell_info("Deactivating #{user.nickname}")
User.deactivate(user)

user
|> User.get_friends()
|> Enum.each(fn friend ->
user = User.get_cached_by_id(user.id)

shell_info("Unsubscribing #{friend.nickname} from #{user.nickname}")
User.unfollow(user, friend)
end)

:timer.sleep(500)

user = User.get_cached_by_id(user.id)

if Enum.empty?(User.get_friends(user)) do
shell_info("Successfully unsubscribed all followers from #{user.nickname}")
if Enum.empty?(Enum.filter(User.get_friends(user), & &1.local)) do
shell_info("Successfully unsubscribed all local followers from #{user.nickname}")
end
else
_ ->
@@ -173,7 +163,7 @@ defmodule Mix.Tasks.Pleroma.User do
end
end

def run(["unsubscribe_all_from_instance", instance]) do
def run(["deactivate_all_from_instance", instance]) do
start_pleroma()

Pleroma.User.Query.build(%{nickname: "@#{instance}"})
@@ -181,7 +171,7 @@ defmodule Mix.Tasks.Pleroma.User do
|> Stream.each(fn users ->
users
|> Enum.each(fn user ->
run(["unsubscribe", user.nickname])
run(["deactivate", user.nickname])
end)
end)
|> Stream.run()


+ 15
- 5
lib/pleroma/user.ex View File

@@ -749,7 +749,19 @@ defmodule Pleroma.User do
{:error, "Not subscribed!"}
end

@spec unfollow(User.t(), User.t()) :: {:ok, User.t(), Activity.t()} | {:error, String.t()}
def unfollow(%User{} = follower, %User{} = followed) do
case do_unfollow(follower, followed) do
{:ok, follower, followed} ->
{:ok, follower, Utils.fetch_latest_follow(follower, followed)}

error ->
error
end
end

@spec do_unfollow(User.t(), User.t()) :: {:ok, User.t(), User.t()} | {:error, String.t()}
defp do_unfollow(%User{} = follower, %User{} = followed) do
case get_follow_state(follower, followed) do
state when state in [:follow_pending, :follow_accept] ->
FollowingRelationship.unfollow(follower, followed)
@@ -760,7 +772,7 @@ defmodule Pleroma.User do
|> update_following_count()
|> set_cache()

{:ok, follower, Utils.fetch_latest_follow(follower, followed)}
{:ok, follower, followed}

nil ->
{:error, "Not subscribed!"}
@@ -1402,15 +1414,13 @@ defmodule Pleroma.User do
user
|> get_followers()
|> Enum.filter(& &1.local)
|> Enum.each(fn follower ->
follower |> update_following_count() |> set_cache()
end)
|> Enum.each(&set_cache(update_following_count(&1)))

# Only update local user counts, remote will be update during the next pull.
user
|> get_friends()
|> Enum.filter(& &1.local)
|> Enum.each(&update_follower_count/1)
|> Enum.each(&do_unfollow(user, &1))

{:ok, user}
end


+ 97
- 0
lib/pleroma/web/activity_pub/mrf/steal_emoji_policy.ex View File

@@ -0,0 +1,97 @@
# 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.MRF.StealEmojiPolicy do
require Logger

alias Pleroma.Config

@moduledoc "Detect new emojis by their shortcode and steals them"
@behaviour Pleroma.Web.ActivityPub.MRF

defp remote_host?(host), do: host != Config.get([Pleroma.Web.Endpoint, :url, :host])

defp accept_host?(host), do: host in Config.get([:mrf_steal_emoji, :hosts], [])

defp steal_emoji({shortcode, url}) do
url = Pleroma.Web.MediaProxy.url(url)
{:ok, response} = Pleroma.HTTP.get(url)
size_limit = Config.get([:mrf_steal_emoji, :size_limit], 50_000)

if byte_size(response.body) <= size_limit do
emoji_dir_path =
Config.get(
[:mrf_steal_emoji, :path],
Path.join(Config.get([:instance, :static_dir]), "emoji/stolen")
)

extension =
url
|> URI.parse()
|> Map.get(:path)
|> Path.basename()
|> Path.extname()

file_path = Path.join([emoji_dir_path, shortcode <> (extension || ".png")])

try do
:ok = File.write(file_path, response.body)

shortcode
rescue
e ->
Logger.warn("MRF.StealEmojiPolicy: Failed to write to #{file_path}: #{inspect(e)}")
nil
end
else
Logger.debug(
"MRF.StealEmojiPolicy: :#{shortcode}: at #{url} (#{byte_size(response.body)} B) over size limit (#{
size_limit
} B)"
)

nil
end
rescue
e ->
Logger.warn("MRF.StealEmojiPolicy: Failed to fetch #{url}: #{inspect(e)}")
nil
end

@impl true
def filter(%{"object" => %{"emoji" => foreign_emojis, "actor" => actor}} = message) do
host = URI.parse(actor).host

if remote_host?(host) and accept_host?(host) do
installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end)

new_emojis =
foreign_emojis
|> Enum.filter(fn {shortcode, _url} -> shortcode not in installed_emoji end)
|> Enum.filter(fn {shortcode, _url} ->
reject_emoji? =
Config.get([:mrf_steal_emoji, :rejected_shortcodes], [])
|> Enum.find(false, fn regex -> String.match?(shortcode, regex) end)

!reject_emoji?
end)
|> Enum.map(&steal_emoji(&1))
|> Enum.filter(& &1)

if !Enum.empty?(new_emojis) do
Logger.info("Stole new emojis: #{inspect(new_emojis)}")
Pleroma.Emoji.reload()
end
end

{:ok, message}
end

def filter(message), do: {:ok, message}

@impl true
def describe do
{:ok, %{}}
end
end

+ 102
- 0
lib/pleroma/web/api_spec/operations/emoji_reaction_operation.ex View File

@@ -0,0 +1,102 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.ApiSpec.EmojiReactionOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.Account
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.Schemas.Status

def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end

def index_operation do
%Operation{
tags: ["Emoji Reactions"],
summary:
"Get an object of emoji to account mappings with accounts that reacted to the post",
parameters: [
Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
Operation.parameter(:emoji, :path, :string, "Filter by a single unicode emoji",
required: false
)
],
security: [%{"oAuth" => ["read:statuses"]}],
operationId: "EmojiReactionController.index",
responses: %{
200 => array_of_reactions_response()
}
}
end

def create_operation do
%Operation{
tags: ["Emoji Reactions"],
summary: "React to a post with a unicode emoji",
parameters: [
Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
Operation.parameter(:emoji, :path, :string, "A single character unicode emoji",
required: true
)
],
security: [%{"oAuth" => ["write:statuses"]}],
operationId: "EmojiReactionController.create",
responses: %{
200 => Operation.response("Status", "application/json", Status)
}
}
end

def delete_operation do
%Operation{
tags: ["Emoji Reactions"],
summary: "Remove a reaction to a post with a unicode emoji",
parameters: [
Operation.parameter(:id, :path, FlakeID, "Status ID", required: true),
Operation.parameter(:emoji, :path, :string, "A single character unicode emoji",
required: true
)
],
security: [%{"oAuth" => ["write:statuses"]}],
operationId: "EmojiReactionController.delete",
responses: %{
200 => Operation.response("Status", "application/json", Status)
}
}
end

defp array_of_reactions_response do
Operation.response("Array of Emoji Reactions", "application/json", %Schema{
type: :array,
items: emoji_reaction(),
example: [emoji_reaction().example]
})
end

defp emoji_reaction do
%Schema{
title: "EmojiReaction",
type: :object,
properties: %{
name: %Schema{type: :string, description: "Emoji"},
count: %Schema{type: :integer, description: "Count of reactions with this emoji"},
me: %Schema{type: :boolean, description: "Did I react with this emoji?"},
accounts: %Schema{
type: :array,
items: Account,
description: "Array of accounts reacted with this emoji"
}
},
example: %{
"name" => "😱",
"count" => 1,
"me" => false,
"accounts" => [Account.schema().example]
}
}
end
end

+ 1
- 1
lib/pleroma/web/api_spec/operations/notification_operation.ex View File

@@ -145,7 +145,7 @@ defmodule Pleroma.Web.ApiSpec.NotificationOperation do
}
end

defp notification do
def notification do
%Schema{
title: "Notification",
description: "Response schema for a notification",


+ 106
- 0
lib/pleroma/web/api_spec/operations/pleroma_conversation_operation.ex View File

@@ -0,0 +1,106 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.ApiSpec.PleromaConversationOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.Schemas.Conversation
alias Pleroma.Web.ApiSpec.Schemas.FlakeID
alias Pleroma.Web.ApiSpec.StatusOperation

import Pleroma.Web.ApiSpec.Helpers

def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end

def show_operation do
%Operation{
tags: ["Conversations"],
summary: "The conversation with the given ID",
parameters: [
Operation.parameter(:id, :path, :string, "Conversation ID",
example: "123",
required: true
)
],
security: [%{"oAuth" => ["read:statuses"]}],
operationId: "PleromaAPI.ConversationController.show",
responses: %{
200 => Operation.response("Conversation", "application/json", Conversation)
}
}
end

def statuses_operation do
%Operation{
tags: ["Conversations"],
summary: "Timeline for a given conversation",
parameters: [
Operation.parameter(:id, :path, :string, "Conversation ID",
example: "123",
required: true
)
| pagination_params()
],
security: [%{"oAuth" => ["read:statuses"]}],
operationId: "PleromaAPI.ConversationController.statuses",
responses: %{
200 =>
Operation.response(
"Array of Statuses",
"application/json",
StatusOperation.array_of_statuses()
)
}
}
end

def update_operation do
%Operation{
tags: ["Conversations"],
summary: "Update a conversation. Used to change the set of recipients.",
parameters: [
Operation.parameter(:id, :path, :string, "Conversation ID",
example: "123",
required: true
),
Operation.parameter(
:recipients,
:query,
%Schema{type: :array, items: FlakeID},
"A list of ids of users that should receive posts to this conversation. This will replace the current list of recipients, so submit the full list. The owner of owner of the conversation will always be part of the set of recipients, though.",
required: true
)
],
security: [%{"oAuth" => ["write:conversations"]}],
operationId: "PleromaAPI.ConversationController.update",
responses: %{
200 => Operation.response("Conversation", "application/json", Conversation)
}
}
end

def mark_as_read_operation do
%Operation{
tags: ["Conversations"],
summary: "Marks all user's conversations as read",
security: [%{"oAuth" => ["write:conversations"]}],
operationId: "PleromaAPI.ConversationController.mark_as_read",
responses: %{
200 =>
Operation.response(
"Array of Conversations that were marked as read",
"application/json",
%Schema{
type: :array,
items: Conversation,
example: [Conversation.schema().example]
}
)
}
}
end
end

+ 42
- 0
lib/pleroma/web/api_spec/operations/pleroma_notification_operation.ex View File

@@ -0,0 +1,42 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.ApiSpec.PleromaNotificationOperation do
alias OpenApiSpex.Operation
alias OpenApiSpex.Schema
alias Pleroma.Web.ApiSpec.NotificationOperation
alias Pleroma.Web.ApiSpec.Schemas.ApiError

def open_api_operation(action) do
operation = String.to_existing_atom("#{action}_operation")
apply(__MODULE__, operation, [])
end

def mark_as_read_operation do
%Operation{
tags: ["Notifications"],
summary: "Mark notifications as read. Query parameters are mutually exclusive.",
parameters: [
Operation.parameter(:id, :query, :string, "A single notification ID to read"),
Operation.parameter(:max_id, :query, :string, "Read all notifications up to this id")
],
security: [%{"oAuth" => ["write:notifications"]}],
operationId: "PleromaAPI.NotificationController.mark_as_read",
responses: %{
200 =>
Operation.response(
"A Notification or array of Motifications",
"application/json",
%Schema{
anyOf: [
%Schema{type: :array, items: NotificationOperation.notification()},
NotificationOperation.notification()
]
}
),
400 => Operation.response("Bad Request", "application/json", ApiError)
}
}
end
end

+ 95
- 0
lib/pleroma/web/pleroma_api/controllers/conversation_controller.ex View File

@@ -0,0 +1,95 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.PleromaAPI.ConversationController do
use Pleroma.Web, :controller

import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]

alias Pleroma.Conversation.Participation
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.MastodonAPI.StatusView

plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(:put_view, Pleroma.Web.MastodonAPI.ConversationView)
plug(OAuthScopesPlug, %{scopes: ["read:statuses"]} when action in [:show, :statuses])

plug(
OAuthScopesPlug,
%{scopes: ["write:conversations"]} when action in [:update, :mark_as_read]
)

defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaConversationOperation

def show(%{assigns: %{user: %{id: user_id} = user}} = conn, %{id: participation_id}) do
with %Participation{user_id: ^user_id} = participation <- Participation.get(participation_id) do
render(conn, "participation.json", participation: participation, for: user)
else
_error ->
conn
|> put_status(:not_found)
|> json(%{"error" => "Unknown conversation id"})
end
end

def statuses(
%{assigns: %{user: %{id: user_id} = user}} = conn,
%{id: participation_id} = params
) do
with %Participation{user_id: ^user_id} = participation <-
Participation.get(participation_id, preload: [:conversation]) do
params =
params
|> Map.new(fn {key, value} -> {to_string(key), value} end)
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)

activities =
participation.conversation.ap_id
|> ActivityPub.fetch_activities_for_context_query(params)
|> Pleroma.Pagination.fetch_paginated(Map.put(params, "total", false))
|> Enum.reverse()

conn
|> add_link_headers(activities)
|> put_view(StatusView)
|> render("index.json", activities: activities, for: user, as: :activity)
else
_error ->
conn
|> put_status(:not_found)
|> json(%{"error" => "Unknown conversation id"})
end
end

def update(
%{assigns: %{user: %{id: user_id} = user}} = conn,
%{id: participation_id, recipients: recipients}
) do
with %Participation{user_id: ^user_id} = participation <- Participation.get(participation_id),
{:ok, participation} <- Participation.set_recipients(participation, recipients) do
render(conn, "participation.json", participation: participation, for: user)
else
{:error, message} ->
conn
|> put_status(:bad_request)
|> json(%{"error" => message})

_error ->
conn
|> put_status(:not_found)
|> json(%{"error" => "Unknown conversation id"})
end
end

def mark_as_read(%{assigns: %{user: user}} = conn, _params) do
with {:ok, _, participations} <- Participation.mark_all_as_read(user) do
conn
|> add_link_headers(participations)
|> render("participations.json", participations: participations, for: user)
end
end
end

+ 61
- 0
lib/pleroma/web/pleroma_api/controllers/emoji_reaction_controller.ex View File

@@ -0,0 +1,61 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.PleromaAPI.EmojiReactionController do
use Pleroma.Web, :controller

alias Pleroma.Activity
alias Pleroma.Object
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.StatusView

plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["write:statuses"]} when action in [:create, :delete])

plug(
OAuthScopesPlug,
%{scopes: ["read:statuses"], fallback: :proceed_unauthenticated}
when action == :index
)

defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.EmojiReactionOperation

def index(%{assigns: %{user: user}} = conn, %{id: activity_id} = params) do
with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
%Object{data: %{"reactions" => reactions}} when is_list(reactions) <-
Object.normalize(activity) do
reactions = filter(reactions, params)
render(conn, "index.json", emoji_reactions: reactions, user: user)
else
_e -> json(conn, [])
end
end

defp filter(reactions, %{emoji: emoji}) when is_binary(emoji) do
Enum.filter(reactions, fn [e, _] -> e == emoji end)
end

defp filter(reactions, _), do: reactions

def create(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do
with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji) do
activity = Activity.get_by_id(activity_id)

conn
|> put_view(StatusView)
|> render("show.json", activity: activity, for: user, as: :activity)
end
end

def delete(%{assigns: %{user: user}} = conn, %{id: activity_id, emoji: emoji}) do
with {:ok, _activity} <- CommonAPI.unreact_with_emoji(activity_id, user, emoji) do
activity = Activity.get_by_id(activity_id)

conn
|> put_view(StatusView)
|> render("show.json", activity: activity, for: user, as: :activity)
end
end
end

+ 36
- 0
lib/pleroma/web/pleroma_api/controllers/notification_controller.ex View File

@@ -0,0 +1,36 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.PleromaAPI.NotificationController do
use Pleroma.Web, :controller

alias Pleroma.Notification
alias Pleroma.Plugs.OAuthScopesPlug

plug(Pleroma.Web.ApiSpec.CastAndValidate)
plug(OAuthScopesPlug, %{scopes: ["write:notifications"]} when action == :mark_as_read)
plug(:put_view, Pleroma.Web.MastodonAPI.NotificationView)

defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.PleromaNotificationOperation

def mark_as_read(%{assigns: %{user: user}} = conn, %{id: notification_id}) do
with {:ok, notification} <- Notification.read_one(user, notification_id) do
render(conn, "show.json", notification: notification, for: user)
else
{:error, message} ->
conn
|> put_status(:bad_request)
|> json(%{"error" => message})
end
end

def mark_as_read(%{assigns: %{user: user}} = conn, %{max_id: max_id}) do
notifications =
user
|> Notification.set_read_up_to(max_id)
|> Enum.take(80)

render(conn, "index.json", notifications: notifications, for: user)
end
end

+ 0
- 220
lib/pleroma/web/pleroma_api/controllers/pleroma_api_controller.ex View File

@@ -1,220 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.PleromaAPI.PleromaAPIController do
use Pleroma.Web, :controller

import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]

alias Pleroma.Activity
alias Pleroma.Conversation.Participation
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Plugs.OAuthScopesPlug
alias Pleroma.User
alias Pleroma.Web.ActivityPub.ActivityPub
alias Pleroma.Web.CommonAPI
alias Pleroma.Web.MastodonAPI.AccountView
alias Pleroma.Web.MastodonAPI.ConversationView
alias Pleroma.Web.MastodonAPI.NotificationView
alias Pleroma.Web.MastodonAPI.StatusView

plug(
OAuthScopesPlug,
%{scopes: ["read:statuses"]}
when action in [:conversation, :conversation_statuses]
)

plug(
OAuthScopesPlug,
%{scopes: ["read:statuses"], fallback: :proceed_unauthenticated}
when action == :emoji_reactions_by
)

plug(
OAuthScopesPlug,
%{scopes: ["write:statuses"]}
when action in [:react_with_emoji, :unreact_with_emoji]
)

plug(
OAuthScopesPlug,
%{scopes: ["write:conversations"]}
when action in [:update_conversation, :mark_conversations_as_read]
)

plug(
OAuthScopesPlug,
%{scopes: ["write:notifications"]} when action == :mark_notifications_as_read
)

def emoji_reactions_by(%{assigns: %{user: user}} = conn, %{"id" => activity_id} = params) do
with %Activity{} = activity <- Activity.get_by_id_with_object(activity_id),
%Object{data: %{"reactions" => emoji_reactions}} when is_list(emoji_reactions) <-
Object.normalize(activity) do
reactions =
emoji_reactions
|> Enum.map(fn [emoji, user_ap_ids] ->
if params["emoji"] && params["emoji"] != emoji do
nil
else
users =
Enum.map(user_ap_ids, &User.get_cached_by_ap_id/1)
|> Enum.filter(fn
%{deactivated: false} -> true
_ -> false
end)

%{
name: emoji,
count: length(users),
accounts:
AccountView.render("index.json", %{
users: users,
for: user,
as: :user
}),
me: !!(user && user.ap_id in user_ap_ids)
}
end
end)
|> Enum.filter(& &1)

conn
|> json(reactions)
else
_e ->
conn
|> json([])
end
end

def react_with_emoji(%{assigns: %{user: user}} = conn, %{"id" => activity_id, "emoji" => emoji}) do
with {:ok, _activity} <- CommonAPI.react_with_emoji(activity_id, user, emoji),
activity <- Activity.get_by_id(activity_id) do
conn
|> put_view(StatusView)
|> render("show.json", %{activity: activity, for: user, as: :activity})
end
end

def unreact_with_emoji(%{assigns: %{user: user}} = conn, %{
"id" => activity_id,
"emoji" => emoji
}) do
with {:ok, _activity} <-
CommonAPI.unreact_with_emoji(activity_id, user, emoji),
activity <- Activity.get_by_id(activity_id) do
conn
|> put_view(StatusView)
|> render("show.json", %{activity: activity, for: user, as: :activity})
end
end

def conversation(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
with %Participation{} = participation <- Participation.get(participation_id),
true <- user.id == participation.user_id do
conn
|> put_view(ConversationView)
|> render("participation.json", %{participation: participation, for: user})
else
_error ->
conn
|> put_status(404)
|> json(%{"error" => "Unknown conversation id"})
end
end

def conversation_statuses(
%{assigns: %{user: %{id: user_id} = user}} = conn,
%{"id" => participation_id} = params
) do
with %Participation{user_id: ^user_id} = participation <-
Participation.get(participation_id, preload: [:conversation]) do
params =
params
|> Map.put("blocking_user", user)
|> Map.put("muting_user", user)
|> Map.put("user", user)

activities =
participation.conversation.ap_id
|> ActivityPub.fetch_activities_for_context_query(params)
|> Pleroma.Pagination.fetch_paginated(Map.put(params, "total", false))
|> Enum.reverse()

conn
|> add_link_headers(activities)
|> put_view(StatusView)
|> render("index.json",
activities: activities,
for: user,
as: :activity
)
else
_error ->
conn
|> put_status(404)
|> json(%{"error" => "Unknown conversation id"})
end
end

def update_conversation(
%{assigns: %{user: user}} = conn,
%{"id" => participation_id, "recipients" => recipients}
) do
with %Participation{} = participation <- Participation.get(participation_id),
true <- user.id == participation.user_id,
{:ok, participation} <- Participation.set_recipients(participation, recipients) do
conn
|> put_view(ConversationView)
|> render("participation.json", %{participation: participation, for: user})
else
{:error, message} ->
conn
|> put_status(:bad_request)
|> json(%{"error" => message})

_error ->
conn
|> put_status(404)
|> json(%{"error" => "Unknown conversation id"})
end
end

def mark_conversations_as_read(%{assigns: %{user: user}} = conn, _params) do
with {:ok, _, participations} <- Participation.mark_all_as_read(user) do
conn
|> add_link_headers(participations)
|> put_view(ConversationView)
|> render("participations.json", participations: participations, for: user)
end
end

def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{"id" => notification_id}) do
with {:ok, notification} <- Notification.read_one(user, notification_id) do
conn
|> put_view(NotificationView)
|> render("show.json", %{notification: notification, for: user})
else
{:error, message} ->
conn
|> put_status(:bad_request)
|> json(%{"error" => message})
end
end

def mark_notifications_as_read(%{assigns: %{user: user}} = conn, %{"max_id" => max_id}) do
with notifications <- Notification.set_read_up_to(user, max_id) do
notifications = Enum.take(notifications, 80)

conn
|> put_view(NotificationView)
|> render("index.json",
notifications: notifications,
for: user
)
end
end
end

+ 33
- 0
lib/pleroma/web/pleroma_api/views/emoji_reaction_view.ex View File

@@ -0,0 +1,33 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.PleromaAPI.EmojiReactionView do
use Pleroma.Web, :view

alias Pleroma.Web.MastodonAPI.AccountView

def render("index.json", %{emoji_reactions: emoji_reactions} = opts) do
render_many(emoji_reactions, __MODULE__, "show.json", opts)
end

def render("show.json", %{emoji_reaction: [emoji, user_ap_ids], user: user}) do
users = fetch_users(user_ap_ids)

%{
name: emoji,
count: length(users),
accounts: render(AccountView, "index.json", users: users, for: user, as: :user),
me: !!(user && user.ap_id in user_ap_ids)
}
end

defp fetch_users(user_ap_ids) do
user_ap_ids
|> Enum.map(&Pleroma.User.get_cached_by_ap_id/1)
|> Enum.filter(fn
%{deactivated: false} -> true
_ -> false
end)
end
end

+ 9
- 17
lib/pleroma/web/router.ex View File

@@ -298,8 +298,8 @@ defmodule Pleroma.Web.Router do
scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
pipe_through(:api)

get("/statuses/:id/reactions/:emoji", PleromaAPIController, :emoji_reactions_by)
get("/statuses/:id/reactions", PleromaAPIController, :emoji_reactions_by)
get("/statuses/:id/reactions/:emoji", EmojiReactionController, :index)
get("/statuses/:id/reactions", EmojiReactionController, :index)
end

scope "/api/v1/pleroma", Pleroma.Web.PleromaAPI do
@@ -313,23 +313,15 @@ defmodule Pleroma.Web.Router do
post("/chats/:id/messages", ChatController, :post_chat_message)
delete("/chats/:id/messages/:message_id", ChatController, :delete_message)
post("/chats/:id/read", ChatController, :mark_as_read)
end

scope [] do
pipe_through(:authenticated_api)

get("/conversations/:id/statuses", PleromaAPIController, :conversation_statuses)
get("/conversations/:id", PleromaAPIController, :conversation)
post("/conversations/read", PleromaAPIController, :mark_conversations_as_read)
end

scope [] do
pipe_through(:authenticated_api)
get("/conversations/:id/statuses", ConversationController, :statuses)
get("/conversations/:id", ConversationController, :show)
post("/conversations/read", ConversationController, :mark_as_read)
patch("/conversations/:id", ConversationController, :update)

patch("/conversations/:id", PleromaAPIController, :update_conversation)
put("/statuses/:id/reactions/:emoji", PleromaAPIController, :react_with_emoji)
delete("/statuses/:id/reactions/:emoji", PleromaAPIController, :unreact_with_emoji)
post("/notifications/read", PleromaAPIController, :mark_notifications_as_read)
put("/statuses/:id/reactions/:emoji", EmojiReactionController, :create)
delete("/statuses/:id/reactions/:emoji", EmojiReactionController, :delete)
post("/notifications/read", NotificationController, :mark_as_read)

patch("/accounts/update_avatar", AccountController, :update_avatar)
patch("/accounts/update_banner", AccountController, :update_banner)


+ 1
- 1
test/support/api_spec_helpers.ex View File

@@ -51,7 +51,7 @@ defmodule Pleroma.Tests.ApiSpecHelpers do
|> Map.take([:delete, :get, :head, :options, :patch, :post, :put, :trace])
|> Map.values()
|> Enum.reject(&is_nil/1)
|> Enum.uniq()
end)
|> Enum.uniq()
end
end

+ 4
- 0
test/support/http_request_mock.ex View File

@@ -1291,6 +1291,10 @@ defmodule HttpRequestMock do
}}
end

def get("https://example.org/emoji/firedfox.png", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/image.jpg")}}
end

def get("https://skippers-bin.com/users/7v1w1r8ce6", _, _, _) do
{:ok, %Tesla.Env{status: 200, body: File.read!("test/fixtures/tesla_mock/sjw.json")}}
end


+ 8
- 8
test/tasks/user_test.exs View File

@@ -169,31 +169,31 @@ defmodule Mix.Tasks.Pleroma.UserTest do
end
end

describe "running unsubscribe" do
describe "running deactivate" do
test "user is unsubscribed" do
followed = insert(:user)
remote_followed = insert(:user, local: false)
user = insert(:user)

User.follow(user, followed, :follow_accept)
User.follow(user, remote_followed, :follow_accept)

Mix.Tasks.Pleroma.User.run(["unsubscribe", user.nickname])
Mix.Tasks.Pleroma.User.run(["deactivate", user.nickname])

assert_received {:mix_shell, :info, [message]}
assert message =~ "Deactivating"

assert_received {:mix_shell, :info, [message]}
assert message =~ "Unsubscribing"

# Note that the task has delay :timer.sleep(500)
assert_received {:mix_shell, :info, [message]}
assert message =~ "Successfully unsubscribed"

user = User.get_cached_by_nickname(user.nickname)
assert Enum.empty?(User.get_friends(user))
assert Enum.empty?(Enum.filter(User.get_friends(user), & &1.local))
assert user.deactivated
end

test "no user to unsubscribe" do
Mix.Tasks.Pleroma.User.run(["unsubscribe", "nonexistent"])
test "no user to deactivate" do
Mix.Tasks.Pleroma.User.run(["deactivate", "nonexistent"])

assert_received {:mix_shell, :error, [message]}
assert message =~ "No user"


+ 64
- 0
test/web/activity_pub/mrf/steal_emoji_policy_test.exs View File

@@ -0,0 +1,64 @@
# 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.MRF.StealEmojiPolicyTest do
use Pleroma.DataCase

alias Pleroma.Config
alias Pleroma.Web.ActivityPub.MRF.StealEmojiPolicy

setup_all do
Tesla.Mock.mock_global(fn env -> apply(HttpRequestMock, :request, [env]) end)
:ok
end

setup do
clear_config(:mrf_steal_emoji)

emoji_path = Path.join(Config.get([:instance, :static_dir]), "emoji/stolen")
File.rm_rf!(emoji_path)
File.mkdir!(emoji_path)

Pleroma.Emoji.reload()
end

test "does nothing by default" do
installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end)
refute "firedfox" in installed_emoji

message = %{
"type" => "Create",
"object" => %{
"emoji" => [{"firedfox", "https://example.org/emoji/firedfox.png"}],
"actor" => "https://example.org/users/admin"
}
}

assert {:ok, message} == StealEmojiPolicy.filter(message)

installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end)
refute "firedfox" in installed_emoji
end

test "Steals emoji on unknown shortcode from allowed remote host" do
installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end)
refute "firedfox" in installed_emoji

message = %{
"type" => "Create",
"object" => %{
"emoji" => [{"firedfox", "https://example.org/emoji/firedfox.png"}],
"actor" => "https://example.org/users/admin"
}
}

Config.put([:mrf_steal_emoji, :hosts], ["example.org"])
Config.put([:mrf_steal_emoji, :size_limit], 284_468)

assert {:ok, message} == StealEmojiPolicy.filter(message)

installed_emoji = Pleroma.Emoji.get_all() |> Enum.map(fn {k, _} -> k end)
assert "firedfox" in installed_emoji
end
end

+ 136
- 0
test/web/pleroma_api/controllers/conversation_controller_test.exs View File

@@ -0,0 +1,136 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.PleromaAPI.ConversationControllerTest do
use Pleroma.Web.ConnCase

alias Pleroma.Conversation.Participation
alias Pleroma.Repo
alias Pleroma.User
alias Pleroma.Web.CommonAPI

import Pleroma.Factory

test "/api/v1/pleroma/conversations/:id" do
user = insert(:user)
%{user: other_user, conn: conn} = oauth_access(["read:statuses"])

{:ok, _activity} =
CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}!", visibility: "direct"})

[participation] = Participation.for_user(other_user)

result =
conn
|> get("/api/v1/pleroma/conversations/#{participation.id}")
|> json_response_and_validate_schema(200)

assert result["id"] == participation.id |> to_string()
end

test "/api/v1/pleroma/conversations/:id/statuses" do
user = insert(:user)
%{user: other_user, conn: conn} = oauth_access(["read:statuses"])
third_user = insert(:user)

{:ok, _activity} =
CommonAPI.post(user, %{status: "Hi @#{third_user.nickname}!", visibility: "direct"})

{:ok, activity} =
CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}!", visibility: "direct"})

[participation] = Participation.for_user(other_user)

{:ok, activity_two} =
CommonAPI.post(other_user, %{
status: "Hi!",
in_reply_to_status_id: activity.id,
in_reply_to_conversation_id: participation.id
})

result =
conn
|> get("/api/v1/pleroma/conversations/#{participation.id}/statuses")
|> json_response_and_validate_schema(200)

assert length(result) == 2

id_one = activity.id
id_two = activity_two.id
assert [%{"id" => ^id_one}, %{"id" => ^id_two}] = result

{:ok, %{id: id_three}} =
CommonAPI.post(other_user, %{
status: "Bye!",
in_reply_to_status_id: activity.id,
in_reply_to_conversation_id: participation.id
})

assert [%{"id" => ^id_two}, %{"id" => ^id_three}] =
conn
|> get("/api/v1/pleroma/conversations/#{participation.id}/statuses?limit=2")
|> json_response_and_validate_schema(:ok)

assert [%{"id" => ^id_three}] =
conn
|> get("/api/v1/pleroma/conversations/#{participation.id}/statuses?min_id=#{id_two}")
|> json_response_and_validate_schema(:ok)
end

test "PATCH /api/v1/pleroma/conversations/:id" do
%{user: user, conn: conn} = oauth_access(["write:conversations"])
other_user = insert(:user)

{:ok, _activity} = CommonAPI.post(user, %{status: "Hi", visibility: "direct"})

[participation] = Participation.for_user(user)

participation = Repo.preload(participation, :recipients)

user = User.get_cached_by_id(user.id)
assert [user] == participation.recipients
assert other_user not in participation.recipients

query = "recipients[]=#{user.id}&recipients[]=#{other_user.id}"

result =
conn
|> patch("/api/v1/pleroma/conversations/#{participation.id}?#{query}")
|> json_response_and_validate_schema(200)

assert result["id"] == participation.id |> to_string

[participation] = Participation.for_user(user)
participation = Repo.preload(participation, :recipients)

assert user in participation.recipients
assert other_user in participation.recipients
end

test "POST /api/v1/pleroma/conversations/read" do
user = insert(:user)
%{user: other_user, conn: conn} = oauth_access(["write:conversations"])

{:ok, _activity} =
CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}", visibility: "direct"})

{:ok, _activity} =
CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}", visibility: "direct"})

[participation2, participation1] = Participation.for_user(other_user)
assert Participation.get(participation2.id).read == false
assert Participation.get(participation1.id).read == false
assert User.get_cached_by_id(other_user.id).unread_conversation_count == 2

[%{"unread" => false}, %{"unread" => false}] =
conn
|> post("/api/v1/pleroma/conversations/read", %{})
|> json_response_and_validate_schema(200)

[participation2, participation1] = Participation.for_user(other_user)
assert Participation.get(participation2.id).read == true
assert Participation.get(participation1.id).read == true
assert User.get_cached_by_id(other_user.id).unread_conversation_count == 0
end
end

+ 125
- 0
test/web/pleroma_api/controllers/emoji_reaction_controller_test.exs View File

@@ -0,0 +1,125 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.PleromaAPI.EmojiReactionControllerTest do
use Oban.Testing, repo: Pleroma.Repo
use Pleroma.Web.ConnCase

alias Pleroma.Object
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.CommonAPI

import Pleroma.Factory

test "PUT /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)

{:ok, activity} = CommonAPI.post(user, %{status: "#cofe"})

result =
conn
|> assign(:user, other_user)
|> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"]))
|> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/☕")
|> json_response_and_validate_schema(200)

# We return the status, but this our implementation detail.
assert %{"id" => id} = result
assert to_string(activity.id) == id

assert result["pleroma"]["emoji_reactions"] == [
%{"name" => "☕", "count" => 1, "me" => true}
]
end

test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)

{:ok, activity} = CommonAPI.post(user, %{status: "#cofe"})
{:ok, _reaction_activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")

ObanHelpers.perform_all()

result =
conn
|> assign(:user, other_user)
|> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"]))
|> delete("/api/v1/pleroma/statuses/#{activity.id}/reactions/☕")

assert %{"id" => id} = json_response_and_validate_schema(result, 200)
assert to_string(activity.id) == id

ObanHelpers.perform_all()

object = Object.get_by_ap_id(activity.data["object"])

assert object.data["reaction_count"] == 0
end

test "GET /api/v1/pleroma/statuses/:id/reactions", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
doomed_user = insert(:user)

{:ok, activity} = CommonAPI.post(user, %{status: "#cofe"})

result =
conn
|> get("/api/v1/pleroma/statuses/#{activity.id}/reactions")
|> json_response_and_validate_schema(200)

assert result == []

{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
{:ok, _} = CommonAPI.react_with_emoji(activity.id, doomed_user, "🎅")

User.perform(:delete, doomed_user)

result =
conn
|> get("/api/v1/pleroma/statuses/#{activity.id}/reactions")
|> json_response_and_validate_schema(200)

[%{"name" => "🎅", "count" => 1, "accounts" => [represented_user], "me" => false}] = result

assert represented_user["id"] == other_user.id

result =
conn
|> assign(:user, other_user)
|> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:statuses"]))
|> get("/api/v1/pleroma/statuses/#{activity.id}/reactions")
|> json_response_and_validate_schema(200)

assert [%{"name" => "🎅", "count" => 1, "accounts" => [_represented_user], "me" => true}] =
result
end

test "GET /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)

{:ok, activity} = CommonAPI.post(user, %{status: "#cofe"})

result =
conn
|> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅")
|> json_response_and_validate_schema(200)

assert result == []

{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")

assert [%{"name" => "🎅", "count" => 1, "accounts" => [represented_user], "me" => false}] =
conn
|> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅")
|> json_response_and_validate_schema(200)

assert represented_user["id"] == other_user.id
end
end

+ 63
- 0
test/web/pleroma_api/controllers/notification_controller_test.exs View File

@@ -0,0 +1,63 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.PleromaAPI.NotificationControllerTest do
use Pleroma.Web.ConnCase

alias Pleroma.Notification
alias Pleroma.Repo
alias Pleroma.Web.CommonAPI

import Pleroma.Factory

describe "POST /api/v1/pleroma/notifications/read" do
setup do: oauth_access(["write:notifications"])

test "it marks a single notification as read", %{user: user1, conn: conn} do
user2 = insert(:user)
{:ok, activity1} = CommonAPI.post(user2, %{status: "hi @#{user1.nickname}"})
{:ok, activity2} = CommonAPI.post(user2, %{status: "hi @#{user1.nickname}"})
{:ok, [notification1]} = Notification.create_notifications(activity1)
{:ok, [notification2]} = Notification.create_notifications(activity2)

response =
conn
|> post("/api/v1/pleroma/notifications/read?id=#{notification1.id}")
|> json_response_and_validate_schema(:ok)

assert %{"pleroma" => %{"is_seen" => true}} = response
assert Repo.get(Notification, notification1.id).seen
refute Repo.get(Notification, notification2.id).seen
end

test "it marks multiple notifications as read", %{user: user1, conn: conn} do
user2 = insert(:user)
{:ok, _activity1} = CommonAPI.post(user2, %{status: "hi @#{user1.nickname}"})
{:ok, _activity2} = CommonAPI.post(user2, %{status: "hi @#{user1.nickname}"})
{:ok, _activity3} = CommonAPI.post(user2, %{status: "HIE @#{user1.nickname}"})

[notification3, notification2, notification1] = Notification.for_user(user1, %{limit: 3})

[response1, response2] =
conn
|> post("/api/v1/pleroma/notifications/read?max_id=#{notification2.id}")
|> json_response_and_validate_schema(:ok)

assert %{"pleroma" => %{"is_seen" => true}} = response1
assert %{"pleroma" => %{"is_seen" => true}} = response2
assert Repo.get(Notification, notification1.id).seen
assert Repo.get(Notification, notification2.id).seen
refute Repo.get(Notification, notification3.id).seen
end

test "it returns error when notification not found", %{conn: conn} do
response =
conn
|> post("/api/v1/pleroma/notifications/read?id=22222222222222")
|> json_response_and_validate_schema(:bad_request)

assert response == %{"error" => "Cannot get notification"}
end
end
end

+ 0
- 302
test/web/pleroma_api/controllers/pleroma_api_controller_test.exs View File

@@ -1,302 +0,0 @@
# Pleroma: A lightweight social networking server
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: AGPL-3.0-only

defmodule Pleroma.Web.PleromaAPI.PleromaAPIControllerTest do
use Oban.Testing, repo: Pleroma.Repo
use Pleroma.Web.ConnCase

alias Pleroma.Conversation.Participation
alias Pleroma.Notification
alias Pleroma.Object
alias Pleroma.Repo
alias Pleroma.Tests.ObanHelpers
alias Pleroma.User
alias Pleroma.Web.CommonAPI

import Pleroma.Factory

test "PUT /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)

{:ok, activity} = CommonAPI.post(user, %{status: "#cofe"})

result =
conn
|> assign(:user, other_user)
|> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"]))
|> put("/api/v1/pleroma/statuses/#{activity.id}/reactions/☕")
|> json_response(200)

# We return the status, but this our implementation detail.
assert %{"id" => id} = result
assert to_string(activity.id) == id

assert result["pleroma"]["emoji_reactions"] == [
%{"name" => "☕", "count" => 1, "me" => true}
]
end

test "DELETE /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)

{:ok, activity} = CommonAPI.post(user, %{status: "#cofe"})
{:ok, _reaction_activity} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")

ObanHelpers.perform_all()

result =
conn
|> assign(:user, other_user)
|> assign(:token, insert(:oauth_token, user: other_user, scopes: ["write:statuses"]))
|> delete("/api/v1/pleroma/statuses/#{activity.id}/reactions/☕")

assert %{"id" => id} = json_response(result, 200)
assert to_string(activity.id) == id

ObanHelpers.perform_all()

object = Object.get_by_ap_id(activity.data["object"])

assert object.data["reaction_count"] == 0
end

test "GET /api/v1/pleroma/statuses/:id/reactions", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)
doomed_user = insert(:user)

{:ok, activity} = CommonAPI.post(user, %{status: "#cofe"})

result =
conn
|> get("/api/v1/pleroma/statuses/#{activity.id}/reactions")
|> json_response(200)

assert result == []

{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
{:ok, _} = CommonAPI.react_with_emoji(activity.id, doomed_user, "🎅")

User.perform(:delete, doomed_user)

result =
conn
|> get("/api/v1/pleroma/statuses/#{activity.id}/reactions")
|> json_response(200)

[%{"name" => "🎅", "count" => 1, "accounts" => [represented_user], "me" => false}] = result

assert represented_user["id"] == other_user.id

result =
conn
|> assign(:user, other_user)
|> assign(:token, insert(:oauth_token, user: other_user, scopes: ["read:statuses"]))
|> get("/api/v1/pleroma/statuses/#{activity.id}/reactions")
|> json_response(200)

assert [%{"name" => "🎅", "count" => 1, "accounts" => [_represented_user], "me" => true}] =
result
end

test "GET /api/v1/pleroma/statuses/:id/reactions/:emoji", %{conn: conn} do
user = insert(:user)
other_user = insert(:user)

{:ok, activity} = CommonAPI.post(user, %{status: "#cofe"})

result =
conn
|> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅")
|> json_response(200)

assert result == []

{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "🎅")
{:ok, _} = CommonAPI.react_with_emoji(activity.id, other_user, "☕")

result =
conn
|> get("/api/v1/pleroma/statuses/#{activity.id}/reactions/🎅")
|> json_response(200)

[%{"name" => "🎅", "count" => 1, "accounts" => [represented_user], "me" => false}] = result

assert represented_user["id"] == other_user.id
end

test "/api/v1/pleroma/conversations/:id" do
user = insert(:user)
%{user: other_user, conn: conn} = oauth_access(["read:statuses"])

{:ok, _activity} =
CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}!", visibility: "direct"})

[participation] = Participation.for_user(other_user)

result =
conn
|> get("/api/v1/pleroma/conversations/#{participation.id}")
|> json_response(200)

assert result["id"] == participation.id |> to_string()
end

test "/api/v1/pleroma/conversations/:id/statuses" do
user = insert(:user)
%{user: other_user, conn: conn} = oauth_access(["read:statuses"])
third_user = insert(:user)

{:ok, _activity} =
CommonAPI.post(user, %{status: "Hi @#{third_user.nickname}!", visibility: "direct"})

{:ok, activity} =
CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}!", visibility: "direct"})

[participation] = Participation.for_user(other_user)

{:ok, activity_two} =
CommonAPI.post(other_user, %{
status: "Hi!",
in_reply_to_status_id: activity.id,
in_reply_to_conversation_id: participation.id
})

result =
conn
|> get("/api/v1/pleroma/conversations/#{participation.id}/statuses")
|> json_response(200)

assert length(result) == 2

id_one = activity.id
id_two = activity_two.id
assert [%{"id" => ^id_one}, %{"id" => ^id_two}] = result

{:ok, %{id: id_three}} =
CommonAPI.post(other_user, %{
status: "Bye!",
in_reply_to_status_id: activity.id,
in_reply_to_conversation_id: participation.id
})

assert [%{"id" => ^id_two}, %{"id" => ^id_three}] =
conn
|> get("/api/v1/pleroma/conversations/#{participation.id}/statuses?limit=2")
|> json_response(:ok)

assert [%{"id" => ^id_three}] =
conn
|> get("/api/v1/pleroma/conversations/#{participation.id}/statuses?min_id=#{id_two}")
|> json_response(:ok)
end

test "PATCH /api/v1/pleroma/conversations/:id" do
%{user: user, conn: conn} = oauth_access(["write:conversations"])
other_user = insert(:user)

{:ok, _activity} = CommonAPI.post(user, %{status: "Hi", visibility: "direct"})

[participation] = Participation.for_user(user)

participation = Repo.preload(participation, :recipients)

user = User.get_cached_by_id(user.id)
assert [user] == participation.recipients
assert other_user not in participation.recipients

result =
conn
|> patch("/api/v1/pleroma/conversations/#{participation.id}", %{
"recipients" => [user.id, other_user.id]
})
|> json_response(200)

assert result["id"] == participation.id |> to_string

[participation] = Participation.for_user(user)
participation = Repo.preload(participation, :recipients)

assert user in participation.recipients
assert other_user in participation.recipients
end

test "POST /api/v1/pleroma/conversations/read" do
user = insert(:user)
%{user: other_user, conn: conn} = oauth_access(["write:conversations"])

{:ok, _activity} =
CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}", visibility: "direct"})

{:ok, _activity} =
CommonAPI.post(user, %{status: "Hi @#{other_user.nickname}", visibility: "direct"})

[participation2, participation1] = Participation.for_user(other_user)
assert Participation.get(participation2.id).read == false
assert Participation.get(participation1.id).read == false
assert User.get_cached_by_id(other_user.id).unread_conversation_count == 2

[%{"unread" => false}, %{"unread" => false}] =
conn
|> post("/api/v1/pleroma/conversations/read", %{})
|> json_response(200)

[participation2, participation1] = Participation.for_user(other_user)
assert Participation.get(participation2.id).read == true
assert Participation.get(participation1.id).read == true
assert User.get_cached_by_id(other_user.id).unread_conversation_count == 0
end

describe "POST /api/v1/pleroma/notifications/read" do
setup do: oauth_access(["write:notifications"])

test "it marks a single notification as read", %{user: user1, conn: conn} do
user2 = insert(:user)
{:ok, activity1} = CommonAPI.post(user2, %{status: "hi @#{user1.nickname}"})
{:ok, activity2} = CommonAPI.post(user2, %{status: "hi @#{user1.nickname}"})
{:ok, [notification1]} = Notification.create_notifications(activity1)
{:ok, [notification2]} = Notification.create_notifications(activity2)

response =
conn
|> post("/api/v1/pleroma/notifications/read", %{"id" => "#{notification1.id}"})
|> json_response(:ok)

assert %{"pleroma" => %{"is_seen" => true}} = response
assert Repo.get(Notification, notification1.id).seen
refute Repo.get(Notification, notification2.id).seen
end

test "it marks multiple notifications as read", %{user: user1, conn: conn} do
user2 = insert(:user)
{:ok, _activity1} = CommonAPI.post(user2, %{status: "hi @#{user1.nickname}"})
{:ok, _activity2} = CommonAPI.post(user2, %{status: "hi @#{user1.nickname}"})
{:ok, _activity3} = CommonAPI.post(user2, %{status: "HIE @#{user1.nickname}"})

[notification3, notification2, notification1] = Notification.for_user(user1, %{limit: 3})

[response1, response2] =
conn
|> post("/api/v1/pleroma/notifications/read", %{"max_id" => "#{notification2.id}"})
|> json_response(:ok)

assert %{"pleroma" => %{"is_seen" => true}} = response1
assert %{"pleroma" => %{"is_seen" => true}} = response2
assert Repo.get(Notification, notification1.id).seen
assert Repo.get(Notification, notification2.id).seen
refute Repo.get(Notification, notification3.id).seen
end

test "it returns error when notification not found", %{conn: conn} do
response =
conn
|> post("/api/v1/pleroma/notifications/read", %{"id" => "22222222222222"})
|> json_response(:bad_request)

assert response == %{"error" => "Cannot get notification"}
end
end
end

Loading…
Cancel
Save