89869bda097785cb38f9122ce8f218f97fe3a222
[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.RateLimiter
16 alias Pleroma.Repo
17 alias Pleroma.ScheduledActivity
18 alias Pleroma.User
19 alias Pleroma.Web.ActivityPub.ActivityPub
20 alias Pleroma.Web.ActivityPub.Visibility
21 alias Pleroma.Web.CommonAPI
22 alias Pleroma.Web.MastodonAPI.AccountView
23 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
24 alias Pleroma.Web.MastodonAPI.StatusView
25
26 @rate_limited_status_actions ~w(reblog unreblog favourite unfavourite create delete)a
27
28 plug(
29 RateLimiter,
30 {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
31 when action in ~w(reblog unreblog)a
32 )
33
34 plug(
35 RateLimiter,
36 {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
37 when action in ~w(favourite unfavourite)a
38 )
39
40 plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
41
42 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
43
44 @doc """
45 GET `/api/v1/statuses?ids[]=1&ids[]=2`
46
47 `ids` query param is required
48 """
49 def index(%{assigns: %{user: user}} = conn, %{"ids" => ids}) do
50 limit = 100
51
52 activities =
53 ids
54 |> Enum.take(limit)
55 |> Activity.all_by_ids_with_object()
56 |> Enum.filter(&Visibility.visible_for_user?(&1, user))
57
58 render(conn, "index.json", activities: activities, for: user, as: :activity)
59 end
60
61 @doc """
62 POST /api/v1/statuses
63
64 Creates a scheduled status when `scheduled_at` param is present and it's far enough
65 """
66 def create(
67 %{assigns: %{user: user}} = conn,
68 %{"status" => _, "scheduled_at" => scheduled_at} = params
69 ) do
70 params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])
71
72 if ScheduledActivity.far_enough?(scheduled_at) do
73 with {:ok, scheduled_activity} <-
74 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
75 conn
76 |> put_view(ScheduledActivityView)
77 |> render("show.json", scheduled_activity: scheduled_activity)
78 end
79 else
80 create(conn, Map.drop(params, ["scheduled_at"]))
81 end
82 end
83
84 @doc """
85 POST /api/v1/statuses
86
87 Creates a regular status
88 """
89 def create(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
90 params = Map.put(params, "in_reply_to_status_id", params["in_reply_to_id"])
91
92 with {:ok, activity} <- CommonAPI.post(user, params) do
93 try_render(conn, "show.json",
94 activity: activity,
95 for: user,
96 as: :activity,
97 with_direct_conversation_id: true
98 )
99 else
100 {:error, message} ->
101 conn
102 |> put_status(:unprocessable_entity)
103 |> json(%{error: message})
104 end
105 end
106
107 @doc "GET /api/v1/statuses/:id"
108 def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
109 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
110 true <- Visibility.visible_for_user?(activity, user) do
111 try_render(conn, "show.json", activity: activity, for: user)
112 end
113 end
114
115 @doc "DELETE /api/v1/statuses/:id"
116 def delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
117 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
118 json(conn, %{})
119 else
120 _e -> render_error(conn, :forbidden, "Can't delete this post")
121 end
122 end
123
124 @doc "POST /api/v1/statuses/:id/reblog"
125 def reblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
126 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
127 %Activity{} = announce <- Activity.normalize(announce.data) do
128 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
129 end
130 end
131
132 @doc "POST /api/v1/statuses/:id/unreblog"
133 def unreblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
134 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
135 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
136 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
137 end
138 end
139
140 @doc "POST /api/v1/statuses/:id/favourite"
141 def favourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
142 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
143 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
144 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
145 end
146 end
147
148 @doc "POST /api/v1/statuses/:id/unfavourite"
149 def unfavourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
150 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
151 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
152 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
153 end
154 end
155
156 @doc "POST /api/v1/statuses/:id/pin"
157 def pin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
158 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
159 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
160 end
161 end
162
163 @doc "POST /api/v1/statuses/:id/unpin"
164 def unpin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
165 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
166 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
167 end
168 end
169
170 @doc "POST /api/v1/statuses/:id/bookmark"
171 def bookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do
172 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
173 %User{} = user <- User.get_cached_by_nickname(user.nickname),
174 true <- Visibility.visible_for_user?(activity, user),
175 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
176 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
177 end
178 end
179
180 @doc "POST /api/v1/statuses/:id/unbookmark"
181 def unbookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do
182 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
183 %User{} = user <- User.get_cached_by_nickname(user.nickname),
184 true <- Visibility.visible_for_user?(activity, user),
185 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
186 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
187 end
188 end
189
190 @doc "POST /api/v1/statuses/:id/mute"
191 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
192 with %Activity{} = activity <- Activity.get_by_id(id),
193 {:ok, activity} <- CommonAPI.add_mute(user, activity) do
194 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
195 end
196 end
197
198 @doc "POST /api/v1/statuses/:id/unmute"
199 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
200 with %Activity{} = activity <- Activity.get_by_id(id),
201 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
202 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
203 end
204 end
205
206 @doc "GET /api/v1/statuses/:id/card"
207 def card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
208 with %Activity{} = activity <- Activity.get_by_id(status_id),
209 true <- Visibility.visible_for_user?(activity, user) do
210 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
211 render(conn, "card.json", data)
212 else
213 _ -> render_error(conn, :not_found, "Record not found")
214 end
215 end
216
217 @doc "GET /api/v1/statuses/:id/favourited_by"
218 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
219 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
220 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
221 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
222 users =
223 User
224 |> Ecto.Query.where([u], u.ap_id in ^likes)
225 |> Repo.all()
226 |> Enum.filter(&(not User.blocks?(user, &1)))
227
228 conn
229 |> put_view(AccountView)
230 |> render("accounts.json", for: user, users: users, as: :user)
231 else
232 {:visible, false} -> {:error, :not_found}
233 _ -> json(conn, [])
234 end
235 end
236
237 @doc "GET /api/v1/statuses/:id/reblogged_by"
238 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
239 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
240 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
241 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
242 users =
243 User
244 |> Ecto.Query.where([u], u.ap_id in ^announces)
245 |> Repo.all()
246 |> Enum.filter(&(not User.blocks?(user, &1)))
247
248 conn
249 |> put_view(AccountView)
250 |> render("accounts.json", for: user, users: users, as: :user)
251 else
252 {:visible, false} -> {:error, :not_found}
253 _ -> json(conn, [])
254 end
255 end
256
257 @doc "GET /api/v1/statuses/:id/context"
258 def context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
259 with %Activity{} = activity <- Activity.get_by_id(id) do
260 activities =
261 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
262 "blocking_user" => user,
263 "user" => user,
264 "exclude_id" => activity.id
265 })
266
267 # TODO: Move to view
268 grouped_activities = Enum.group_by(activities, fn %{id: id} -> id < activity.id end)
269
270 result = %{
271 ancestors:
272 StatusView.render(
273 "index.json",
274 for: user,
275 activities: grouped_activities[true] || [],
276 as: :activity
277 )
278 |> Enum.reverse(),
279 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
280 descendants:
281 StatusView.render(
282 "index.json",
283 for: user,
284 activities: grouped_activities[false] || [],
285 as: :activity
286 )
287 |> Enum.reverse()
288 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
289 }
290
291 json(conn, result)
292 end
293 end
294 end