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