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