Merge branch 'feature/788-separate-email-addresses' into 'develop'
[akkoma] / lib / pleroma / web / mastodon_api / mastodon_api_controller.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
6 use Pleroma.Web, :controller
7
8 alias Ecto.Changeset
9 alias Pleroma.Activity
10 alias Pleroma.Config
11 alias Pleroma.Filter
12 alias Pleroma.Notification
13 alias Pleroma.Object
14 alias Pleroma.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} <- {:followed, User.get_cached_by_id(id)},
819 {_, true} <- {:followed, follower.id != followed.id},
820 false <- User.following?(follower, followed),
821 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
822 conn
823 |> put_view(AccountView)
824 |> render("relationship.json", %{user: follower, target: followed})
825 else
826 {:followed, _} ->
827 {:error, :not_found}
828
829 true ->
830 followed = User.get_cached_by_id(id)
831
832 {:ok, follower} =
833 case conn.params["reblogs"] do
834 true -> CommonAPI.show_reblogs(follower, followed)
835 false -> CommonAPI.hide_reblogs(follower, followed)
836 end
837
838 conn
839 |> put_view(AccountView)
840 |> render("relationship.json", %{user: follower, target: followed})
841
842 {:error, message} ->
843 conn
844 |> put_resp_content_type("application/json")
845 |> send_resp(403, Jason.encode!(%{"error" => message}))
846 end
847 end
848
849 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
850 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
851 {_, true} <- {:followed, follower.id != followed.id},
852 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
853 conn
854 |> put_view(AccountView)
855 |> render("account.json", %{user: followed, for: follower})
856 else
857 {:followed, _} ->
858 {:error, :not_found}
859
860 {:error, message} ->
861 conn
862 |> put_resp_content_type("application/json")
863 |> send_resp(403, Jason.encode!(%{"error" => message}))
864 end
865 end
866
867 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
868 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
869 {_, true} <- {:followed, follower.id != followed.id},
870 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
871 conn
872 |> put_view(AccountView)
873 |> render("relationship.json", %{user: follower, target: followed})
874 else
875 {:followed, _} ->
876 {:error, :not_found}
877
878 error ->
879 error
880 end
881 end
882
883 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
884 with %User{} = muted <- User.get_by_id(id),
885 {:ok, muter} <- User.mute(muter, muted) do
886 conn
887 |> put_view(AccountView)
888 |> render("relationship.json", %{user: muter, target: muted})
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 unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
898 with %User{} = muted <- User.get_by_id(id),
899 {:ok, muter} <- User.unmute(muter, muted) do
900 conn
901 |> put_view(AccountView)
902 |> render("relationship.json", %{user: muter, target: muted})
903 else
904 {:error, message} ->
905 conn
906 |> put_resp_content_type("application/json")
907 |> send_resp(403, Jason.encode!(%{"error" => message}))
908 end
909 end
910
911 def mutes(%{assigns: %{user: user}} = conn, _) do
912 with muted_accounts <- User.muted_users(user) do
913 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
914 json(conn, res)
915 end
916 end
917
918 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
919 with %User{} = blocked <- User.get_by_id(id),
920 {:ok, blocker} <- User.block(blocker, blocked),
921 {:ok, _activity} <- ActivityPub.block(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 unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
934 with %User{} = blocked <- User.get_by_id(id),
935 {:ok, blocker} <- User.unblock(blocker, blocked),
936 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
937 conn
938 |> put_view(AccountView)
939 |> render("relationship.json", %{user: blocker, target: blocked})
940 else
941 {:error, message} ->
942 conn
943 |> put_resp_content_type("application/json")
944 |> send_resp(403, Jason.encode!(%{"error" => message}))
945 end
946 end
947
948 def blocks(%{assigns: %{user: user}} = conn, _) do
949 with blocked_accounts <- User.blocked_users(user) do
950 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
951 json(conn, res)
952 end
953 end
954
955 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
956 json(conn, info.domain_blocks || [])
957 end
958
959 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
960 User.block_domain(blocker, domain)
961 json(conn, %{})
962 end
963
964 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
965 User.unblock_domain(blocker, domain)
966 json(conn, %{})
967 end
968
969 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
970 with %User{} = subscription_target <- User.get_cached_by_id(id),
971 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
972 conn
973 |> put_view(AccountView)
974 |> render("relationship.json", %{user: user, target: subscription_target})
975 else
976 {:error, message} ->
977 conn
978 |> put_resp_content_type("application/json")
979 |> send_resp(403, Jason.encode!(%{"error" => message}))
980 end
981 end
982
983 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
984 with %User{} = subscription_target <- User.get_cached_by_id(id),
985 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
986 conn
987 |> put_view(AccountView)
988 |> render("relationship.json", %{user: user, target: subscription_target})
989 else
990 {:error, message} ->
991 conn
992 |> put_resp_content_type("application/json")
993 |> send_resp(403, Jason.encode!(%{"error" => message}))
994 end
995 end
996
997 def status_search(user, query) do
998 fetched =
999 if Regex.match?(~r/https?:/, query) do
1000 with {:ok, object} <- ActivityPub.fetch_object_from_id(query),
1001 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1002 true <- Visibility.visible_for_user?(activity, user) do
1003 [activity]
1004 else
1005 _e -> []
1006 end
1007 end || []
1008
1009 q =
1010 from(
1011 a in Activity,
1012 where: fragment("?->>'type' = 'Create'", a.data),
1013 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1014 where:
1015 fragment(
1016 "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
1017 a.data,
1018 ^query
1019 ),
1020 limit: 20,
1021 order_by: [desc: :id]
1022 )
1023
1024 Repo.all(q) ++ fetched
1025 end
1026
1027 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1028 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1029
1030 statuses = status_search(user, query)
1031
1032 tags_path = Web.base_url() <> "/tag/"
1033
1034 tags =
1035 query
1036 |> String.split()
1037 |> Enum.uniq()
1038 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1039 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1040 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1041
1042 res = %{
1043 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1044 "statuses" =>
1045 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1046 "hashtags" => tags
1047 }
1048
1049 json(conn, res)
1050 end
1051
1052 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1053 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1054
1055 statuses = status_search(user, query)
1056
1057 tags =
1058 query
1059 |> String.split()
1060 |> Enum.uniq()
1061 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1062 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1063
1064 res = %{
1065 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1066 "statuses" =>
1067 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1068 "hashtags" => tags
1069 }
1070
1071 json(conn, res)
1072 end
1073
1074 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1075 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1076
1077 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1078
1079 json(conn, res)
1080 end
1081
1082 def favourites(%{assigns: %{user: user}} = conn, params) do
1083 params =
1084 params
1085 |> Map.put("type", "Create")
1086 |> Map.put("favorited_by", user.ap_id)
1087 |> Map.put("blocking_user", user)
1088
1089 activities =
1090 ActivityPub.fetch_activities([], params)
1091 |> Enum.reverse()
1092
1093 conn
1094 |> add_link_headers(:favourites, activities)
1095 |> put_view(StatusView)
1096 |> render("index.json", %{activities: activities, for: user, as: :activity})
1097 end
1098
1099 def bookmarks(%{assigns: %{user: user}} = conn, _) do
1100 user = User.get_by_id(user.id)
1101
1102 activities =
1103 user.bookmarks
1104 |> Enum.map(fn id -> Activity.get_create_by_object_ap_id(id) end)
1105 |> Enum.reverse()
1106
1107 conn
1108 |> put_view(StatusView)
1109 |> render("index.json", %{activities: activities, for: user, as: :activity})
1110 end
1111
1112 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1113 lists = Pleroma.List.for_user(user, opts)
1114 res = ListView.render("lists.json", lists: lists)
1115 json(conn, res)
1116 end
1117
1118 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1119 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1120 res = ListView.render("list.json", list: list)
1121 json(conn, res)
1122 else
1123 _e ->
1124 conn
1125 |> put_status(404)
1126 |> json(%{error: "Record not found"})
1127 end
1128 end
1129
1130 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1131 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1132 res = ListView.render("lists.json", lists: lists)
1133 json(conn, res)
1134 end
1135
1136 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1137 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1138 {:ok, _list} <- Pleroma.List.delete(list) do
1139 json(conn, %{})
1140 else
1141 _e ->
1142 json(conn, "error")
1143 end
1144 end
1145
1146 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1147 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1148 res = ListView.render("list.json", list: list)
1149 json(conn, res)
1150 end
1151 end
1152
1153 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1154 accounts
1155 |> Enum.each(fn account_id ->
1156 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1157 %User{} = followed <- User.get_by_id(account_id) do
1158 Pleroma.List.follow(list, followed)
1159 end
1160 end)
1161
1162 json(conn, %{})
1163 end
1164
1165 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1166 accounts
1167 |> Enum.each(fn account_id ->
1168 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1169 %User{} = followed <- Pleroma.User.get_by_id(account_id) do
1170 Pleroma.List.unfollow(list, followed)
1171 end
1172 end)
1173
1174 json(conn, %{})
1175 end
1176
1177 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1178 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1179 {:ok, users} = Pleroma.List.get_following(list) do
1180 conn
1181 |> put_view(AccountView)
1182 |> render("accounts.json", %{users: users, as: :user})
1183 end
1184 end
1185
1186 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1187 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1188 {:ok, list} <- Pleroma.List.rename(list, title) do
1189 res = ListView.render("list.json", list: list)
1190 json(conn, res)
1191 else
1192 _e ->
1193 json(conn, "error")
1194 end
1195 end
1196
1197 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1198 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1199 params =
1200 params
1201 |> Map.put("type", "Create")
1202 |> Map.put("blocking_user", user)
1203 |> Map.put("muting_user", user)
1204
1205 # we must filter the following list for the user to avoid leaking statuses the user
1206 # does not actually have permission to see (for more info, peruse security issue #270).
1207 activities =
1208 following
1209 |> Enum.filter(fn x -> x in user.following end)
1210 |> ActivityPub.fetch_activities_bounded(following, params)
1211 |> Enum.reverse()
1212
1213 conn
1214 |> put_view(StatusView)
1215 |> render("index.json", %{activities: activities, for: user, as: :activity})
1216 else
1217 _e ->
1218 conn
1219 |> put_status(403)
1220 |> json(%{error: "Error."})
1221 end
1222 end
1223
1224 def index(%{assigns: %{user: user}} = conn, _params) do
1225 token = get_session(conn, :oauth_token)
1226
1227 if user && token do
1228 mastodon_emoji = mastodonized_emoji()
1229
1230 limit = Config.get([:instance, :limit])
1231
1232 accounts =
1233 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1234
1235 flavour = get_user_flavour(user)
1236
1237 initial_state =
1238 %{
1239 meta: %{
1240 streaming_api_base_url:
1241 String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
1242 access_token: token,
1243 locale: "en",
1244 domain: Pleroma.Web.Endpoint.host(),
1245 admin: "1",
1246 me: "#{user.id}",
1247 unfollow_modal: false,
1248 boost_modal: false,
1249 delete_modal: true,
1250 auto_play_gif: false,
1251 display_sensitive_media: false,
1252 reduce_motion: false,
1253 max_toot_chars: limit,
1254 mascot: "/images/pleroma-fox-tan-smol.png"
1255 },
1256 rights: %{
1257 delete_others_notice: present?(user.info.is_moderator),
1258 admin: present?(user.info.is_admin)
1259 },
1260 compose: %{
1261 me: "#{user.id}",
1262 default_privacy: user.info.default_scope,
1263 default_sensitive: false,
1264 allow_content_types: Config.get([:instance, :allowed_post_formats])
1265 },
1266 media_attachments: %{
1267 accept_content_types: [
1268 ".jpg",
1269 ".jpeg",
1270 ".png",
1271 ".gif",
1272 ".webm",
1273 ".mp4",
1274 ".m4v",
1275 "image\/jpeg",
1276 "image\/png",
1277 "image\/gif",
1278 "video\/webm",
1279 "video\/mp4"
1280 ]
1281 },
1282 settings:
1283 user.info.settings ||
1284 %{
1285 onboarded: true,
1286 home: %{
1287 shows: %{
1288 reblog: true,
1289 reply: true
1290 }
1291 },
1292 notifications: %{
1293 alerts: %{
1294 follow: true,
1295 favourite: true,
1296 reblog: true,
1297 mention: true
1298 },
1299 shows: %{
1300 follow: true,
1301 favourite: true,
1302 reblog: true,
1303 mention: true
1304 },
1305 sounds: %{
1306 follow: true,
1307 favourite: true,
1308 reblog: true,
1309 mention: true
1310 }
1311 }
1312 },
1313 push_subscription: nil,
1314 accounts: accounts,
1315 custom_emojis: mastodon_emoji,
1316 char_limit: limit
1317 }
1318 |> Jason.encode!()
1319
1320 conn
1321 |> put_layout(false)
1322 |> put_view(MastodonView)
1323 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1324 else
1325 conn
1326 |> put_session(:return_to, conn.request_path)
1327 |> redirect(to: "/web/login")
1328 end
1329 end
1330
1331 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1332 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1333
1334 with changeset <- Ecto.Changeset.change(user),
1335 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1336 {:ok, _user} <- User.update_and_set_cache(changeset) do
1337 json(conn, %{})
1338 else
1339 e ->
1340 conn
1341 |> put_resp_content_type("application/json")
1342 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1343 end
1344 end
1345
1346 @supported_flavours ["glitch", "vanilla"]
1347
1348 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1349 when flavour in @supported_flavours do
1350 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1351
1352 with changeset <- Ecto.Changeset.change(user),
1353 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1354 {:ok, user} <- User.update_and_set_cache(changeset),
1355 flavour <- user.info.flavour do
1356 json(conn, flavour)
1357 else
1358 e ->
1359 conn
1360 |> put_resp_content_type("application/json")
1361 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1362 end
1363 end
1364
1365 def set_flavour(conn, _params) do
1366 conn
1367 |> put_status(400)
1368 |> json(%{error: "Unsupported flavour"})
1369 end
1370
1371 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1372 json(conn, get_user_flavour(user))
1373 end
1374
1375 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1376 flavour
1377 end
1378
1379 defp get_user_flavour(_) do
1380 "glitch"
1381 end
1382
1383 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1384 redirect(conn, to: local_mastodon_root_path(conn))
1385 end
1386
1387 @doc "Local Mastodon FE login init action"
1388 def login(conn, %{"code" => auth_token}) do
1389 with {:ok, app} <- get_or_make_app(),
1390 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1391 {:ok, token} <- Token.exchange_token(app, auth) do
1392 conn
1393 |> put_session(:oauth_token, token.token)
1394 |> redirect(to: local_mastodon_root_path(conn))
1395 end
1396 end
1397
1398 @doc "Local Mastodon FE callback action"
1399 def login(conn, _) do
1400 with {:ok, app} <- get_or_make_app() do
1401 path =
1402 o_auth_path(
1403 conn,
1404 :authorize,
1405 response_type: "code",
1406 client_id: app.client_id,
1407 redirect_uri: ".",
1408 scope: Enum.join(app.scopes, " ")
1409 )
1410
1411 redirect(conn, to: path)
1412 end
1413 end
1414
1415 defp local_mastodon_root_path(conn) do
1416 case get_session(conn, :return_to) do
1417 nil ->
1418 mastodon_api_path(conn, :index, ["getting-started"])
1419
1420 return_to ->
1421 delete_session(conn, :return_to)
1422 return_to
1423 end
1424 end
1425
1426 defp get_or_make_app do
1427 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1428 scopes = ["read", "write", "follow", "push"]
1429
1430 with %App{} = app <- Repo.get_by(App, find_attrs) do
1431 {:ok, app} =
1432 if app.scopes == scopes do
1433 {:ok, app}
1434 else
1435 app
1436 |> Ecto.Changeset.change(%{scopes: scopes})
1437 |> Repo.update()
1438 end
1439
1440 {:ok, app}
1441 else
1442 _e ->
1443 cs =
1444 App.register_changeset(
1445 %App{},
1446 Map.put(find_attrs, :scopes, scopes)
1447 )
1448
1449 Repo.insert(cs)
1450 end
1451 end
1452
1453 def logout(conn, _) do
1454 conn
1455 |> clear_session
1456 |> redirect(to: "/")
1457 end
1458
1459 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1460 Logger.debug("Unimplemented, returning unmodified relationship")
1461
1462 with %User{} = target <- User.get_by_id(id) do
1463 conn
1464 |> put_view(AccountView)
1465 |> render("relationship.json", %{user: user, target: target})
1466 end
1467 end
1468
1469 def empty_array(conn, _) do
1470 Logger.debug("Unimplemented, returning an empty array")
1471 json(conn, [])
1472 end
1473
1474 def empty_object(conn, _) do
1475 Logger.debug("Unimplemented, returning an empty object")
1476 json(conn, %{})
1477 end
1478
1479 def get_filters(%{assigns: %{user: user}} = conn, _) do
1480 filters = Filter.get_filters(user)
1481 res = FilterView.render("filters.json", filters: filters)
1482 json(conn, res)
1483 end
1484
1485 def create_filter(
1486 %{assigns: %{user: user}} = conn,
1487 %{"phrase" => phrase, "context" => context} = params
1488 ) do
1489 query = %Filter{
1490 user_id: user.id,
1491 phrase: phrase,
1492 context: context,
1493 hide: Map.get(params, "irreversible", nil),
1494 whole_word: Map.get(params, "boolean", true)
1495 # expires_at
1496 }
1497
1498 {:ok, response} = Filter.create(query)
1499 res = FilterView.render("filter.json", filter: response)
1500 json(conn, res)
1501 end
1502
1503 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1504 filter = Filter.get(filter_id, user)
1505 res = FilterView.render("filter.json", filter: filter)
1506 json(conn, res)
1507 end
1508
1509 def update_filter(
1510 %{assigns: %{user: user}} = conn,
1511 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1512 ) do
1513 query = %Filter{
1514 user_id: user.id,
1515 filter_id: filter_id,
1516 phrase: phrase,
1517 context: context,
1518 hide: Map.get(params, "irreversible", nil),
1519 whole_word: Map.get(params, "boolean", true)
1520 # expires_at
1521 }
1522
1523 {:ok, response} = Filter.update(query)
1524 res = FilterView.render("filter.json", filter: response)
1525 json(conn, res)
1526 end
1527
1528 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1529 query = %Filter{
1530 user_id: user.id,
1531 filter_id: filter_id
1532 }
1533
1534 {:ok, _} = Filter.delete(query)
1535 json(conn, %{})
1536 end
1537
1538 # fallback action
1539 #
1540 def errors(conn, {:error, %Changeset{} = changeset}) do
1541 error_message =
1542 changeset
1543 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1544 |> Enum.map_join(", ", fn {_k, v} -> v end)
1545
1546 conn
1547 |> put_status(422)
1548 |> json(%{error: error_message})
1549 end
1550
1551 def errors(conn, {:error, :not_found}) do
1552 conn
1553 |> put_status(404)
1554 |> json(%{error: "Record not found"})
1555 end
1556
1557 def errors(conn, _) do
1558 conn
1559 |> put_status(500)
1560 |> json("Something went wrong")
1561 end
1562
1563 def suggestions(%{assigns: %{user: user}} = conn, _) do
1564 suggestions = Config.get(:suggestions)
1565
1566 if Keyword.get(suggestions, :enabled, false) do
1567 api = Keyword.get(suggestions, :third_party_engine, "")
1568 timeout = Keyword.get(suggestions, :timeout, 5000)
1569 limit = Keyword.get(suggestions, :limit, 23)
1570
1571 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1572
1573 user = user.nickname
1574
1575 url =
1576 api
1577 |> String.replace("{{host}}", host)
1578 |> String.replace("{{user}}", user)
1579
1580 with {:ok, %{status: 200, body: body}} <-
1581 @httpoison.get(
1582 url,
1583 [],
1584 adapter: [
1585 recv_timeout: timeout,
1586 pool: :default
1587 ]
1588 ),
1589 {:ok, data} <- Jason.decode(body) do
1590 data =
1591 data
1592 |> Enum.slice(0, limit)
1593 |> Enum.map(fn x ->
1594 Map.put(
1595 x,
1596 "id",
1597 case User.get_or_fetch(x["acct"]) do
1598 %{id: id} -> id
1599 _ -> 0
1600 end
1601 )
1602 end)
1603 |> Enum.map(fn x ->
1604 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1605 end)
1606 |> Enum.map(fn x ->
1607 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1608 end)
1609
1610 conn
1611 |> json(data)
1612 else
1613 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1614 end
1615 else
1616 json(conn, [])
1617 end
1618 end
1619
1620 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1621 with %Activity{} = activity <- Activity.get_by_id(status_id),
1622 true <- Visibility.visible_for_user?(activity, user) do
1623 data =
1624 StatusView.render(
1625 "card.json",
1626 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1627 )
1628
1629 json(conn, data)
1630 else
1631 _e ->
1632 %{}
1633 end
1634 end
1635
1636 def reports(%{assigns: %{user: user}} = conn, params) do
1637 case CommonAPI.report(user, params) do
1638 {:ok, activity} ->
1639 conn
1640 |> put_view(ReportView)
1641 |> try_render("report.json", %{activity: activity})
1642
1643 {:error, err} ->
1644 conn
1645 |> put_status(:bad_request)
1646 |> json(%{error: err})
1647 end
1648 end
1649
1650 def try_render(conn, target, params)
1651 when is_binary(target) do
1652 res = render(conn, target, params)
1653
1654 if res == nil do
1655 conn
1656 |> put_status(501)
1657 |> json(%{error: "Can't display this activity"})
1658 else
1659 res
1660 end
1661 end
1662
1663 def try_render(conn, _, _) do
1664 conn
1665 |> put_status(501)
1666 |> json(%{error: "Can't display this activity"})
1667 end
1668
1669 defp present?(nil), do: false
1670 defp present?(false), do: false
1671 defp present?(_), do: true
1672 end