Add scheduled activities
[akkoma] / lib / pleroma / web / mastodon_api / mastodon_api_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.MastodonAPIController do
6 use Pleroma.Web, :controller
7
8 alias Pleroma.Activity
9 alias Pleroma.Config
10 alias Pleroma.Filter
11 alias Pleroma.Notification
12 alias Pleroma.Object
13 alias Pleroma.Repo
14 alias Pleroma.ScheduledActivity
15 alias Pleroma.Stats
16 alias Pleroma.User
17 alias Pleroma.Web
18 alias Pleroma.Web.ActivityPub.ActivityPub
19 alias Pleroma.Web.ActivityPub.Visibility
20 alias Pleroma.Web.CommonAPI
21 alias Pleroma.Web.MastodonAPI.AccountView
22 alias Pleroma.Web.MastodonAPI.AppView
23 alias Pleroma.Web.MastodonAPI.FilterView
24 alias Pleroma.Web.MastodonAPI.ListView
25 alias Pleroma.Web.MastodonAPI.MastodonAPI
26 alias Pleroma.Web.MastodonAPI.MastodonView
27 alias Pleroma.Web.MastodonAPI.NotificationView
28 alias Pleroma.Web.MastodonAPI.ReportView
29 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
30 alias Pleroma.Web.MastodonAPI.StatusView
31 alias Pleroma.Web.MediaProxy
32 alias Pleroma.Web.OAuth.App
33 alias Pleroma.Web.OAuth.Authorization
34 alias Pleroma.Web.OAuth.Token
35
36 import Pleroma.Web.ControllerHelper, only: [oauth_scopes: 2]
37 import Ecto.Query
38
39 require Logger
40
41 @httpoison Application.get_env(:pleroma, :httpoison)
42 @local_mastodon_name "Mastodon-Local"
43
44 action_fallback(:errors)
45
46 def create_app(conn, params) do
47 scopes = oauth_scopes(params, ["read"])
48
49 app_attrs =
50 params
51 |> Map.drop(["scope", "scopes"])
52 |> Map.put("scopes", scopes)
53
54 with cs <- App.register_changeset(%App{}, app_attrs),
55 false <- cs.changes[:client_name] == @local_mastodon_name,
56 {:ok, app} <- Repo.insert(cs) do
57 conn
58 |> put_view(AppView)
59 |> render("show.json", %{app: app})
60 end
61 end
62
63 defp add_if_present(
64 map,
65 params,
66 params_field,
67 map_field,
68 value_function \\ fn x -> {:ok, x} end
69 ) do
70 if Map.has_key?(params, params_field) do
71 case value_function.(params[params_field]) do
72 {:ok, new_value} -> Map.put(map, map_field, new_value)
73 :error -> map
74 end
75 else
76 map
77 end
78 end
79
80 def update_credentials(%{assigns: %{user: user}} = conn, params) do
81 original_user = user
82
83 user_params =
84 %{}
85 |> add_if_present(params, "display_name", :name)
86 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value)} end)
87 |> add_if_present(params, "avatar", :avatar, fn value ->
88 with %Plug.Upload{} <- value,
89 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
90 {:ok, object.data}
91 else
92 _ -> :error
93 end
94 end)
95
96 info_params =
97 %{}
98 |> add_if_present(params, "locked", :locked, fn value -> {:ok, value == "true"} end)
99 |> add_if_present(params, "header", :banner, fn value ->
100 with %Plug.Upload{} <- value,
101 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
102 {:ok, object.data}
103 else
104 _ -> :error
105 end
106 end)
107
108 info_cng = User.Info.mastodon_profile_update(user.info, info_params)
109
110 with changeset <- User.update_changeset(user, user_params),
111 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
112 {:ok, user} <- User.update_and_set_cache(changeset) do
113 if original_user != user do
114 CommonAPI.update(user)
115 end
116
117 json(conn, AccountView.render("account.json", %{user: user, for: user}))
118 else
119 _e ->
120 conn
121 |> put_status(403)
122 |> json(%{error: "Invalid request"})
123 end
124 end
125
126 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
127 account = AccountView.render("account.json", %{user: user, for: user})
128 json(conn, account)
129 end
130
131 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
132 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
133 conn
134 |> put_view(AppView)
135 |> render("short.json", %{app: app})
136 end
137 end
138
139 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
140 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
141 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
142 account = AccountView.render("account.json", %{user: user, for: for_user})
143 json(conn, account)
144 else
145 _e ->
146 conn
147 |> put_status(404)
148 |> json(%{error: "Can't find user"})
149 end
150 end
151
152 @mastodon_api_level "2.5.0"
153
154 def masto_instance(conn, _params) do
155 instance = Config.get(:instance)
156
157 response = %{
158 uri: Web.base_url(),
159 title: Keyword.get(instance, :name),
160 description: Keyword.get(instance, :description),
161 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
162 email: Keyword.get(instance, :email),
163 urls: %{
164 streaming_api: Pleroma.Web.Endpoint.websocket_url()
165 },
166 stats: Stats.get_stats(),
167 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
168 languages: ["en"],
169 registrations: Pleroma.Config.get([:instance, :registrations_open]),
170 # Extra (not present in Mastodon):
171 max_toot_chars: Keyword.get(instance, :limit)
172 }
173
174 json(conn, response)
175 end
176
177 def peers(conn, _params) do
178 json(conn, Stats.get_peers())
179 end
180
181 defp mastodonized_emoji do
182 Pleroma.Emoji.get_all()
183 |> Enum.map(fn {shortcode, relative_url} ->
184 url = to_string(URI.merge(Web.base_url(), relative_url))
185
186 %{
187 "shortcode" => shortcode,
188 "static_url" => url,
189 "visible_in_picker" => true,
190 "url" => url
191 }
192 end)
193 end
194
195 def custom_emojis(conn, _params) do
196 mastodon_emoji = mastodonized_emoji()
197 json(conn, mastodon_emoji)
198 end
199
200 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
201 params =
202 conn.params
203 |> Map.drop(["since_id", "max_id"])
204 |> Map.merge(params)
205
206 last = List.last(activities)
207 first = List.first(activities)
208
209 if last do
210 min = last.id
211 max = first.id
212
213 {next_url, prev_url} =
214 if param do
215 {
216 mastodon_api_url(
217 Pleroma.Web.Endpoint,
218 method,
219 param,
220 Map.merge(params, %{max_id: min})
221 ),
222 mastodon_api_url(
223 Pleroma.Web.Endpoint,
224 method,
225 param,
226 Map.merge(params, %{since_id: max})
227 )
228 }
229 else
230 {
231 mastodon_api_url(
232 Pleroma.Web.Endpoint,
233 method,
234 Map.merge(params, %{max_id: min})
235 ),
236 mastodon_api_url(
237 Pleroma.Web.Endpoint,
238 method,
239 Map.merge(params, %{since_id: max})
240 )
241 }
242 end
243
244 conn
245 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
246 else
247 conn
248 end
249 end
250
251 def home_timeline(%{assigns: %{user: user}} = conn, params) do
252 params =
253 params
254 |> Map.put("type", ["Create", "Announce"])
255 |> Map.put("blocking_user", user)
256 |> Map.put("muting_user", user)
257 |> Map.put("user", user)
258
259 activities =
260 [user.ap_id | user.following]
261 |> ActivityPub.fetch_activities(params)
262 |> ActivityPub.contain_timeline(user)
263 |> Enum.reverse()
264
265 conn
266 |> add_link_headers(:home_timeline, activities)
267 |> put_view(StatusView)
268 |> render("index.json", %{activities: activities, for: user, as: :activity})
269 end
270
271 def public_timeline(%{assigns: %{user: user}} = conn, params) do
272 local_only = params["local"] in [true, "True", "true", "1"]
273
274 activities =
275 params
276 |> Map.put("type", ["Create", "Announce"])
277 |> Map.put("local_only", local_only)
278 |> Map.put("blocking_user", user)
279 |> Map.put("muting_user", user)
280 |> ActivityPub.fetch_public_activities()
281 |> Enum.reverse()
282
283 conn
284 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
285 |> put_view(StatusView)
286 |> render("index.json", %{activities: activities, for: user, as: :activity})
287 end
288
289 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
290 with %User{} = user <- User.get_by_id(params["id"]) do
291 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
292
293 conn
294 |> add_link_headers(:user_statuses, activities, params["id"])
295 |> put_view(StatusView)
296 |> render("index.json", %{
297 activities: activities,
298 for: reading_user,
299 as: :activity
300 })
301 end
302 end
303
304 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
305 params =
306 params
307 |> Map.put("type", "Create")
308 |> Map.put("blocking_user", user)
309 |> Map.put("user", user)
310 |> Map.put(:visibility, "direct")
311
312 activities =
313 [user.ap_id]
314 |> ActivityPub.fetch_activities_query(params)
315 |> Repo.all()
316
317 conn
318 |> add_link_headers(:dm_timeline, activities)
319 |> put_view(StatusView)
320 |> render("index.json", %{activities: activities, for: user, as: :activity})
321 end
322
323 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
324 with %Activity{} = activity <- Activity.get_by_id(id),
325 true <- Visibility.visible_for_user?(activity, user) do
326 conn
327 |> put_view(StatusView)
328 |> try_render("status.json", %{activity: activity, for: user})
329 end
330 end
331
332 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
333 with %Activity{} = activity <- Activity.get_by_id(id),
334 activities <-
335 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
336 "blocking_user" => user,
337 "user" => user
338 }),
339 activities <-
340 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
341 activities <-
342 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
343 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
344 result = %{
345 ancestors:
346 StatusView.render(
347 "index.json",
348 for: user,
349 activities: grouped_activities[true] || [],
350 as: :activity
351 )
352 |> Enum.reverse(),
353 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
354 descendants:
355 StatusView.render(
356 "index.json",
357 for: user,
358 activities: grouped_activities[false] || [],
359 as: :activity
360 )
361 |> Enum.reverse()
362 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
363 }
364
365 json(conn, result)
366 end
367 end
368
369 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
370 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
371 conn
372 |> add_link_headers(:scheduled_statuses, scheduled_activities)
373 |> put_view(ScheduledActivityView)
374 |> render("index.json", %{scheduled_activities: scheduled_activities})
375 end
376 end
377
378 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
379 with %ScheduledActivity{} = scheduled_activity <-
380 ScheduledActivity.get(user, scheduled_activity_id) do
381 conn
382 |> put_view(ScheduledActivityView)
383 |> render("show.json", %{scheduled_activity: scheduled_activity})
384 else
385 _ -> {:error, :not_found}
386 end
387 end
388
389 def update_scheduled_status(
390 %{assigns: %{user: user}} = conn,
391 %{"id" => scheduled_activity_id} = params
392 ) do
393 with {:ok, scheduled_activity} <-
394 ScheduledActivity.update(user, scheduled_activity_id, params) do
395 conn
396 |> put_view(ScheduledActivityView)
397 |> render("show.json", %{scheduled_activity: scheduled_activity})
398 end
399 end
400
401 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
402 with {:ok, %ScheduledActivity{}} <- ScheduledActivity.delete(user, scheduled_activity_id) do
403 conn
404 |> json(%{})
405 end
406 end
407
408 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
409 when length(media_ids) > 0 do
410 params =
411 params
412 |> Map.put("status", ".")
413
414 post_status(conn, params)
415 end
416
417 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
418 params =
419 params
420 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
421
422 idempotency_key =
423 case get_req_header(conn, "idempotency-key") do
424 [key] -> key
425 _ -> Ecto.UUID.generate()
426 end
427
428 {:ok, activity} =
429 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
430
431 conn
432 |> put_view(StatusView)
433 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
434 end
435
436 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
437 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
438 json(conn, %{})
439 else
440 _e ->
441 conn
442 |> put_status(403)
443 |> json(%{error: "Can't delete this post"})
444 end
445 end
446
447 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
448 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
449 conn
450 |> put_view(StatusView)
451 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
452 end
453 end
454
455 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
456 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
457 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
458 conn
459 |> put_view(StatusView)
460 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
461 end
462 end
463
464 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
465 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
466 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
467 conn
468 |> put_view(StatusView)
469 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
470 end
471 end
472
473 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
474 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
475 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
476 conn
477 |> put_view(StatusView)
478 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
479 end
480 end
481
482 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
483 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
484 conn
485 |> put_view(StatusView)
486 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
487 else
488 {:error, reason} ->
489 conn
490 |> put_resp_content_type("application/json")
491 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
492 end
493 end
494
495 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
496 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
497 conn
498 |> put_view(StatusView)
499 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
500 end
501 end
502
503 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
504 with %Activity{} = activity <- Activity.get_by_id(id),
505 %User{} = user <- User.get_by_nickname(user.nickname),
506 true <- Visibility.visible_for_user?(activity, user),
507 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
508 conn
509 |> put_view(StatusView)
510 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
511 end
512 end
513
514 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
515 with %Activity{} = activity <- Activity.get_by_id(id),
516 %User{} = user <- User.get_by_nickname(user.nickname),
517 true <- Visibility.visible_for_user?(activity, user),
518 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
519 conn
520 |> put_view(StatusView)
521 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
522 end
523 end
524
525 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
526 activity = Activity.get_by_id(id)
527
528 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
529 conn
530 |> put_view(StatusView)
531 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
532 else
533 {:error, reason} ->
534 conn
535 |> put_resp_content_type("application/json")
536 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
537 end
538 end
539
540 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
541 activity = Activity.get_by_id(id)
542
543 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
544 conn
545 |> put_view(StatusView)
546 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
547 end
548 end
549
550 def notifications(%{assigns: %{user: user}} = conn, params) do
551 notifications = MastodonAPI.get_notifications(user, params)
552
553 conn
554 |> add_link_headers(:notifications, notifications)
555 |> put_view(NotificationView)
556 |> render("index.json", %{notifications: notifications, for: user})
557 end
558
559 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
560 with {:ok, notification} <- Notification.get(user, id) do
561 conn
562 |> put_view(NotificationView)
563 |> render("show.json", %{notification: notification, for: user})
564 else
565 {:error, reason} ->
566 conn
567 |> put_resp_content_type("application/json")
568 |> send_resp(403, Jason.encode!(%{"error" => reason}))
569 end
570 end
571
572 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
573 Notification.clear(user)
574 json(conn, %{})
575 end
576
577 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
578 with {:ok, _notif} <- Notification.dismiss(user, id) do
579 json(conn, %{})
580 else
581 {:error, reason} ->
582 conn
583 |> put_resp_content_type("application/json")
584 |> send_resp(403, Jason.encode!(%{"error" => reason}))
585 end
586 end
587
588 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
589 id = List.wrap(id)
590 q = from(u in User, where: u.id in ^id)
591 targets = Repo.all(q)
592
593 conn
594 |> put_view(AccountView)
595 |> render("relationships.json", %{user: user, targets: targets})
596 end
597
598 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
599 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
600
601 def update_media(%{assigns: %{user: user}} = conn, data) do
602 with %Object{} = object <- Repo.get(Object, data["id"]),
603 true <- Object.authorize_mutation(object, user),
604 true <- is_binary(data["description"]),
605 description <- data["description"] do
606 new_data = %{object.data | "name" => description}
607
608 {:ok, _} =
609 object
610 |> Object.change(%{data: new_data})
611 |> Repo.update()
612
613 attachment_data = Map.put(new_data, "id", object.id)
614
615 conn
616 |> put_view(StatusView)
617 |> render("attachment.json", %{attachment: attachment_data})
618 end
619 end
620
621 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
622 with {:ok, object} <-
623 ActivityPub.upload(
624 file,
625 actor: User.ap_id(user),
626 description: Map.get(data, "description")
627 ) do
628 attachment_data = Map.put(object.data, "id", object.id)
629
630 conn
631 |> put_view(StatusView)
632 |> render("attachment.json", %{attachment: attachment_data})
633 end
634 end
635
636 def favourited_by(conn, %{"id" => id}) do
637 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Activity.get_by_id(id) do
638 q = from(u in User, where: u.ap_id in ^likes)
639 users = Repo.all(q)
640
641 conn
642 |> put_view(AccountView)
643 |> render(AccountView, "accounts.json", %{users: users, as: :user})
644 else
645 _ -> json(conn, [])
646 end
647 end
648
649 def reblogged_by(conn, %{"id" => id}) do
650 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Activity.get_by_id(id) do
651 q = from(u in User, where: u.ap_id in ^announces)
652 users = Repo.all(q)
653
654 conn
655 |> put_view(AccountView)
656 |> render("accounts.json", %{users: users, as: :user})
657 else
658 _ -> json(conn, [])
659 end
660 end
661
662 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
663 local_only = params["local"] in [true, "True", "true", "1"]
664
665 tags =
666 [params["tag"], params["any"]]
667 |> List.flatten()
668 |> Enum.uniq()
669 |> Enum.filter(& &1)
670 |> Enum.map(&String.downcase(&1))
671
672 tag_all =
673 params["all"] ||
674 []
675 |> Enum.map(&String.downcase(&1))
676
677 tag_reject =
678 params["none"] ||
679 []
680 |> Enum.map(&String.downcase(&1))
681
682 activities =
683 params
684 |> Map.put("type", "Create")
685 |> Map.put("local_only", local_only)
686 |> Map.put("blocking_user", user)
687 |> Map.put("muting_user", user)
688 |> Map.put("tag", tags)
689 |> Map.put("tag_all", tag_all)
690 |> Map.put("tag_reject", tag_reject)
691 |> ActivityPub.fetch_public_activities()
692 |> Enum.reverse()
693
694 conn
695 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
696 |> put_view(StatusView)
697 |> render("index.json", %{activities: activities, for: user, as: :activity})
698 end
699
700 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
701 with %User{} = user <- User.get_by_id(id),
702 followers <- MastodonAPI.get_followers(user, params) do
703 followers =
704 cond do
705 for_user && user.id == for_user.id -> followers
706 user.info.hide_followers -> []
707 true -> followers
708 end
709
710 conn
711 |> add_link_headers(:followers, followers, user)
712 |> put_view(AccountView)
713 |> render("accounts.json", %{users: followers, as: :user})
714 end
715 end
716
717 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
718 with %User{} = user <- User.get_by_id(id),
719 followers <- MastodonAPI.get_friends(user, params) do
720 followers =
721 cond do
722 for_user && user.id == for_user.id -> followers
723 user.info.hide_follows -> []
724 true -> followers
725 end
726
727 conn
728 |> add_link_headers(:following, followers, user)
729 |> put_view(AccountView)
730 |> render("accounts.json", %{users: followers, as: :user})
731 end
732 end
733
734 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
735 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
736 conn
737 |> put_view(AccountView)
738 |> render("accounts.json", %{users: follow_requests, as: :user})
739 end
740 end
741
742 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
743 with %User{} = follower <- User.get_by_id(id),
744 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
745 conn
746 |> put_view(AccountView)
747 |> render("relationship.json", %{user: followed, target: follower})
748 else
749 {:error, message} ->
750 conn
751 |> put_resp_content_type("application/json")
752 |> send_resp(403, Jason.encode!(%{"error" => message}))
753 end
754 end
755
756 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
757 with %User{} = follower <- User.get_by_id(id),
758 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
759 conn
760 |> put_view(AccountView)
761 |> render("relationship.json", %{user: followed, target: follower})
762 else
763 {:error, message} ->
764 conn
765 |> put_resp_content_type("application/json")
766 |> send_resp(403, Jason.encode!(%{"error" => message}))
767 end
768 end
769
770 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
771 with %User{} = followed <- User.get_by_id(id),
772 false <- User.following?(follower, followed),
773 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
774 conn
775 |> put_view(AccountView)
776 |> render("relationship.json", %{user: follower, target: followed})
777 else
778 true ->
779 followed = User.get_cached_by_id(id)
780
781 {:ok, follower} =
782 case conn.params["reblogs"] do
783 true -> CommonAPI.show_reblogs(follower, followed)
784 false -> CommonAPI.hide_reblogs(follower, followed)
785 end
786
787 conn
788 |> put_view(AccountView)
789 |> render("relationship.json", %{user: follower, target: followed})
790
791 {:error, message} ->
792 conn
793 |> put_resp_content_type("application/json")
794 |> send_resp(403, Jason.encode!(%{"error" => message}))
795 end
796 end
797
798 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
799 with %User{} = followed <- User.get_by_nickname(uri),
800 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
801 conn
802 |> put_view(AccountView)
803 |> render("account.json", %{user: followed, for: follower})
804 else
805 {:error, message} ->
806 conn
807 |> put_resp_content_type("application/json")
808 |> send_resp(403, Jason.encode!(%{"error" => message}))
809 end
810 end
811
812 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
813 with %User{} = followed <- User.get_by_id(id),
814 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
815 conn
816 |> put_view(AccountView)
817 |> render("relationship.json", %{user: follower, target: followed})
818 end
819 end
820
821 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
822 with %User{} = muted <- User.get_by_id(id),
823 {:ok, muter} <- User.mute(muter, muted) do
824 conn
825 |> put_view(AccountView)
826 |> render("relationship.json", %{user: muter, target: muted})
827 else
828 {:error, message} ->
829 conn
830 |> put_resp_content_type("application/json")
831 |> send_resp(403, Jason.encode!(%{"error" => message}))
832 end
833 end
834
835 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
836 with %User{} = muted <- User.get_by_id(id),
837 {:ok, muter} <- User.unmute(muter, muted) do
838 conn
839 |> put_view(AccountView)
840 |> render("relationship.json", %{user: muter, target: muted})
841 else
842 {:error, message} ->
843 conn
844 |> put_resp_content_type("application/json")
845 |> send_resp(403, Jason.encode!(%{"error" => message}))
846 end
847 end
848
849 def mutes(%{assigns: %{user: user}} = conn, _) do
850 with muted_accounts <- User.muted_users(user) do
851 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
852 json(conn, res)
853 end
854 end
855
856 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
857 with %User{} = blocked <- User.get_by_id(id),
858 {:ok, blocker} <- User.block(blocker, blocked),
859 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
860 conn
861 |> put_view(AccountView)
862 |> render("relationship.json", %{user: blocker, target: blocked})
863 else
864 {:error, message} ->
865 conn
866 |> put_resp_content_type("application/json")
867 |> send_resp(403, Jason.encode!(%{"error" => message}))
868 end
869 end
870
871 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
872 with %User{} = blocked <- User.get_by_id(id),
873 {:ok, blocker} <- User.unblock(blocker, blocked),
874 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
875 conn
876 |> put_view(AccountView)
877 |> render("relationship.json", %{user: blocker, target: blocked})
878 else
879 {:error, message} ->
880 conn
881 |> put_resp_content_type("application/json")
882 |> send_resp(403, Jason.encode!(%{"error" => message}))
883 end
884 end
885
886 def blocks(%{assigns: %{user: user}} = conn, _) do
887 with blocked_accounts <- User.blocked_users(user) do
888 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
889 json(conn, res)
890 end
891 end
892
893 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
894 json(conn, info.domain_blocks || [])
895 end
896
897 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
898 User.block_domain(blocker, domain)
899 json(conn, %{})
900 end
901
902 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
903 User.unblock_domain(blocker, domain)
904 json(conn, %{})
905 end
906
907 def status_search(user, query) do
908 fetched =
909 if Regex.match?(~r/https?:/, query) do
910 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
911 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
912 true <- Visibility.visible_for_user?(activity, user) do
913 [activity]
914 else
915 _e -> []
916 end
917 end || []
918
919 q =
920 from(
921 a in Activity,
922 where: fragment("?->>'type' = 'Create'", a.data),
923 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
924 where:
925 fragment(
926 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
927 a.data,
928 ^query
929 ),
930 limit: 20,
931 order_by: [desc: :id]
932 )
933
934 Repo.all(q) ++ fetched
935 end
936
937 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
938 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
939
940 statuses = status_search(user, query)
941
942 tags_path = Web.base_url() <> "/tag/"
943
944 tags =
945 query
946 |> String.split()
947 |> Enum.uniq()
948 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
949 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
950 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
951
952 res = %{
953 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
954 "statuses" =>
955 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
956 "hashtags" => tags
957 }
958
959 json(conn, res)
960 end
961
962 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
963 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
964
965 statuses = status_search(user, query)
966
967 tags =
968 query
969 |> String.split()
970 |> Enum.uniq()
971 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
972 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
973
974 res = %{
975 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
976 "statuses" =>
977 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
978 "hashtags" => tags
979 }
980
981 json(conn, res)
982 end
983
984 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
985 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
986
987 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
988
989 json(conn, res)
990 end
991
992 def favourites(%{assigns: %{user: user}} = conn, params) do
993 params =
994 params
995 |> Map.put("type", "Create")
996 |> Map.put("favorited_by", user.ap_id)
997 |> Map.put("blocking_user", user)
998
999 activities =
1000 ActivityPub.fetch_activities([], params)
1001 |> Enum.reverse()
1002
1003 conn
1004 |> add_link_headers(:favourites, activities)
1005 |> put_view(StatusView)
1006 |> render("index.json", %{activities: activities, for: user, as: :activity})
1007 end
1008
1009 def bookmarks(%{assigns: %{user: user}} = conn, _) do
1010 user = User.get_by_id(user.id)
1011
1012 activities =
1013 user.bookmarks
1014 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
1015 |> Enum.reverse()
1016
1017 conn
1018 |> put_view(StatusView)
1019 |> render("index.json", %{activities: activities, for: user, as: :activity})
1020 end
1021
1022 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1023 lists = Pleroma.List.for_user(user, opts)
1024 res = ListView.render("lists.json", lists: lists)
1025 json(conn, res)
1026 end
1027
1028 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1029 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1030 res = ListView.render("list.json", list: list)
1031 json(conn, res)
1032 else
1033 _e ->
1034 conn
1035 |> put_status(404)
1036 |> json(%{error: "Record not found"})
1037 end
1038 end
1039
1040 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1041 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1042 res = ListView.render("lists.json", lists: lists)
1043 json(conn, res)
1044 end
1045
1046 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1047 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1048 {:ok, _list} <- Pleroma.List.delete(list) do
1049 json(conn, %{})
1050 else
1051 _e ->
1052 json(conn, "error")
1053 end
1054 end
1055
1056 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1057 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1058 res = ListView.render("list.json", list: list)
1059 json(conn, res)
1060 end
1061 end
1062
1063 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1064 accounts
1065 |> Enum.each(fn account_id ->
1066 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1067 %User{} = followed <- User.get_by_id(account_id) do
1068 Pleroma.List.follow(list, followed)
1069 end
1070 end)
1071
1072 json(conn, %{})
1073 end
1074
1075 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1076 accounts
1077 |> Enum.each(fn account_id ->
1078 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1079 %User{} = followed <- Pleroma.User.get_by_id(account_id) do
1080 Pleroma.List.unfollow(list, followed)
1081 end
1082 end)
1083
1084 json(conn, %{})
1085 end
1086
1087 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1088 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1089 {:ok, users} = Pleroma.List.get_following(list) do
1090 conn
1091 |> put_view(AccountView)
1092 |> render("accounts.json", %{users: users, as: :user})
1093 end
1094 end
1095
1096 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1097 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1098 {:ok, list} <- Pleroma.List.rename(list, title) do
1099 res = ListView.render("list.json", list: list)
1100 json(conn, res)
1101 else
1102 _e ->
1103 json(conn, "error")
1104 end
1105 end
1106
1107 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1108 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1109 params =
1110 params
1111 |> Map.put("type", "Create")
1112 |> Map.put("blocking_user", user)
1113 |> Map.put("muting_user", user)
1114
1115 # we must filter the following list for the user to avoid leaking statuses the user
1116 # does not actually have permission to see (for more info, peruse security issue #270).
1117 activities =
1118 following
1119 |> Enum.filter(fn x -> x in user.following end)
1120 |> ActivityPub.fetch_activities_bounded(following, params)
1121 |> Enum.reverse()
1122
1123 conn
1124 |> put_view(StatusView)
1125 |> render("index.json", %{activities: activities, for: user, as: :activity})
1126 else
1127 _e ->
1128 conn
1129 |> put_status(403)
1130 |> json(%{error: "Error."})
1131 end
1132 end
1133
1134 def index(%{assigns: %{user: user}} = conn, _params) do
1135 token = get_session(conn, :oauth_token)
1136
1137 if user && token do
1138 mastodon_emoji = mastodonized_emoji()
1139
1140 limit = Config.get([:instance, :limit])
1141
1142 accounts =
1143 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1144
1145 flavour = get_user_flavour(user)
1146
1147 initial_state =
1148 %{
1149 meta: %{
1150 streaming_api_base_url:
1151 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1152 access_token: token,
1153 locale: "en",
1154 domain: Pleroma.Web.Endpoint.host(),
1155 admin: "1",
1156 me: "#{user.id}",
1157 unfollow_modal: false,
1158 boost_modal: false,
1159 delete_modal: true,
1160 auto_play_gif: false,
1161 display_sensitive_media: false,
1162 reduce_motion: false,
1163 max_toot_chars: limit,
1164 mascot: "/images/pleroma-fox-tan-smol.png"
1165 },
1166 rights: %{
1167 delete_others_notice: present?(user.info.is_moderator),
1168 admin: present?(user.info.is_admin)
1169 },
1170 compose: %{
1171 me: "#{user.id}",
1172 default_privacy: user.info.default_scope,
1173 default_sensitive: false,
1174 allow_content_types: Config.get([:instance, :allowed_post_formats])
1175 },
1176 media_attachments: %{
1177 accept_content_types: [
1178 ".jpg",
1179 ".jpeg",
1180 ".png",
1181 ".gif",
1182 ".webm",
1183 ".mp4",
1184 ".m4v",
1185 "image\/jpeg",
1186 "image\/png",
1187 "image\/gif",
1188 "video\/webm",
1189 "video\/mp4"
1190 ]
1191 },
1192 settings:
1193 user.info.settings ||
1194 %{
1195 onboarded: true,
1196 home: %{
1197 shows: %{
1198 reblog: true,
1199 reply: true
1200 }
1201 },
1202 notifications: %{
1203 alerts: %{
1204 follow: true,
1205 favourite: true,
1206 reblog: true,
1207 mention: true
1208 },
1209 shows: %{
1210 follow: true,
1211 favourite: true,
1212 reblog: true,
1213 mention: true
1214 },
1215 sounds: %{
1216 follow: true,
1217 favourite: true,
1218 reblog: true,
1219 mention: true
1220 }
1221 }
1222 },
1223 push_subscription: nil,
1224 accounts: accounts,
1225 custom_emojis: mastodon_emoji,
1226 char_limit: limit
1227 }
1228 |> Jason.encode!()
1229
1230 conn
1231 |> put_layout(false)
1232 |> put_view(MastodonView)
1233 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1234 else
1235 conn
1236 |> put_session(:return_to, conn.request_path)
1237 |> redirect(to: "/web/login")
1238 end
1239 end
1240
1241 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1242 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1243
1244 with changeset <- Ecto.Changeset.change(user),
1245 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1246 {:ok, _user} <- User.update_and_set_cache(changeset) do
1247 json(conn, %{})
1248 else
1249 e ->
1250 conn
1251 |> put_resp_content_type("application/json")
1252 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1253 end
1254 end
1255
1256 @supported_flavours ["glitch", "vanilla"]
1257
1258 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1259 when flavour in @supported_flavours do
1260 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1261
1262 with changeset <- Ecto.Changeset.change(user),
1263 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1264 {:ok, user} <- User.update_and_set_cache(changeset),
1265 flavour <- user.info.flavour do
1266 json(conn, flavour)
1267 else
1268 e ->
1269 conn
1270 |> put_resp_content_type("application/json")
1271 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1272 end
1273 end
1274
1275 def set_flavour(conn, _params) do
1276 conn
1277 |> put_status(400)
1278 |> json(%{error: "Unsupported flavour"})
1279 end
1280
1281 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1282 json(conn, get_user_flavour(user))
1283 end
1284
1285 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1286 flavour
1287 end
1288
1289 defp get_user_flavour(_) do
1290 "glitch"
1291 end
1292
1293 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1294 redirect(conn, to: local_mastodon_root_path(conn))
1295 end
1296
1297 @doc "Local Mastodon FE login init action"
1298 def login(conn, %{"code" => auth_token}) do
1299 with {:ok, app} <- get_or_make_app(),
1300 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1301 {:ok, token} <- Token.exchange_token(app, auth) do
1302 conn
1303 |> put_session(:oauth_token, token.token)
1304 |> redirect(to: local_mastodon_root_path(conn))
1305 end
1306 end
1307
1308 @doc "Local Mastodon FE callback action"
1309 def login(conn, _) do
1310 with {:ok, app} <- get_or_make_app() do
1311 path =
1312 o_auth_path(
1313 conn,
1314 :authorize,
1315 response_type: "code",
1316 client_id: app.client_id,
1317 redirect_uri: ".",
1318 scope: Enum.join(app.scopes, " ")
1319 )
1320
1321 redirect(conn, to: path)
1322 end
1323 end
1324
1325 defp local_mastodon_root_path(conn) do
1326 case get_session(conn, :return_to) do
1327 nil ->
1328 mastodon_api_path(conn, :index, ["getting-started"])
1329
1330 return_to ->
1331 delete_session(conn, :return_to)
1332 return_to
1333 end
1334 end
1335
1336 defp get_or_make_app do
1337 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1338 scopes = ["read", "write", "follow", "push"]
1339
1340 with %App{} = app <- Repo.get_by(App, find_attrs) do
1341 {:ok, app} =
1342 if app.scopes == scopes do
1343 {:ok, app}
1344 else
1345 app
1346 |> Ecto.Changeset.change(%{scopes: scopes})
1347 |> Repo.update()
1348 end
1349
1350 {:ok, app}
1351 else
1352 _e ->
1353 cs =
1354 App.register_changeset(
1355 %App{},
1356 Map.put(find_attrs, :scopes, scopes)
1357 )
1358
1359 Repo.insert(cs)
1360 end
1361 end
1362
1363 def logout(conn, _) do
1364 conn
1365 |> clear_session
1366 |> redirect(to: "/")
1367 end
1368
1369 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1370 Logger.debug("Unimplemented, returning unmodified relationship")
1371
1372 with %User{} = target <- User.get_by_id(id) do
1373 conn
1374 |> put_view(AccountView)
1375 |> render("relationship.json", %{user: user, target: target})
1376 end
1377 end
1378
1379 def empty_array(conn, _) do
1380 Logger.debug("Unimplemented, returning an empty array")
1381 json(conn, [])
1382 end
1383
1384 def empty_object(conn, _) do
1385 Logger.debug("Unimplemented, returning an empty object")
1386 json(conn, %{})
1387 end
1388
1389 def get_filters(%{assigns: %{user: user}} = conn, _) do
1390 filters = Filter.get_filters(user)
1391 res = FilterView.render("filters.json", filters: filters)
1392 json(conn, res)
1393 end
1394
1395 def create_filter(
1396 %{assigns: %{user: user}} = conn,
1397 %{"phrase" => phrase, "context" => context} = params
1398 ) do
1399 query = %Filter{
1400 user_id: user.id,
1401 phrase: phrase,
1402 context: context,
1403 hide: Map.get(params, "irreversible", nil),
1404 whole_word: Map.get(params, "boolean", true)
1405 # expires_at
1406 }
1407
1408 {:ok, response} = Filter.create(query)
1409 res = FilterView.render("filter.json", filter: response)
1410 json(conn, res)
1411 end
1412
1413 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1414 filter = Filter.get(filter_id, user)
1415 res = FilterView.render("filter.json", filter: filter)
1416 json(conn, res)
1417 end
1418
1419 def update_filter(
1420 %{assigns: %{user: user}} = conn,
1421 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1422 ) do
1423 query = %Filter{
1424 user_id: user.id,
1425 filter_id: filter_id,
1426 phrase: phrase,
1427 context: context,
1428 hide: Map.get(params, "irreversible", nil),
1429 whole_word: Map.get(params, "boolean", true)
1430 # expires_at
1431 }
1432
1433 {:ok, response} = Filter.update(query)
1434 res = FilterView.render("filter.json", filter: response)
1435 json(conn, res)
1436 end
1437
1438 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1439 query = %Filter{
1440 user_id: user.id,
1441 filter_id: filter_id
1442 }
1443
1444 {:ok, _} = Filter.delete(query)
1445 json(conn, %{})
1446 end
1447
1448 # fallback action
1449 #
1450 def errors(conn, {:error, :not_found}) do
1451 conn
1452 |> put_status(404)
1453 |> json(%{error: "Record not found"})
1454 end
1455
1456 def errors(conn, _) do
1457 conn
1458 |> put_status(500)
1459 |> json("Something went wrong")
1460 end
1461
1462 def suggestions(%{assigns: %{user: user}} = conn, _) do
1463 suggestions = Config.get(:suggestions)
1464
1465 if Keyword.get(suggestions, :enabled, false) do
1466 api = Keyword.get(suggestions, :third_party_engine, "")
1467 timeout = Keyword.get(suggestions, :timeout, 5000)
1468 limit = Keyword.get(suggestions, :limit, 23)
1469
1470 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1471
1472 user = user.nickname
1473
1474 url =
1475 api
1476 |> String.replace("{{host}}", host)
1477 |> String.replace("{{user}}", user)
1478
1479 with {:ok, %{status: 200, body: body}} <-
1480 @httpoison.get(
1481 url,
1482 [],
1483 adapter: [
1484 recv_timeout: timeout,
1485 pool: :default
1486 ]
1487 ),
1488 {:ok, data} <- Jason.decode(body) do
1489 data =
1490 data
1491 |> Enum.slice(0, limit)
1492 |> Enum.map(fn x ->
1493 Map.put(
1494 x,
1495 "id",
1496 case User.get_or_fetch(x["acct"]) do
1497 %{id: id} -> id
1498 _ -> 0
1499 end
1500 )
1501 end)
1502 |> Enum.map(fn x ->
1503 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1504 end)
1505 |> Enum.map(fn x ->
1506 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1507 end)
1508
1509 conn
1510 |> json(data)
1511 else
1512 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1513 end
1514 else
1515 json(conn, [])
1516 end
1517 end
1518
1519 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1520 with %Activity{} = activity <- Activity.get_by_id(status_id),
1521 true <- Visibility.visible_for_user?(activity, user) do
1522 data =
1523 StatusView.render(
1524 "card.json",
1525 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1526 )
1527
1528 json(conn, data)
1529 else
1530 _e ->
1531 %{}
1532 end
1533 end
1534
1535 def reports(%{assigns: %{user: user}} = conn, params) do
1536 case CommonAPI.report(user, params) do
1537 {:ok, activity} ->
1538 conn
1539 |> put_view(ReportView)
1540 |> try_render("report.json", %{activity: activity})
1541
1542 {:error, err} ->
1543 conn
1544 |> put_status(:bad_request)
1545 |> json(%{error: err})
1546 end
1547 end
1548
1549 def try_render(conn, target, params)
1550 when is_binary(target) do
1551 res = render(conn, target, params)
1552
1553 if res == nil do
1554 conn
1555 |> put_status(501)
1556 |> json(%{error: "Can't display this activity"})
1557 else
1558 res
1559 end
1560 end
1561
1562 def try_render(conn, _, _) do
1563 conn
1564 |> put_status(501)
1565 |> json(%{error: "Can't display this activity"})
1566 end
1567
1568 defp present?(nil), do: false
1569 defp present?(false), do: false
1570 defp present?(_), do: true
1571 end