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