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