Merge branch 'bugfix/post-empty-status' into 'develop'
[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 def card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
211 with %Activity{} = activity <- Activity.get_by_id(status_id),
212 true <- Visibility.visible_for_user?(activity, user) do
213 data = Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
214 render(conn, "card.json", data)
215 else
216 _ -> render_error(conn, :not_found, "Record not found")
217 end
218 end
219
220 @doc "GET /api/v1/statuses/:id/favourited_by"
221 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
222 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
223 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
224 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
225 users =
226 User
227 |> Ecto.Query.where([u], u.ap_id in ^likes)
228 |> Repo.all()
229 |> Enum.filter(&(not User.blocks?(user, &1)))
230
231 conn
232 |> put_view(AccountView)
233 |> render("accounts.json", for: user, users: users, as: :user)
234 else
235 {:visible, false} -> {:error, :not_found}
236 _ -> json(conn, [])
237 end
238 end
239
240 @doc "GET /api/v1/statuses/:id/reblogged_by"
241 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
242 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
243 {:visible, true} <- {:visible, Visibility.visible_for_user?(activity, user)},
244 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
245 users =
246 User
247 |> Ecto.Query.where([u], u.ap_id in ^announces)
248 |> Repo.all()
249 |> Enum.filter(&(not User.blocks?(user, &1)))
250
251 conn
252 |> put_view(AccountView)
253 |> render("accounts.json", for: user, users: users, as: :user)
254 else
255 {:visible, false} -> {:error, :not_found}
256 _ -> json(conn, [])
257 end
258 end
259
260 @doc "GET /api/v1/statuses/:id/context"
261 def context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
262 with %Activity{} = activity <- Activity.get_by_id(id) do
263 activities =
264 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
265 "blocking_user" => user,
266 "user" => user,
267 "exclude_id" => activity.id
268 })
269
270 render(conn, "context.json", activity: activity, activities: activities, user: user)
271 end
272 end
273 end