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