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