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