Handle `scheduled_at` on status creation.
[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 scheduled_at = params["scheduled_at"]
429
430 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
431 {:ok, scheduled_activity} =
432 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
433 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at})
434 end)
435
436 conn
437 |> put_view(ScheduledActivityView)
438 |> render("show.json", %{scheduled_activity: scheduled_activity})
439 else
440 params = Map.drop(params, ["scheduled_at"])
441
442 {:ok, activity} =
443 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
444 CommonAPI.post(user, params)
445 end)
446
447 conn
448 |> put_view(StatusView)
449 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
450 end
451 end
452
453 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
454 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
455 json(conn, %{})
456 else
457 _e ->
458 conn
459 |> put_status(403)
460 |> json(%{error: "Can't delete this post"})
461 end
462 end
463
464 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
465 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user) do
466 conn
467 |> put_view(StatusView)
468 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
469 end
470 end
471
472 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
473 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
474 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
475 conn
476 |> put_view(StatusView)
477 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
478 end
479 end
480
481 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
482 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
483 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
484 conn
485 |> put_view(StatusView)
486 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
487 end
488 end
489
490 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
491 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
492 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
493 conn
494 |> put_view(StatusView)
495 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
496 end
497 end
498
499 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
500 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
501 conn
502 |> put_view(StatusView)
503 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
504 else
505 {:error, reason} ->
506 conn
507 |> put_resp_content_type("application/json")
508 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
509 end
510 end
511
512 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
513 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
514 conn
515 |> put_view(StatusView)
516 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
517 end
518 end
519
520 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
521 with %Activity{} = activity <- Activity.get_by_id(id),
522 %User{} = user <- User.get_by_nickname(user.nickname),
523 true <- Visibility.visible_for_user?(activity, user),
524 {:ok, user} <- User.bookmark(user, activity.data["object"]["id"]) do
525 conn
526 |> put_view(StatusView)
527 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
528 end
529 end
530
531 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
532 with %Activity{} = activity <- Activity.get_by_id(id),
533 %User{} = user <- User.get_by_nickname(user.nickname),
534 true <- Visibility.visible_for_user?(activity, user),
535 {:ok, user} <- User.unbookmark(user, activity.data["object"]["id"]) do
536 conn
537 |> put_view(StatusView)
538 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
539 end
540 end
541
542 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
543 activity = Activity.get_by_id(id)
544
545 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
546 conn
547 |> put_view(StatusView)
548 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
549 else
550 {:error, reason} ->
551 conn
552 |> put_resp_content_type("application/json")
553 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
554 end
555 end
556
557 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
558 activity = Activity.get_by_id(id)
559
560 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
561 conn
562 |> put_view(StatusView)
563 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
564 end
565 end
566
567 def notifications(%{assigns: %{user: user}} = conn, params) do
568 notifications = MastodonAPI.get_notifications(user, params)
569
570 conn
571 |> add_link_headers(:notifications, notifications)
572 |> put_view(NotificationView)
573 |> render("index.json", %{notifications: notifications, for: user})
574 end
575
576 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
577 with {:ok, notification} <- Notification.get(user, id) do
578 conn
579 |> put_view(NotificationView)
580 |> render("show.json", %{notification: notification, for: user})
581 else
582 {:error, reason} ->
583 conn
584 |> put_resp_content_type("application/json")
585 |> send_resp(403, Jason.encode!(%{"error" => reason}))
586 end
587 end
588
589 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
590 Notification.clear(user)
591 json(conn, %{})
592 end
593
594 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
595 with {:ok, _notif} <- Notification.dismiss(user, id) do
596 json(conn, %{})
597 else
598 {:error, reason} ->
599 conn
600 |> put_resp_content_type("application/json")
601 |> send_resp(403, Jason.encode!(%{"error" => reason}))
602 end
603 end
604
605 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
606 id = List.wrap(id)
607 q = from(u in User, where: u.id in ^id)
608 targets = Repo.all(q)
609
610 conn
611 |> put_view(AccountView)
612 |> render("relationships.json", %{user: user, targets: targets})
613 end
614
615 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
616 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
617
618 def update_media(%{assigns: %{user: user}} = conn, data) do
619 with %Object{} = object <- Repo.get(Object, data["id"]),
620 true <- Object.authorize_mutation(object, user),
621 true <- is_binary(data["description"]),
622 description <- data["description"] do
623 new_data = %{object.data | "name" => description}
624
625 {:ok, _} =
626 object
627 |> Object.change(%{data: new_data})
628 |> Repo.update()
629
630 attachment_data = Map.put(new_data, "id", object.id)
631
632 conn
633 |> put_view(StatusView)
634 |> render("attachment.json", %{attachment: attachment_data})
635 end
636 end
637
638 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
639 with {:ok, object} <-
640 ActivityPub.upload(
641 file,
642 actor: User.ap_id(user),
643 description: Map.get(data, "description")
644 ) do
645 attachment_data = Map.put(object.data, "id", object.id)
646
647 conn
648 |> put_view(StatusView)
649 |> render("attachment.json", %{attachment: attachment_data})
650 end
651 end
652
653 def favourited_by(conn, %{"id" => id}) do
654 with %Activity{data: %{"object" => %{"likes" => likes}}} <- Activity.get_by_id(id) do
655 q = from(u in User, where: u.ap_id in ^likes)
656 users = Repo.all(q)
657
658 conn
659 |> put_view(AccountView)
660 |> render(AccountView, "accounts.json", %{users: users, as: :user})
661 else
662 _ -> json(conn, [])
663 end
664 end
665
666 def reblogged_by(conn, %{"id" => id}) do
667 with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Activity.get_by_id(id) do
668 q = from(u in User, where: u.ap_id in ^announces)
669 users = Repo.all(q)
670
671 conn
672 |> put_view(AccountView)
673 |> render("accounts.json", %{users: users, as: :user})
674 else
675 _ -> json(conn, [])
676 end
677 end
678
679 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
680 local_only = params["local"] in [true, "True", "true", "1"]
681
682 tags =
683 [params["tag"], params["any"]]
684 |> List.flatten()
685 |> Enum.uniq()
686 |> Enum.filter(& &1)
687 |> Enum.map(&String.downcase(&1))
688
689 tag_all =
690 params["all"] ||
691 []
692 |> Enum.map(&String.downcase(&1))
693
694 tag_reject =
695 params["none"] ||
696 []
697 |> Enum.map(&String.downcase(&1))
698
699 activities =
700 params
701 |> Map.put("type", "Create")
702 |> Map.put("local_only", local_only)
703 |> Map.put("blocking_user", user)
704 |> Map.put("muting_user", user)
705 |> Map.put("tag", tags)
706 |> Map.put("tag_all", tag_all)
707 |> Map.put("tag_reject", tag_reject)
708 |> ActivityPub.fetch_public_activities()
709 |> Enum.reverse()
710
711 conn
712 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
713 |> put_view(StatusView)
714 |> render("index.json", %{activities: activities, for: user, as: :activity})
715 end
716
717 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
718 with %User{} = user <- User.get_by_id(id),
719 followers <- MastodonAPI.get_followers(user, params) do
720 followers =
721 cond do
722 for_user && user.id == for_user.id -> followers
723 user.info.hide_followers -> []
724 true -> followers
725 end
726
727 conn
728 |> add_link_headers(:followers, followers, user)
729 |> put_view(AccountView)
730 |> render("accounts.json", %{users: followers, as: :user})
731 end
732 end
733
734 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
735 with %User{} = user <- User.get_by_id(id),
736 followers <- MastodonAPI.get_friends(user, params) do
737 followers =
738 cond do
739 for_user && user.id == for_user.id -> followers
740 user.info.hide_follows -> []
741 true -> followers
742 end
743
744 conn
745 |> add_link_headers(:following, followers, user)
746 |> put_view(AccountView)
747 |> render("accounts.json", %{users: followers, as: :user})
748 end
749 end
750
751 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
752 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
753 conn
754 |> put_view(AccountView)
755 |> render("accounts.json", %{users: follow_requests, as: :user})
756 end
757 end
758
759 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
760 with %User{} = follower <- User.get_by_id(id),
761 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
762 conn
763 |> put_view(AccountView)
764 |> render("relationship.json", %{user: followed, target: follower})
765 else
766 {:error, message} ->
767 conn
768 |> put_resp_content_type("application/json")
769 |> send_resp(403, Jason.encode!(%{"error" => message}))
770 end
771 end
772
773 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
774 with %User{} = follower <- User.get_by_id(id),
775 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
776 conn
777 |> put_view(AccountView)
778 |> render("relationship.json", %{user: followed, target: follower})
779 else
780 {:error, message} ->
781 conn
782 |> put_resp_content_type("application/json")
783 |> send_resp(403, Jason.encode!(%{"error" => message}))
784 end
785 end
786
787 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
788 with %User{} = followed <- User.get_by_id(id),
789 false <- User.following?(follower, followed),
790 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
791 conn
792 |> put_view(AccountView)
793 |> render("relationship.json", %{user: follower, target: followed})
794 else
795 true ->
796 followed = User.get_cached_by_id(id)
797
798 {:ok, follower} =
799 case conn.params["reblogs"] do
800 true -> CommonAPI.show_reblogs(follower, followed)
801 false -> CommonAPI.hide_reblogs(follower, followed)
802 end
803
804 conn
805 |> put_view(AccountView)
806 |> render("relationship.json", %{user: follower, target: followed})
807
808 {:error, message} ->
809 conn
810 |> put_resp_content_type("application/json")
811 |> send_resp(403, Jason.encode!(%{"error" => message}))
812 end
813 end
814
815 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
816 with %User{} = followed <- User.get_by_nickname(uri),
817 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
818 conn
819 |> put_view(AccountView)
820 |> render("account.json", %{user: followed, for: follower})
821 else
822 {:error, message} ->
823 conn
824 |> put_resp_content_type("application/json")
825 |> send_resp(403, Jason.encode!(%{"error" => message}))
826 end
827 end
828
829 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
830 with %User{} = followed <- User.get_by_id(id),
831 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
832 conn
833 |> put_view(AccountView)
834 |> render("relationship.json", %{user: follower, target: followed})
835 end
836 end
837
838 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
839 with %User{} = muted <- User.get_by_id(id),
840 {:ok, muter} <- User.mute(muter, muted) do
841 conn
842 |> put_view(AccountView)
843 |> render("relationship.json", %{user: muter, target: muted})
844 else
845 {:error, message} ->
846 conn
847 |> put_resp_content_type("application/json")
848 |> send_resp(403, Jason.encode!(%{"error" => message}))
849 end
850 end
851
852 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
853 with %User{} = muted <- User.get_by_id(id),
854 {:ok, muter} <- User.unmute(muter, muted) do
855 conn
856 |> put_view(AccountView)
857 |> render("relationship.json", %{user: muter, target: muted})
858 else
859 {:error, message} ->
860 conn
861 |> put_resp_content_type("application/json")
862 |> send_resp(403, Jason.encode!(%{"error" => message}))
863 end
864 end
865
866 def mutes(%{assigns: %{user: user}} = conn, _) do
867 with muted_accounts <- User.muted_users(user) do
868 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
869 json(conn, res)
870 end
871 end
872
873 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
874 with %User{} = blocked <- User.get_by_id(id),
875 {:ok, blocker} <- User.block(blocker, blocked),
876 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
877 conn
878 |> put_view(AccountView)
879 |> render("relationship.json", %{user: blocker, target: blocked})
880 else
881 {:error, message} ->
882 conn
883 |> put_resp_content_type("application/json")
884 |> send_resp(403, Jason.encode!(%{"error" => message}))
885 end
886 end
887
888 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
889 with %User{} = blocked <- User.get_by_id(id),
890 {:ok, blocker} <- User.unblock(blocker, blocked),
891 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
892 conn
893 |> put_view(AccountView)
894 |> render("relationship.json", %{user: blocker, target: blocked})
895 else
896 {:error, message} ->
897 conn
898 |> put_resp_content_type("application/json")
899 |> send_resp(403, Jason.encode!(%{"error" => message}))
900 end
901 end
902
903 def blocks(%{assigns: %{user: user}} = conn, _) do
904 with blocked_accounts <- User.blocked_users(user) do
905 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
906 json(conn, res)
907 end
908 end
909
910 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
911 json(conn, info.domain_blocks || [])
912 end
913
914 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
915 User.block_domain(blocker, domain)
916 json(conn, %{})
917 end
918
919 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
920 User.unblock_domain(blocker, domain)
921 json(conn, %{})
922 end
923
924 def status_search(user, query) do
925 fetched =
926 if Regex.match?(~r/https?:/, query) do
927 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
928 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
929 true <- Visibility.visible_for_user?(activity, user) do
930 [activity]
931 else
932 _e -> []
933 end
934 end || []
935
936 q =
937 from(
938 a in Activity,
939 where: fragment("?->>'type' = 'Create'", a.data),
940 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
941 where:
942 fragment(
943 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
944 a.data,
945 ^query
946 ),
947 limit: 20,
948 order_by: [desc: :id]
949 )
950
951 Repo.all(q) ++ fetched
952 end
953
954 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
955 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
956
957 statuses = status_search(user, query)
958
959 tags_path = Web.base_url() <> "/tag/"
960
961 tags =
962 query
963 |> String.split()
964 |> Enum.uniq()
965 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
966 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
967 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
968
969 res = %{
970 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
971 "statuses" =>
972 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
973 "hashtags" => tags
974 }
975
976 json(conn, res)
977 end
978
979 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
980 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
981
982 statuses = status_search(user, query)
983
984 tags =
985 query
986 |> String.split()
987 |> Enum.uniq()
988 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
989 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
990
991 res = %{
992 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
993 "statuses" =>
994 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
995 "hashtags" => tags
996 }
997
998 json(conn, res)
999 end
1000
1001 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1002 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1003
1004 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1005
1006 json(conn, res)
1007 end
1008
1009 def favourites(%{assigns: %{user: user}} = conn, params) do
1010 params =
1011 params
1012 |> Map.put("type", "Create")
1013 |> Map.put("favorited_by", user.ap_id)
1014 |> Map.put("blocking_user", user)
1015
1016 activities =
1017 ActivityPub.fetch_activities([], params)
1018 |> Enum.reverse()
1019
1020 conn
1021 |> add_link_headers(:favourites, activities)
1022 |> put_view(StatusView)
1023 |> render("index.json", %{activities: activities, for: user, as: :activity})
1024 end
1025
1026 def bookmarks(%{assigns: %{user: user}} = conn, _) do
1027 user = User.get_by_id(user.id)
1028
1029 activities =
1030 user.bookmarks
1031 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
1032 |> Enum.reverse()
1033
1034 conn
1035 |> put_view(StatusView)
1036 |> render("index.json", %{activities: activities, for: user, as: :activity})
1037 end
1038
1039 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1040 lists = Pleroma.List.for_user(user, opts)
1041 res = ListView.render("lists.json", lists: lists)
1042 json(conn, res)
1043 end
1044
1045 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1046 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1047 res = ListView.render("list.json", list: list)
1048 json(conn, res)
1049 else
1050 _e ->
1051 conn
1052 |> put_status(404)
1053 |> json(%{error: "Record not found"})
1054 end
1055 end
1056
1057 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1058 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1059 res = ListView.render("lists.json", lists: lists)
1060 json(conn, res)
1061 end
1062
1063 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1064 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1065 {:ok, _list} <- Pleroma.List.delete(list) do
1066 json(conn, %{})
1067 else
1068 _e ->
1069 json(conn, "error")
1070 end
1071 end
1072
1073 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1074 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1075 res = ListView.render("list.json", list: list)
1076 json(conn, res)
1077 end
1078 end
1079
1080 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1081 accounts
1082 |> Enum.each(fn account_id ->
1083 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1084 %User{} = followed <- User.get_by_id(account_id) do
1085 Pleroma.List.follow(list, followed)
1086 end
1087 end)
1088
1089 json(conn, %{})
1090 end
1091
1092 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1093 accounts
1094 |> Enum.each(fn account_id ->
1095 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1096 %User{} = followed <- Pleroma.User.get_by_id(account_id) do
1097 Pleroma.List.unfollow(list, followed)
1098 end
1099 end)
1100
1101 json(conn, %{})
1102 end
1103
1104 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1105 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1106 {:ok, users} = Pleroma.List.get_following(list) do
1107 conn
1108 |> put_view(AccountView)
1109 |> render("accounts.json", %{users: users, as: :user})
1110 end
1111 end
1112
1113 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1114 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1115 {:ok, list} <- Pleroma.List.rename(list, title) do
1116 res = ListView.render("list.json", list: list)
1117 json(conn, res)
1118 else
1119 _e ->
1120 json(conn, "error")
1121 end
1122 end
1123
1124 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1125 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1126 params =
1127 params
1128 |> Map.put("type", "Create")
1129 |> Map.put("blocking_user", user)
1130 |> Map.put("muting_user", user)
1131
1132 # we must filter the following list for the user to avoid leaking statuses the user
1133 # does not actually have permission to see (for more info, peruse security issue #270).
1134 activities =
1135 following
1136 |> Enum.filter(fn x -> x in user.following end)
1137 |> ActivityPub.fetch_activities_bounded(following, params)
1138 |> Enum.reverse()
1139
1140 conn
1141 |> put_view(StatusView)
1142 |> render("index.json", %{activities: activities, for: user, as: :activity})
1143 else
1144 _e ->
1145 conn
1146 |> put_status(403)
1147 |> json(%{error: "Error."})
1148 end
1149 end
1150
1151 def index(%{assigns: %{user: user}} = conn, _params) do
1152 token = get_session(conn, :oauth_token)
1153
1154 if user && token do
1155 mastodon_emoji = mastodonized_emoji()
1156
1157 limit = Config.get([:instance, :limit])
1158
1159 accounts =
1160 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1161
1162 flavour = get_user_flavour(user)
1163
1164 initial_state =
1165 %{
1166 meta: %{
1167 streaming_api_base_url:
1168 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1169 access_token: token,
1170 locale: "en",
1171 domain: Pleroma.Web.Endpoint.host(),
1172 admin: "1",
1173 me: "#{user.id}",
1174 unfollow_modal: false,
1175 boost_modal: false,
1176 delete_modal: true,
1177 auto_play_gif: false,
1178 display_sensitive_media: false,
1179 reduce_motion: false,
1180 max_toot_chars: limit,
1181 mascot: "/images/pleroma-fox-tan-smol.png"
1182 },
1183 rights: %{
1184 delete_others_notice: present?(user.info.is_moderator),
1185 admin: present?(user.info.is_admin)
1186 },
1187 compose: %{
1188 me: "#{user.id}",
1189 default_privacy: user.info.default_scope,
1190 default_sensitive: false,
1191 allow_content_types: Config.get([:instance, :allowed_post_formats])
1192 },
1193 media_attachments: %{
1194 accept_content_types: [
1195 ".jpg",
1196 ".jpeg",
1197 ".png",
1198 ".gif",
1199 ".webm",
1200 ".mp4",
1201 ".m4v",
1202 "image\/jpeg",
1203 "image\/png",
1204 "image\/gif",
1205 "video\/webm",
1206 "video\/mp4"
1207 ]
1208 },
1209 settings:
1210 user.info.settings ||
1211 %{
1212 onboarded: true,
1213 home: %{
1214 shows: %{
1215 reblog: true,
1216 reply: true
1217 }
1218 },
1219 notifications: %{
1220 alerts: %{
1221 follow: true,
1222 favourite: true,
1223 reblog: true,
1224 mention: true
1225 },
1226 shows: %{
1227 follow: true,
1228 favourite: true,
1229 reblog: true,
1230 mention: true
1231 },
1232 sounds: %{
1233 follow: true,
1234 favourite: true,
1235 reblog: true,
1236 mention: true
1237 }
1238 }
1239 },
1240 push_subscription: nil,
1241 accounts: accounts,
1242 custom_emojis: mastodon_emoji,
1243 char_limit: limit
1244 }
1245 |> Jason.encode!()
1246
1247 conn
1248 |> put_layout(false)
1249 |> put_view(MastodonView)
1250 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1251 else
1252 conn
1253 |> put_session(:return_to, conn.request_path)
1254 |> redirect(to: "/web/login")
1255 end
1256 end
1257
1258 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1259 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1260
1261 with changeset <- Ecto.Changeset.change(user),
1262 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1263 {:ok, _user} <- User.update_and_set_cache(changeset) do
1264 json(conn, %{})
1265 else
1266 e ->
1267 conn
1268 |> put_resp_content_type("application/json")
1269 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1270 end
1271 end
1272
1273 @supported_flavours ["glitch", "vanilla"]
1274
1275 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1276 when flavour in @supported_flavours do
1277 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1278
1279 with changeset <- Ecto.Changeset.change(user),
1280 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1281 {:ok, user} <- User.update_and_set_cache(changeset),
1282 flavour <- user.info.flavour do
1283 json(conn, flavour)
1284 else
1285 e ->
1286 conn
1287 |> put_resp_content_type("application/json")
1288 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1289 end
1290 end
1291
1292 def set_flavour(conn, _params) do
1293 conn
1294 |> put_status(400)
1295 |> json(%{error: "Unsupported flavour"})
1296 end
1297
1298 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1299 json(conn, get_user_flavour(user))
1300 end
1301
1302 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1303 flavour
1304 end
1305
1306 defp get_user_flavour(_) do
1307 "glitch"
1308 end
1309
1310 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1311 redirect(conn, to: local_mastodon_root_path(conn))
1312 end
1313
1314 @doc "Local Mastodon FE login init action"
1315 def login(conn, %{"code" => auth_token}) do
1316 with {:ok, app} <- get_or_make_app(),
1317 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1318 {:ok, token} <- Token.exchange_token(app, auth) do
1319 conn
1320 |> put_session(:oauth_token, token.token)
1321 |> redirect(to: local_mastodon_root_path(conn))
1322 end
1323 end
1324
1325 @doc "Local Mastodon FE callback action"
1326 def login(conn, _) do
1327 with {:ok, app} <- get_or_make_app() do
1328 path =
1329 o_auth_path(
1330 conn,
1331 :authorize,
1332 response_type: "code",
1333 client_id: app.client_id,
1334 redirect_uri: ".",
1335 scope: Enum.join(app.scopes, " ")
1336 )
1337
1338 redirect(conn, to: path)
1339 end
1340 end
1341
1342 defp local_mastodon_root_path(conn) do
1343 case get_session(conn, :return_to) do
1344 nil ->
1345 mastodon_api_path(conn, :index, ["getting-started"])
1346
1347 return_to ->
1348 delete_session(conn, :return_to)
1349 return_to
1350 end
1351 end
1352
1353 defp get_or_make_app do
1354 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1355 scopes = ["read", "write", "follow", "push"]
1356
1357 with %App{} = app <- Repo.get_by(App, find_attrs) do
1358 {:ok, app} =
1359 if app.scopes == scopes do
1360 {:ok, app}
1361 else
1362 app
1363 |> Ecto.Changeset.change(%{scopes: scopes})
1364 |> Repo.update()
1365 end
1366
1367 {:ok, app}
1368 else
1369 _e ->
1370 cs =
1371 App.register_changeset(
1372 %App{},
1373 Map.put(find_attrs, :scopes, scopes)
1374 )
1375
1376 Repo.insert(cs)
1377 end
1378 end
1379
1380 def logout(conn, _) do
1381 conn
1382 |> clear_session
1383 |> redirect(to: "/")
1384 end
1385
1386 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1387 Logger.debug("Unimplemented, returning unmodified relationship")
1388
1389 with %User{} = target <- User.get_by_id(id) do
1390 conn
1391 |> put_view(AccountView)
1392 |> render("relationship.json", %{user: user, target: target})
1393 end
1394 end
1395
1396 def empty_array(conn, _) do
1397 Logger.debug("Unimplemented, returning an empty array")
1398 json(conn, [])
1399 end
1400
1401 def empty_object(conn, _) do
1402 Logger.debug("Unimplemented, returning an empty object")
1403 json(conn, %{})
1404 end
1405
1406 def get_filters(%{assigns: %{user: user}} = conn, _) do
1407 filters = Filter.get_filters(user)
1408 res = FilterView.render("filters.json", filters: filters)
1409 json(conn, res)
1410 end
1411
1412 def create_filter(
1413 %{assigns: %{user: user}} = conn,
1414 %{"phrase" => phrase, "context" => context} = params
1415 ) do
1416 query = %Filter{
1417 user_id: user.id,
1418 phrase: phrase,
1419 context: context,
1420 hide: Map.get(params, "irreversible", nil),
1421 whole_word: Map.get(params, "boolean", true)
1422 # expires_at
1423 }
1424
1425 {:ok, response} = Filter.create(query)
1426 res = FilterView.render("filter.json", filter: response)
1427 json(conn, res)
1428 end
1429
1430 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1431 filter = Filter.get(filter_id, user)
1432 res = FilterView.render("filter.json", filter: filter)
1433 json(conn, res)
1434 end
1435
1436 def update_filter(
1437 %{assigns: %{user: user}} = conn,
1438 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1439 ) do
1440 query = %Filter{
1441 user_id: user.id,
1442 filter_id: filter_id,
1443 phrase: phrase,
1444 context: context,
1445 hide: Map.get(params, "irreversible", nil),
1446 whole_word: Map.get(params, "boolean", true)
1447 # expires_at
1448 }
1449
1450 {:ok, response} = Filter.update(query)
1451 res = FilterView.render("filter.json", filter: response)
1452 json(conn, res)
1453 end
1454
1455 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1456 query = %Filter{
1457 user_id: user.id,
1458 filter_id: filter_id
1459 }
1460
1461 {:ok, _} = Filter.delete(query)
1462 json(conn, %{})
1463 end
1464
1465 # fallback action
1466 #
1467 def errors(conn, {:error, :not_found}) do
1468 conn
1469 |> put_status(404)
1470 |> json(%{error: "Record not found"})
1471 end
1472
1473 def errors(conn, _) do
1474 conn
1475 |> put_status(500)
1476 |> json("Something went wrong")
1477 end
1478
1479 def suggestions(%{assigns: %{user: user}} = conn, _) do
1480 suggestions = Config.get(:suggestions)
1481
1482 if Keyword.get(suggestions, :enabled, false) do
1483 api = Keyword.get(suggestions, :third_party_engine, "")
1484 timeout = Keyword.get(suggestions, :timeout, 5000)
1485 limit = Keyword.get(suggestions, :limit, 23)
1486
1487 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1488
1489 user = user.nickname
1490
1491 url =
1492 api
1493 |> String.replace("{{host}}", host)
1494 |> String.replace("{{user}}", user)
1495
1496 with {:ok, %{status: 200, body: body}} <-
1497 @httpoison.get(
1498 url,
1499 [],
1500 adapter: [
1501 recv_timeout: timeout,
1502 pool: :default
1503 ]
1504 ),
1505 {:ok, data} <- Jason.decode(body) do
1506 data =
1507 data
1508 |> Enum.slice(0, limit)
1509 |> Enum.map(fn x ->
1510 Map.put(
1511 x,
1512 "id",
1513 case User.get_or_fetch(x["acct"]) do
1514 %{id: id} -> id
1515 _ -> 0
1516 end
1517 )
1518 end)
1519 |> Enum.map(fn x ->
1520 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1521 end)
1522 |> Enum.map(fn x ->
1523 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1524 end)
1525
1526 conn
1527 |> json(data)
1528 else
1529 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1530 end
1531 else
1532 json(conn, [])
1533 end
1534 end
1535
1536 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1537 with %Activity{} = activity <- Activity.get_by_id(status_id),
1538 true <- Visibility.visible_for_user?(activity, user) do
1539 data =
1540 StatusView.render(
1541 "card.json",
1542 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1543 )
1544
1545 json(conn, data)
1546 else
1547 _e ->
1548 %{}
1549 end
1550 end
1551
1552 def reports(%{assigns: %{user: user}} = conn, params) do
1553 case CommonAPI.report(user, params) do
1554 {:ok, activity} ->
1555 conn
1556 |> put_view(ReportView)
1557 |> try_render("report.json", %{activity: activity})
1558
1559 {:error, err} ->
1560 conn
1561 |> put_status(:bad_request)
1562 |> json(%{error: err})
1563 end
1564 end
1565
1566 def try_render(conn, target, params)
1567 when is_binary(target) do
1568 res = render(conn, target, params)
1569
1570 if res == nil do
1571 conn
1572 |> put_status(501)
1573 |> json(%{error: "Can't display this activity"})
1574 else
1575 res
1576 end
1577 end
1578
1579 def try_render(conn, _, _) do
1580 conn
1581 |> put_status(501)
1582 |> json(%{error: "Can't display this activity"})
1583 end
1584
1585 defp present?(nil), do: false
1586 defp present?(false), do: false
1587 defp present?(_), do: true
1588 end