Move view logic from StatusController.context to StatusView and add a test
[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 @doc "GET /api/v1/statuses/:id"
107 def show(%{assigns: %{user: user}} = conn, %{"id" => id}) do
108 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
109 true <- Visibility.visible_for_user?(activity, user) do
110 try_render(conn, "show.json", activity: activity, for: user)
111 end
112 end
113
114 @doc "DELETE /api/v1/statuses/:id"
115 def delete(%{assigns: %{user: user}} = conn, %{"id" => id}) do
116 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
117 json(conn, %{})
118 else
119 _e -> render_error(conn, :forbidden, "Can't delete this post")
120 end
121 end
122
123 @doc "POST /api/v1/statuses/:id/reblog"
124 def reblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
125 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
126 %Activity{} = announce <- Activity.normalize(announce.data) do
127 try_render(conn, "show.json", %{activity: announce, for: user, as: :activity})
128 end
129 end
130
131 @doc "POST /api/v1/statuses/:id/unreblog"
132 def unreblog(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
133 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
134 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
135 try_render(conn, "show.json", %{activity: activity, for: user, as: :activity})
136 end
137 end
138
139 @doc "POST /api/v1/statuses/:id/favourite"
140 def favourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
141 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
142 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
143 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
144 end
145 end
146
147 @doc "POST /api/v1/statuses/:id/unfavourite"
148 def unfavourite(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
149 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
150 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
151 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
152 end
153 end
154
155 @doc "POST /api/v1/statuses/:id/pin"
156 def pin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
157 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
158 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
159 end
160 end
161
162 @doc "POST /api/v1/statuses/:id/unpin"
163 def unpin(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
164 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
165 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
166 end
167 end
168
169 @doc "POST /api/v1/statuses/:id/bookmark"
170 def bookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do
171 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
172 %User{} = user <- User.get_cached_by_nickname(user.nickname),
173 true <- Visibility.visible_for_user?(activity, user),
174 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
175 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
176 end
177 end
178
179 @doc "POST /api/v1/statuses/:id/unbookmark"
180 def unbookmark(%{assigns: %{user: user}} = conn, %{"id" => id}) do
181 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
182 %User{} = user <- User.get_cached_by_nickname(user.nickname),
183 true <- Visibility.visible_for_user?(activity, user),
184 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
185 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
186 end
187 end
188
189 @doc "POST /api/v1/statuses/:id/mute"
190 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
191 with %Activity{} = activity <- Activity.get_by_id(id),
192 {:ok, activity} <- CommonAPI.add_mute(user, activity) do
193 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
194 end
195 end
196
197 @doc "POST /api/v1/statuses/:id/unmute"
198 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
199 with %Activity{} = activity <- Activity.get_by_id(id),
200 {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
201 try_render(conn, "show.json", activity: activity, for: user, as: :activity)
202 end
203 end
204
205 @doc "GET /api/v1/statuses/:id/card"
206 def card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
207 with %Activity{} = activity <- Activity.get_by_id(status_id),
208 true <- Visibility.visible_for_user?(activity, user) do
209 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
210 render(conn, "card.json", data)
211 else
212 _ -> render_error(conn, :not_found, "Record not found")
213 end
214 end
215
216 @doc "GET /api/v1/statuses/:id/favourited_by"
217 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
218 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
219 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
220 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
221 users =
222 User
223 |> Ecto.Query.where([u], u.ap_id in ^likes)
224 |> Repo.all()
225 |> Enum.filter(&(not User.blocks?(user, &1)))
226
227 conn
228 |> put_view(AccountView)
229 |> render("accounts.json", for: user, users: users, as: :user)
230 else
231 {:visible, false} -> {:error, :not_found}
232 _ -> json(conn, [])
233 end
234 end
235
236 @doc "GET /api/v1/statuses/:id/reblogged_by"
237 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
238 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
239 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
240 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
241 users =
242 User
243 |> Ecto.Query.where([u], u.ap_id in ^announces)
244 |> Repo.all()
245 |> Enum.filter(&(not User.blocks?(user, &1)))
246
247 conn
248 |> put_view(AccountView)
249 |> render("accounts.json", for: user, users: users, as: :user)
250 else
251 {:visible, false} -> {:error, :not_found}
252 _ -> json(conn, [])
253 end
254 end
255
256 @doc "GET /api/v1/statuses/:id/context"
257 def context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
258 with %Activity{} = activity <- Activity.get_by_id(id) do
259 activities =
260 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
261 "blocking_user" => user,
262 "user" => user,
263 "exclude_id" => activity.id
264 })
265
266 render(conn, "context.json", activity: activity, activities: activities, user: user)
267 end
268 end
269 end