Merge branch 'idempotency-key-optimistic-posting' into 'develop'
[akkoma] / lib / pleroma / web / pleroma_api / controllers / chat_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4 defmodule Pleroma.Web.PleromaAPI.ChatController do
5 use Pleroma.Web, :controller
6
7 import Pleroma.Web.ControllerHelper, only: [add_link_headers: 2]
8
9 alias Pleroma.Activity
10 alias Pleroma.Chat
11 alias Pleroma.Chat.MessageReference
12 alias Pleroma.Object
13 alias Pleroma.Pagination
14 alias Pleroma.Repo
15 alias Pleroma.User
16 alias Pleroma.Web.CommonAPI
17 alias Pleroma.Web.PleromaAPI.Chat.MessageReferenceView
18 alias Pleroma.Web.PleromaAPI.ChatView
19 alias Pleroma.Web.Plugs.OAuthScopesPlug
20
21 import Ecto.Query
22
23 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
24
25 plug(
26 OAuthScopesPlug,
27 %{scopes: ["write:chats"]}
28 when action in [
29 :post_chat_message,
30 :create,
31 :mark_as_read,
32 :mark_message_as_read,
33 :delete_message
34 ]
35 )
36
37 plug(
38 OAuthScopesPlug,
39 %{scopes: ["read:chats"]} when action in [:messages, :index, :show]
40 )
41
42 plug(OpenApiSpex.Plug.CastAndValidate, render_error: Pleroma.Web.ApiSpec.RenderError)
43
44 defdelegate open_api_operation(action), to: Pleroma.Web.ApiSpec.ChatOperation
45
46 def delete_message(%{assigns: %{user: %{id: user_id} = user}} = conn, %{
47 message_id: message_id,
48 id: chat_id
49 }) do
50 with %MessageReference{} = cm_ref <-
51 MessageReference.get_by_id(message_id),
52 ^chat_id <- to_string(cm_ref.chat_id),
53 %Chat{user_id: ^user_id} <- Chat.get_by_id(chat_id),
54 {:ok, _} <- remove_or_delete(cm_ref, user) do
55 conn
56 |> put_view(MessageReferenceView)
57 |> render("show.json", chat_message_reference: cm_ref)
58 else
59 _e ->
60 {:error, :could_not_delete}
61 end
62 end
63
64 defp remove_or_delete(
65 %{object: %{data: %{"actor" => actor, "id" => id}}},
66 %{ap_id: actor} = user
67 ) do
68 with %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
69 CommonAPI.delete(activity.id, user)
70 end
71 end
72
73 defp remove_or_delete(cm_ref, _), do: MessageReference.delete(cm_ref)
74
75 def post_chat_message(
76 %{body_params: params, assigns: %{user: user}} = conn,
77 %{id: id}
78 ) do
79 with {:ok, chat} <- Chat.get_by_user_and_id(user, id),
80 %User{} = recipient <- User.get_cached_by_ap_id(chat.recipient),
81 {:ok, activity} <-
82 CommonAPI.post_chat_message(user, recipient, params[:content],
83 media_id: params[:media_id],
84 idempotency_key: idempotency_key(conn)
85 ),
86 message <- Object.normalize(activity, false),
87 cm_ref <- MessageReference.for_chat_and_object(chat, message) do
88 conn
89 |> put_view(MessageReferenceView)
90 |> render("show.json", chat_message_reference: cm_ref)
91 else
92 {:reject, message} ->
93 conn
94 |> put_status(:unprocessable_entity)
95 |> json(%{error: message})
96
97 {:error, message} ->
98 conn
99 |> put_status(:bad_request)
100 |> json(%{error: message})
101 end
102 end
103
104 def mark_message_as_read(
105 %{assigns: %{user: %{id: user_id}}} = conn,
106 %{id: chat_id, message_id: message_id}
107 ) do
108 with %MessageReference{} = cm_ref <- MessageReference.get_by_id(message_id),
109 ^chat_id <- to_string(cm_ref.chat_id),
110 %Chat{user_id: ^user_id} <- Chat.get_by_id(chat_id),
111 {:ok, cm_ref} <- MessageReference.mark_as_read(cm_ref) do
112 conn
113 |> put_view(MessageReferenceView)
114 |> render("show.json", chat_message_reference: cm_ref)
115 end
116 end
117
118 def mark_as_read(
119 %{body_params: %{last_read_id: last_read_id}, assigns: %{user: user}} = conn,
120 %{id: id}
121 ) do
122 with {:ok, chat} <- Chat.get_by_user_and_id(user, id),
123 {_n, _} <- MessageReference.set_all_seen_for_chat(chat, last_read_id) do
124 conn
125 |> put_view(ChatView)
126 |> render("show.json", chat: chat)
127 end
128 end
129
130 def messages(%{assigns: %{user: user}} = conn, %{id: id} = params) do
131 with {:ok, chat} <- Chat.get_by_user_and_id(user, id) do
132 chat_message_refs =
133 chat
134 |> MessageReference.for_chat_query()
135 |> Pagination.fetch_paginated(params)
136
137 conn
138 |> add_link_headers(chat_message_refs)
139 |> put_view(MessageReferenceView)
140 |> render("index.json", chat_message_references: chat_message_refs)
141 end
142 end
143
144 def index(%{assigns: %{user: %{id: user_id} = user}} = conn, _params) do
145 blocked_ap_ids = User.blocked_users_ap_ids(user)
146
147 chats =
148 Chat.for_user_query(user_id)
149 |> where([c], c.recipient not in ^blocked_ap_ids)
150 |> Repo.all()
151
152 conn
153 |> put_view(ChatView)
154 |> render("index.json", chats: chats)
155 end
156
157 def create(%{assigns: %{user: user}} = conn, %{id: id}) do
158 with %User{ap_id: recipient} <- User.get_cached_by_id(id),
159 {:ok, %Chat{} = chat} <- Chat.get_or_create(user.id, recipient) do
160 conn
161 |> put_view(ChatView)
162 |> render("show.json", chat: chat)
163 end
164 end
165
166 def show(%{assigns: %{user: user}} = conn, %{id: id}) do
167 with {:ok, chat} <- Chat.get_by_user_and_id(user, id) do
168 conn
169 |> put_view(ChatView)
170 |> render("show.json", chat: chat)
171 end
172 end
173
174 defp idempotency_key(conn) do
175 case get_req_header(conn, "idempotency-key") do
176 [key] -> key
177 _ -> nil
178 end
179 end
180 end