[#1234] Merge remote-tracking branch 'remotes/upstream/develop' into 1234-mastodon...
[akkoma] / lib / pleroma / web / mastodon_api / controllers / status_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.MastodonAPI.StatusController do
6 use Pleroma.Web, :controller
7
8 import Pleroma.Web.MastodonAPI.MastodonAPIController, only: [try_render: 3]
9
10 require Ecto.Query
11
12 alias Pleroma.Activity
13 alias Pleroma.Bookmark
14 alias Pleroma.Object
15 alias Pleroma.Plugs.OAuthScopesPlug
16 alias Pleroma.Plugs.RateLimiter
17 alias Pleroma.Repo
18 alias Pleroma.ScheduledActivity
19 alias Pleroma.User
20 alias Pleroma.Web.ActivityPub.ActivityPub
21 alias Pleroma.Web.ActivityPub.Visibility
22 alias Pleroma.Web.CommonAPI
23 alias Pleroma.Web.MastodonAPI.AccountView
24 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
25
26 @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
27
28 plug(
29 OAuthScopesPlug,
30 %{@unauthenticated_access | scopes: ["read:statuses"]}
31 when action in [
32 :index,
33 :show,
34 :card,
35 :context
36 ]
37 )
38
39 plug(
40 OAuthScopesPlug,
41 %{scopes: ["write:statuses"]}
42 when action in [
43 :create,
44 :delete,
45 :reblog,
46 :unreblog
47 ]
48 )
49
50 plug(
51 OAuthScopesPlug,
52 %{scopes: ["write:favourites"]} when action in [:favourite, :unfavourite]
53 )
54
55 plug(
56 OAuthScopesPlug,
57 %{scopes: ["write:mutes"]} when action in [:mute_conversation, :unmute_conversation]
58 )
59
60 plug(
61 OAuthScopesPlug,
62 %{@unauthenticated_access | scopes: ["read:accounts"]}
63 when action in [:favourited_by, :reblogged_by]
64 )
65
66 plug(OAuthScopesPlug, %{scopes: ["write:accounts"]} when action in [:pin, :unpin])
67
68 # Note: scope not present in Mastodon: write:bookmarks
69 plug(
70 OAuthScopesPlug,
71 %{scopes: ["write:bookmarks"]} when action in [:bookmark, :unbookmark]
72 )
73
74 plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug)
75
76 @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
77
78 plug(
79 RateLimiter,
80 {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
81 when action in ~w(reblog unreblog)a
82 )
83
84 plug(
85 RateLimiter,
86 {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
87 when action in ~w(favourite unfavourite)a
88 )
89
90 plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
91
92 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
93
94 @doc """
95 GET `/api/v1/statuses?ids[]=1&ids[]=2`
96
97 `ids` query param is required
98 """
99 def index(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
100 limit = 100
101
102 activities =
103 ids
104 |> Enum.take(limit)
105 |> Activity.all_by_ids_with_object()
106 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
107
108 render(conn, "index.json", activities: activities, for: user, as: :activity)
109 end
110
111 @doc """
112 POST /api/v1/statuses
113
114 Creates a scheduled status when `scheduled_at` param is present and it's far enough
115 """
116 def create(
117 %{assigns: %{user: user}} = conn,
118 %{"status" => _, "scheduled_at" => scheduled_at} = params
119 ) do
120 params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])
121
122 if ScheduledActivity.far_enough?(scheduled_at) do
123 with {:ok, scheduled_activity} <-
124 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
125 conn
126 |> put_view(ScheduledActivityView)
127 |> render("show.json", scheduled_activity: scheduled_activity)
128 end
129 else
130 create(conn, Map.drop(params, ["scheduled_at"]))
131 end
132 end
133
134 @doc """
135 POST /api/v1/statuses
136
137 Creates a regular status
138 """
139 def create(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
140 params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])
141
142 with {:ok, activity} <- CommonAPI.post(user, params) do
143 try_render(conn, "show.json",
144 activity: activity,
145 for: user,
146 as: :activity,
147 with_direct_conversation_id: true
148 )
149 else
150 {:error, message} ->
151 conn
152 |> put_status(:unprocessable_entity)
153 |> json(%{error: message})
154 end
155 end
156
157 def create(%{assigns: %{user: _user}} = conn, %{"media_ids" => _} = params) do
158 create(conn, Map.put(params, "status", ""))
159 end
160
161 @doc "GET /api/v1/statuses/:id"
162 def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
163 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
164 true <- Visibility.visible_for_user?(activity, user) do
165 try_render(conn, "show.json", activity: activity, for: user)
166 end
167 end
168
169 @doc "DELETE /api/v1/statuses/:id"
170 def delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
171 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
172 json(conn, %{})
173 else
174 _e -> render_error(conn, :forbidden, "Can't delete this post")
175 end
176 end
177
178 @doc "POST /api/v1/statuses/:id/reblog"
179 def reblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
180 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
181 %Activity{} = announce <- Activity.normalize(announce.data) do
182 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
183 end
184 end
185
186 @doc "POST /api/v1/statuses/:id/unreblog"
187 def unreblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
188 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
189 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
190 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
191 end
192 end
193
194 @doc "POST /api/v1/statuses/:id/favourite"
195 def favourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
196 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
197 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
198 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
199 end
200 end
201
202 @doc "POST /api/v1/statuses/:id/unfavourite"
203 def unfavourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
204 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
205 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
206 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
207 end
208 end
209
210 @doc "POST /api/v1/statuses/:id/pin"
211 def pin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
212 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
213 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
214 end
215 end
216
217 @doc "POST /api/v1/statuses/:id/unpin"
218 def unpin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
219 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
220 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
221 end
222 end
223
224 @doc "POST /api/v1/statuses/:id/bookmark"
225 def bookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do
226 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
227 %User{} = user <- User.get_cached_by_nickname(user.nickname),
228 true <- Visibility.visible_for_user?(activity, user),
229 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
230 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
231 end
232 end
233
234 @doc "POST /api/v1/statuses/:id/unbookmark"
235 def unbookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do
236 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
237 %User{} = user <- User.get_cached_by_nickname(user.nickname),
238 true <- Visibility.visible_for_user?(activity, user),
239 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
240 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
241 end
242 end
243
244 @doc "POST /api/v1/statuses/:id/mute"
245 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
246 with %Activity{} = activity <- Activity.get_by_id(id),
247 {:ok, activity} <- CommonAPI.add_mute(user, activity) do
248 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
249 end
250 end
251
252 @doc "POST /api/v1/statuses/:id/unmute"
253 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
254 with %Activity{} = activity <- Activity.get_by_id(id),
255 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
256 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
257 end
258 end
259
260 @doc "GET /api/v1/statuses/:id/card"
261 @deprecated "https://github.com/tootsuite/mastodon/pull/11213"
262 def card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
263 with %Activity{} = activity <- Activity.get_by_id(status_id),
264 true <- Visibility.visible_for_user?(activity, user) do
265 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
266 render(conn, "card.json", data)
267 else
268 _ -> render_error(conn, :not_found, "Record not found")
269 end
270 end
271
272 @doc "GET /api/v1/statuses/:id/favourited_by"
273 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
274 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
275 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
276 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
277 users =
278 User
279 |> Ecto.Query.where([u], u.ap_id in ^likes)
280 |> Repo.all()
281 |> Enum.filter(&(not User.blocks?(user, &1)))
282
283 conn
284 |> put_view(AccountView)
285 |> render("index.json", for: user, users: users, as: :user)
286 else
287 {:visible, false} -> {:error, :not_found}
288 _ -> json(conn, [])
289 end
290 end
291
292 @doc "GET /api/v1/statuses/:id/reblogged_by"
293 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
294 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
295 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
296 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
297 users =
298 User
299 |> Ecto.Query.where([u], u.ap_id in ^announces)
300 |> Repo.all()
301 |> Enum.filter(&(not User.blocks?(user, &1)))
302
303 conn
304 |> put_view(AccountView)
305 |> render("index.json", for: user, users: users, as: :user)
306 else
307 {:visible, false} -> {:error, :not_found}
308 _ -> json(conn, [])
309 end
310 end
311
312 @doc "GET /api/v1/statuses/:id/context"
313 def context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
314 with %Activity{} = activity <- Activity.get_by_id(id) do
315 activities =
316 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
317 "blocking_user" => user,
318 "user" => user,
319 "exclude_id" => activity.id
320 })
321
322 render(conn, "context.json", activity: activity, activities: activities, user: user)
323 end
324 end
325 end