1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
6 use Pleroma.Web, :controller
11 alias Pleroma.Conversation.Participation
13 alias Pleroma.Formatter
14 alias Pleroma.Notification
16 alias Pleroma.Object.Fetcher
17 alias Pleroma.Pagination
19 alias Pleroma.ScheduledActivity
23 alias Pleroma.Web.ActivityPub.ActivityPub
24 alias Pleroma.Web.ActivityPub.Visibility
25 alias Pleroma.Web.CommonAPI
26 alias Pleroma.Web.MastodonAPI.AccountView
27 alias Pleroma.Web.MastodonAPI.AppView
28 alias Pleroma.Web.MastodonAPI.ConversationView
29 alias Pleroma.Web.MastodonAPI.FilterView
30 alias Pleroma.Web.MastodonAPI.ListView
31 alias Pleroma.Web.MastodonAPI.MastodonAPI
32 alias Pleroma.Web.MastodonAPI.MastodonView
33 alias Pleroma.Web.MastodonAPI.NotificationView
34 alias Pleroma.Web.MastodonAPI.ReportView
35 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
36 alias Pleroma.Web.MastodonAPI.StatusView
37 alias Pleroma.Web.MediaProxy
38 alias Pleroma.Web.OAuth.App
39 alias Pleroma.Web.OAuth.Authorization
40 alias Pleroma.Web.OAuth.Scopes
41 alias Pleroma.Web.OAuth.Token
42 alias Pleroma.Web.TwitterAPI.TwitterAPI
44 alias Pleroma.Web.ControllerHelper
50 Pleroma.Plugs.RateLimitPlug,
52 max_requests: Config.get([:app_account_creation, :max_requests]),
53 interval: Config.get([:app_account_creation, :interval])
55 when action in [:account_register]
58 @httpoison Application.get_env(:pleroma, :httpoison)
59 @local_mastodon_name "Mastodon-Local"
61 action_fallback(:errors)
63 def create_app(conn, params) do
64 scopes = Scopes.fetch_scopes(params, ["read"])
68 |> Map.drop(["scope", "scopes"])
69 |> Map.put("scopes", scopes)
71 with cs <- App.register_changeset(%App{}, app_attrs),
72 false <- cs.changes[:client_name] == @local_mastodon_name,
73 {:ok, app} <- Repo.insert(cs) do
76 |> render("show.json", %{app: app})
85 value_function \\ fn x -> {:ok, x} end
87 if Map.has_key?(params, params_field) do
88 case value_function.(params[params_field]) do
89 {:ok, new_value} -> Map.put(map, map_field, new_value)
97 def update_credentials(%{assigns: %{user: user}} = conn, params) do
102 |> add_if_present(params, "display_name", :name)
103 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
104 |> add_if_present(params, "avatar", :avatar, fn value ->
105 with %Plug.Upload{} <- value,
106 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
113 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
116 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
120 [:no_rich_text, :locked, :hide_followers, :hide_follows, :hide_favorites, :show_role]
121 |> Enum.reduce(%{}, fn key, acc ->
122 add_if_present(acc, params, to_string(key), key, fn value ->
123 {:ok, ControllerHelper.truthy_param?(value)}
126 |> add_if_present(params, "default_scope", :default_scope)
127 |> add_if_present(params, "header", :banner, fn value ->
128 with %Plug.Upload{} <- value,
129 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
135 |> Map.put(:emoji, user_info_emojis)
137 info_cng = User.Info.profile_update(user.info, info_params)
139 with changeset <- User.update_changeset(user, user_params),
140 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
141 {:ok, user} <- User.update_and_set_cache(changeset) do
142 if original_user != user do
143 CommonAPI.update(user)
146 json(conn, AccountView.render("account.json", %{user: user, for: user}))
151 |> json(%{error: "Invalid request"})
155 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
156 change = Changeset.change(user, %{avatar: nil})
157 {:ok, user} = User.update_and_set_cache(change)
158 CommonAPI.update(user)
160 json(conn, %{url: nil})
163 def update_avatar(%{assigns: %{user: user}} = conn, params) do
164 {:ok, object} = ActivityPub.upload(params, type: :avatar)
165 change = Changeset.change(user, %{avatar: object.data})
166 {:ok, user} = User.update_and_set_cache(change)
167 CommonAPI.update(user)
168 %{"url" => [%{"href" => href} | _]} = object.data
170 json(conn, %{url: href})
173 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
174 with new_info <- %{"banner" => %{}},
175 info_cng <- User.Info.profile_update(user.info, new_info),
176 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
177 {:ok, user} <- User.update_and_set_cache(changeset) do
178 CommonAPI.update(user)
180 json(conn, %{url: nil})
184 def update_banner(%{assigns: %{user: user}} = conn, params) do
185 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
186 new_info <- %{"banner" => object.data},
187 info_cng <- User.Info.profile_update(user.info, new_info),
188 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
189 {:ok, user} <- User.update_and_set_cache(changeset) do
190 CommonAPI.update(user)
191 %{"url" => [%{"href" => href} | _]} = object.data
193 json(conn, %{url: href})
197 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
198 with new_info <- %{"background" => %{}},
199 info_cng <- User.Info.profile_update(user.info, new_info),
200 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
201 {:ok, _user} <- User.update_and_set_cache(changeset) do
202 json(conn, %{url: nil})
206 def update_background(%{assigns: %{user: user}} = conn, params) do
207 with {:ok, object} <- ActivityPub.upload(params, type: :background),
208 new_info <- %{"background" => object.data},
209 info_cng <- User.Info.profile_update(user.info, new_info),
210 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
211 {:ok, _user} <- User.update_and_set_cache(changeset) do
212 %{"url" => [%{"href" => href} | _]} = object.data
214 json(conn, %{url: href})
218 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
219 account = AccountView.render("account.json", %{user: user, for: user})
223 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
224 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
227 |> render("short.json", %{app: app})
231 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
232 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
233 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
234 account = AccountView.render("account.json", %{user: user, for: for_user})
240 |> json(%{error: "Can't find user"})
244 @mastodon_api_level "2.7.2"
246 def masto_instance(conn, _params) do
247 instance = Config.get(:instance)
251 title: Keyword.get(instance, :name),
252 description: Keyword.get(instance, :description),
253 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
254 email: Keyword.get(instance, :email),
256 streaming_api: Pleroma.Web.Endpoint.websocket_url()
258 stats: Stats.get_stats(),
259 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
261 registrations: Pleroma.Config.get([:instance, :registrations_open]),
262 # Extra (not present in Mastodon):
263 max_toot_chars: Keyword.get(instance, :limit)
269 def peers(conn, _params) do
270 json(conn, Stats.get_peers())
273 defp mastodonized_emoji do
274 Pleroma.Emoji.get_all()
275 |> Enum.map(fn {shortcode, relative_url, tags} ->
276 url = to_string(URI.merge(Web.base_url(), relative_url))
279 "shortcode" => shortcode,
281 "visible_in_picker" => true,
288 def custom_emojis(conn, _params) do
289 mastodon_emoji = mastodonized_emoji()
290 json(conn, mastodon_emoji)
293 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
296 |> Map.drop(["since_id", "max_id", "min_id"])
299 last = List.last(activities)
306 |> Map.get("limit", "20")
307 |> String.to_integer()
310 if length(activities) <= limit do
316 |> Enum.at(limit * -1)
320 {next_url, prev_url} =
324 Pleroma.Web.Endpoint,
327 Map.merge(params, %{max_id: max_id})
330 Pleroma.Web.Endpoint,
333 Map.merge(params, %{min_id: min_id})
339 Pleroma.Web.Endpoint,
341 Map.merge(params, %{max_id: max_id})
344 Pleroma.Web.Endpoint,
346 Map.merge(params, %{min_id: min_id})
352 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
358 def home_timeline(%{assigns: %{user: user}} = conn, params) do
361 |> Map.put("type", ["Create", "Announce"])
362 |> Map.put("blocking_user", user)
363 |> Map.put("muting_user", user)
364 |> Map.put("user", user)
367 [user.ap_id | user.following]
368 |> ActivityPub.fetch_activities(params)
372 |> add_link_headers(:home_timeline, activities)
373 |> put_view(StatusView)
374 |> render("index.json", %{activities: activities, for: user, as: :activity})
377 def public_timeline(%{assigns: %{user: user}} = conn, params) do
378 local_only = params["local"] in [true, "True", "true", "1"]
382 |> Map.put("type", ["Create", "Announce"])
383 |> Map.put("local_only", local_only)
384 |> Map.put("blocking_user", user)
385 |> Map.put("muting_user", user)
386 |> ActivityPub.fetch_public_activities()
390 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
391 |> put_view(StatusView)
392 |> render("index.json", %{activities: activities, for: user, as: :activity})
395 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
396 with %User{} = user <- User.get_cached_by_id(params["id"]) do
397 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
400 |> add_link_headers(:user_statuses, activities, params["id"])
401 |> put_view(StatusView)
402 |> render("index.json", %{
403 activities: activities,
410 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
413 |> Map.put("type", "Create")
414 |> Map.put("blocking_user", user)
415 |> Map.put("user", user)
416 |> Map.put(:visibility, "direct")
420 |> ActivityPub.fetch_activities_query(params)
421 |> Pagination.fetch_paginated(params)
424 |> add_link_headers(:dm_timeline, activities)
425 |> put_view(StatusView)
426 |> render("index.json", %{activities: activities, for: user, as: :activity})
429 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
430 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
431 true <- Visibility.visible_for_user?(activity, user) do
433 |> put_view(StatusView)
434 |> try_render("status.json", %{activity: activity, for: user})
438 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
439 with %Activity{} = activity <- Activity.get_by_id(id),
441 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
442 "blocking_user" => user,
446 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
448 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
449 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
455 activities: grouped_activities[true] || [],
459 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
464 activities: grouped_activities[false] || [],
468 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
475 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
476 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
478 |> add_link_headers(:scheduled_statuses, scheduled_activities)
479 |> put_view(ScheduledActivityView)
480 |> render("index.json", %{scheduled_activities: scheduled_activities})
484 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
485 with %ScheduledActivity{} = scheduled_activity <-
486 ScheduledActivity.get(user, scheduled_activity_id) do
488 |> put_view(ScheduledActivityView)
489 |> render("show.json", %{scheduled_activity: scheduled_activity})
491 _ -> {:error, :not_found}
495 def update_scheduled_status(
496 %{assigns: %{user: user}} = conn,
497 %{"id" => scheduled_activity_id} = params
499 with %ScheduledActivity{} = scheduled_activity <-
500 ScheduledActivity.get(user, scheduled_activity_id),
501 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
503 |> put_view(ScheduledActivityView)
504 |> render("show.json", %{scheduled_activity: scheduled_activity})
506 nil -> {:error, :not_found}
511 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
512 with %ScheduledActivity{} = scheduled_activity <-
513 ScheduledActivity.get(user, scheduled_activity_id),
514 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
516 |> put_view(ScheduledActivityView)
517 |> render("show.json", %{scheduled_activity: scheduled_activity})
519 nil -> {:error, :not_found}
524 def post_status(conn, %{"status" => "", "media_ids" => media_ids} = params)
525 when length(media_ids) > 0 do
528 |> Map.put("status", ".")
530 post_status(conn, params)
533 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
536 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
539 case get_req_header(conn, "idempotency-key") do
541 _ -> Ecto.UUID.generate()
544 scheduled_at = params["scheduled_at"]
546 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
547 with {:ok, scheduled_activity} <-
548 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
550 |> put_view(ScheduledActivityView)
551 |> render("show.json", %{scheduled_activity: scheduled_activity})
554 params = Map.drop(params, ["scheduled_at"])
557 Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ ->
558 CommonAPI.post(user, params)
562 |> put_view(StatusView)
563 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
567 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
568 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
574 |> json(%{error: "Can't delete this post"})
578 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
579 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
580 %Activity{} = announce <- Activity.normalize(announce.data) do
582 |> put_view(StatusView)
583 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
587 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
588 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
589 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
591 |> put_view(StatusView)
592 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
596 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
597 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
598 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
600 |> put_view(StatusView)
601 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
605 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
606 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
607 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
609 |> put_view(StatusView)
610 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
614 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
615 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
617 |> put_view(StatusView)
618 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
622 |> put_resp_content_type("application/json")
623 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
627 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
628 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
630 |> put_view(StatusView)
631 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
635 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
636 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
637 %User{} = user <- User.get_cached_by_nickname(user.nickname),
638 true <- Visibility.visible_for_user?(activity, user),
639 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
641 |> put_view(StatusView)
642 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
646 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
647 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
648 %User{} = user <- User.get_cached_by_nickname(user.nickname),
649 true <- Visibility.visible_for_user?(activity, user),
650 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
652 |> put_view(StatusView)
653 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
657 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
658 activity = Activity.get_by_id(id)
660 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
662 |> put_view(StatusView)
663 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
667 |> put_resp_content_type("application/json")
668 |> send_resp(:bad_request, Jason.encode!(%{"error" => reason}))
672 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
673 activity = Activity.get_by_id(id)
675 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
677 |> put_view(StatusView)
678 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
682 def notifications(%{assigns: %{user: user}} = conn, params) do
683 notifications = MastodonAPI.get_notifications(user, params)
686 |> add_link_headers(:notifications, notifications)
687 |> put_view(NotificationView)
688 |> render("index.json", %{notifications: notifications, for: user})
691 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
692 with {:ok, notification} <- Notification.get(user, id) do
694 |> put_view(NotificationView)
695 |> render("show.json", %{notification: notification, for: user})
699 |> put_resp_content_type("application/json")
700 |> send_resp(403, Jason.encode!(%{"error" => reason}))
704 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
705 Notification.clear(user)
709 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
710 with {:ok, _notif} <- Notification.dismiss(user, id) do
715 |> put_resp_content_type("application/json")
716 |> send_resp(403, Jason.encode!(%{"error" => reason}))
720 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
721 Notification.destroy_multiple(user, ids)
725 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
727 q = from(u in User, where: u.id in ^id)
728 targets = Repo.all(q)
731 |> put_view(AccountView)
732 |> render("relationships.json", %{user: user, targets: targets})
735 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
736 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
738 def update_media(%{assigns: %{user: user}} = conn, data) do
739 with %Object{} = object <- Repo.get(Object, data["id"]),
740 true <- Object.authorize_mutation(object, user),
741 true <- is_binary(data["description"]),
742 description <- data["description"] do
743 new_data = %{object.data | "name" => description}
747 |> Object.change(%{data: new_data})
750 attachment_data = Map.put(new_data, "id", object.id)
753 |> put_view(StatusView)
754 |> render("attachment.json", %{attachment: attachment_data})
758 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
759 with {:ok, object} <-
762 actor: User.ap_id(user),
763 description: Map.get(data, "description")
765 attachment_data = Map.put(object.data, "id", object.id)
768 |> put_view(StatusView)
769 |> render("attachment.json", %{attachment: attachment_data})
773 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
774 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
775 %{} = attachment_data <- Map.put(object.data, "id", object.id),
776 %{type: type} = rendered <-
777 StatusView.render("attachment.json", %{attachment: attachment_data}) do
778 # Reject if not an image
779 if type == "image" do
781 # Save to the user's info
782 info_changeset = User.Info.mascot_update(user.info, rendered)
786 |> Ecto.Changeset.change()
787 |> Ecto.Changeset.put_embed(:info, info_changeset)
789 {:ok, _user} = User.update_and_set_cache(user_changeset)
795 |> put_resp_content_type("application/json")
796 |> send_resp(415, Jason.encode!(%{"error" => "mascots can only be images"}))
801 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
802 mascot = User.get_mascot(user)
808 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
809 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
810 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
811 q = from(u in User, where: u.ap_id in ^likes)
815 |> put_view(AccountView)
816 |> render(AccountView, "accounts.json", %{for: user, users: users, as: :user})
822 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
823 with %Activity{data: %{"object" => object}} <- Repo.get(Activity, id),
824 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
825 q = from(u in User, where: u.ap_id in ^announces)
829 |> put_view(AccountView)
830 |> render("accounts.json", %{for: user, users: users, as: :user})
836 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
837 local_only = params["local"] in [true, "True", "true", "1"]
840 [params["tag"], params["any"]]
844 |> Enum.map(&String.downcase(&1))
849 |> Enum.map(&String.downcase(&1))
854 |> Enum.map(&String.downcase(&1))
858 |> Map.put("type", "Create")
859 |> Map.put("local_only", local_only)
860 |> Map.put("blocking_user", user)
861 |> Map.put("muting_user", user)
862 |> Map.put("tag", tags)
863 |> Map.put("tag_all", tag_all)
864 |> Map.put("tag_reject", tag_reject)
865 |> ActivityPub.fetch_public_activities()
869 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
870 |> put_view(StatusView)
871 |> render("index.json", %{activities: activities, for: user, as: :activity})
874 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
875 with %User{} = user <- User.get_cached_by_id(id),
876 followers <- MastodonAPI.get_followers(user, params) do
879 for_user && user.id == for_user.id -> followers
880 user.info.hide_followers -> []
885 |> add_link_headers(:followers, followers, user)
886 |> put_view(AccountView)
887 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
891 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
892 with %User{} = user <- User.get_cached_by_id(id),
893 followers <- MastodonAPI.get_friends(user, params) do
896 for_user && user.id == for_user.id -> followers
897 user.info.hide_follows -> []
902 |> add_link_headers(:following, followers, user)
903 |> put_view(AccountView)
904 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
908 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
909 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
911 |> put_view(AccountView)
912 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
916 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
917 with %User{} = follower <- User.get_cached_by_id(id),
918 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
920 |> put_view(AccountView)
921 |> render("relationship.json", %{user: followed, target: follower})
925 |> put_resp_content_type("application/json")
926 |> send_resp(403, Jason.encode!(%{"error" => message}))
930 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
931 with %User{} = follower <- User.get_cached_by_id(id),
932 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
934 |> put_view(AccountView)
935 |> render("relationship.json", %{user: followed, target: follower})
939 |> put_resp_content_type("application/json")
940 |> send_resp(403, Jason.encode!(%{"error" => message}))
944 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
945 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
946 {_, true} <- {:followed, follower.id != followed.id},
947 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
949 |> put_view(AccountView)
950 |> render("relationship.json", %{user: follower, target: followed})
957 |> put_resp_content_type("application/json")
958 |> send_resp(403, Jason.encode!(%{"error" => message}))
962 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
963 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
964 {_, true} <- {:followed, follower.id != followed.id},
965 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
967 |> put_view(AccountView)
968 |> render("account.json", %{user: followed, for: follower})
975 |> put_resp_content_type("application/json")
976 |> send_resp(403, Jason.encode!(%{"error" => message}))
980 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
981 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
982 {_, true} <- {:followed, follower.id != followed.id},
983 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
985 |> put_view(AccountView)
986 |> render("relationship.json", %{user: follower, target: followed})
996 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
997 with %User{} = muted <- User.get_cached_by_id(id),
998 {:ok, muter} <- User.mute(muter, muted) do
1000 |> put_view(AccountView)
1001 |> render("relationship.json", %{user: muter, target: muted})
1003 {:error, message} ->
1005 |> put_resp_content_type("application/json")
1006 |> send_resp(403, Jason.encode!(%{"error" => message}))
1010 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1011 with %User{} = muted <- User.get_cached_by_id(id),
1012 {:ok, muter} <- User.unmute(muter, muted) do
1014 |> put_view(AccountView)
1015 |> render("relationship.json", %{user: muter, target: muted})
1017 {:error, message} ->
1019 |> put_resp_content_type("application/json")
1020 |> send_resp(403, Jason.encode!(%{"error" => message}))
1024 def mutes(%{assigns: %{user: user}} = conn, _) do
1025 with muted_accounts <- User.muted_users(user) do
1026 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1031 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1032 with %User{} = blocked <- User.get_cached_by_id(id),
1033 {:ok, blocker} <- User.block(blocker, blocked),
1034 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1036 |> put_view(AccountView)
1037 |> render("relationship.json", %{user: blocker, target: blocked})
1039 {:error, message} ->
1041 |> put_resp_content_type("application/json")
1042 |> send_resp(403, Jason.encode!(%{"error" => message}))
1046 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1047 with %User{} = blocked <- User.get_cached_by_id(id),
1048 {:ok, blocker} <- User.unblock(blocker, blocked),
1049 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1051 |> put_view(AccountView)
1052 |> render("relationship.json", %{user: blocker, target: blocked})
1054 {:error, message} ->
1056 |> put_resp_content_type("application/json")
1057 |> send_resp(403, Jason.encode!(%{"error" => message}))
1061 def blocks(%{assigns: %{user: user}} = conn, _) do
1062 with blocked_accounts <- User.blocked_users(user) do
1063 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1068 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1069 json(conn, info.domain_blocks || [])
1072 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1073 User.block_domain(blocker, domain)
1077 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1078 User.unblock_domain(blocker, domain)
1082 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1083 with %User{} = subscription_target <- User.get_cached_by_id(id),
1084 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1086 |> put_view(AccountView)
1087 |> render("relationship.json", %{user: user, target: subscription_target})
1089 {:error, message} ->
1091 |> put_resp_content_type("application/json")
1092 |> send_resp(403, Jason.encode!(%{"error" => message}))
1096 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1097 with %User{} = subscription_target <- User.get_cached_by_id(id),
1098 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1100 |> put_view(AccountView)
1101 |> render("relationship.json", %{user: user, target: subscription_target})
1103 {:error, message} ->
1105 |> put_resp_content_type("application/json")
1106 |> send_resp(403, Jason.encode!(%{"error" => message}))
1110 def status_search_query_with_gin(q, query) do
1114 "to_tsvector('english', ?->>'content') @@ plainto_tsquery('english', ?)",
1118 order_by: [desc: :id]
1122 def status_search_query_with_rum(q, query) do
1126 "? @@ plainto_tsquery('english', ?)",
1130 order_by: [fragment("? <=> now()::date", o.inserted_at)]
1134 def status_search(user, query) do
1136 if Regex.match?(~r/https?:/, query) do
1137 with {:ok, object} <- Fetcher.fetch_object_from_id(query),
1138 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
1139 true <- Visibility.visible_for_user?(activity, user) do
1147 from([a, o] in Activity.with_preloaded_object(Activity),
1148 where: fragment("?->>'type' = 'Create'", a.data),
1149 where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
1154 if Pleroma.Config.get([:database, :rum_enabled]) do
1155 status_search_query_with_rum(q, query)
1157 status_search_query_with_gin(q, query)
1160 Repo.all(q) ++ fetched
1163 def search2(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1164 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1166 statuses = status_search(user, query)
1168 tags_path = Web.base_url() <> "/tag/"
1174 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1175 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1176 |> Enum.map(fn tag -> %{name: tag, url: tags_path <> tag} end)
1179 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1181 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1188 def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1189 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1191 statuses = status_search(user, query)
1197 |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
1198 |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
1201 "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
1203 StatusView.render("index.json", activities: statuses, for: user, as: :activity),
1210 def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
1211 accounts = User.search(query, resolve: params["resolve"] == "true", for_user: user)
1213 res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
1218 def favourites(%{assigns: %{user: user}} = conn, params) do
1221 |> Map.put("type", "Create")
1222 |> Map.put("favorited_by", user.ap_id)
1223 |> Map.put("blocking_user", user)
1226 ActivityPub.fetch_activities([], params)
1230 |> add_link_headers(:favourites, activities)
1231 |> put_view(StatusView)
1232 |> render("index.json", %{activities: activities, for: user, as: :activity})
1235 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1236 with %User{} = user <- User.get_by_id(id),
1237 false <- user.info.hide_favorites do
1240 |> Map.put("type", "Create")
1241 |> Map.put("favorited_by", user.ap_id)
1242 |> Map.put("blocking_user", for_user)
1246 ["https://www.w3.org/ns/activitystreams#Public"] ++
1247 [for_user.ap_id | for_user.following]
1249 ["https://www.w3.org/ns/activitystreams#Public"]
1254 |> ActivityPub.fetch_activities(params)
1258 |> add_link_headers(:favourites, activities)
1259 |> put_view(StatusView)
1260 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1263 {:error, :not_found}
1268 |> json(%{error: "Can't get favorites"})
1272 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1273 user = User.get_cached_by_id(user.id)
1276 Bookmark.for_user_query(user.id)
1277 |> Pagination.fetch_paginated(params)
1281 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1284 |> add_link_headers(:bookmarks, bookmarks)
1285 |> put_view(StatusView)
1286 |> render("index.json", %{activities: activities, for: user, as: :activity})
1289 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1290 lists = Pleroma.List.for_user(user, opts)
1291 res = ListView.render("lists.json", lists: lists)
1295 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1296 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1297 res = ListView.render("list.json", list: list)
1303 |> json(%{error: "Record not found"})
1307 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1308 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1309 res = ListView.render("lists.json", lists: lists)
1313 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1314 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1315 {:ok, _list} <- Pleroma.List.delete(list) do
1323 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1324 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1325 res = ListView.render("list.json", list: list)
1330 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1332 |> Enum.each(fn account_id ->
1333 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1334 %User{} = followed <- User.get_cached_by_id(account_id) do
1335 Pleroma.List.follow(list, followed)
1342 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1344 |> Enum.each(fn account_id ->
1345 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1346 %User{} = followed <- User.get_cached_by_id(account_id) do
1347 Pleroma.List.unfollow(list, followed)
1354 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1355 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1356 {:ok, users} = Pleroma.List.get_following(list) do
1358 |> put_view(AccountView)
1359 |> render("accounts.json", %{for: user, users: users, as: :user})
1363 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1364 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1365 {:ok, list} <- Pleroma.List.rename(list, title) do
1366 res = ListView.render("list.json", list: list)
1374 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1375 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1378 |> Map.put("type", "Create")
1379 |> Map.put("blocking_user", user)
1380 |> Map.put("muting_user", user)
1382 # we must filter the following list for the user to avoid leaking statuses the user
1383 # does not actually have permission to see (for more info, peruse security issue #270).
1386 |> Enum.filter(fn x -> x in user.following end)
1387 |> ActivityPub.fetch_activities_bounded(following, params)
1391 |> put_view(StatusView)
1392 |> render("index.json", %{activities: activities, for: user, as: :activity})
1397 |> json(%{error: "Error."})
1401 def index(%{assigns: %{user: user}} = conn, _params) do
1402 token = get_session(conn, :oauth_token)
1405 mastodon_emoji = mastodonized_emoji()
1407 limit = Config.get([:instance, :limit])
1410 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1412 flavour = get_user_flavour(user)
1417 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1418 access_token: token,
1420 domain: Pleroma.Web.Endpoint.host(),
1423 unfollow_modal: false,
1426 auto_play_gif: false,
1427 display_sensitive_media: false,
1428 reduce_motion: false,
1429 max_toot_chars: limit,
1430 mascot: User.get_mascot(user)["url"]
1433 delete_others_notice: present?(user.info.is_moderator),
1434 admin: present?(user.info.is_admin)
1438 default_privacy: user.info.default_scope,
1439 default_sensitive: false,
1440 allow_content_types: Config.get([:instance, :allowed_post_formats])
1442 media_attachments: %{
1443 accept_content_types: [
1459 user.info.settings ||
1489 push_subscription: nil,
1491 custom_emojis: mastodon_emoji,
1497 |> put_layout(false)
1498 |> put_view(MastodonView)
1499 |> render("index.html", %{initial_state: initial_state, flavour: flavour})
1502 |> put_session(:return_to, conn.request_path)
1503 |> redirect(to: "/web/login")
1507 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1508 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1510 with changeset <- Ecto.Changeset.change(user),
1511 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1512 {:ok, _user} <- User.update_and_set_cache(changeset) do
1517 |> put_resp_content_type("application/json")
1518 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1522 @supported_flavours ["glitch", "vanilla"]
1524 def set_flavour(%{assigns: %{user: user}} = conn, %{"flavour" => flavour} = _params)
1525 when flavour in @supported_flavours do
1526 flavour_cng = User.Info.mastodon_flavour_update(user.info, flavour)
1528 with changeset <- Ecto.Changeset.change(user),
1529 changeset <- Ecto.Changeset.put_embed(changeset, :info, flavour_cng),
1530 {:ok, user} <- User.update_and_set_cache(changeset),
1531 flavour <- user.info.flavour do
1536 |> put_resp_content_type("application/json")
1537 |> send_resp(500, Jason.encode!(%{"error" => inspect(e)}))
1541 def set_flavour(conn, _params) do
1544 |> json(%{error: "Unsupported flavour"})
1547 def get_flavour(%{assigns: %{user: user}} = conn, _params) do
1548 json(conn, get_user_flavour(user))
1551 defp get_user_flavour(%User{info: %{flavour: flavour}}) when flavour in @supported_flavours do
1555 defp get_user_flavour(_) do
1559 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1560 redirect(conn, to: local_mastodon_root_path(conn))
1563 @doc "Local Mastodon FE login init action"
1564 def login(conn, %{"code" => auth_token}) do
1565 with {:ok, app} <- get_or_make_app(),
1566 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1567 {:ok, token} <- Token.exchange_token(app, auth) do
1569 |> put_session(:oauth_token, token.token)
1570 |> redirect(to: local_mastodon_root_path(conn))
1574 @doc "Local Mastodon FE callback action"
1575 def login(conn, _) do
1576 with {:ok, app} <- get_or_make_app() do
1581 response_type: "code",
1582 client_id: app.client_id,
1584 scope: Enum.join(app.scopes, " ")
1587 redirect(conn, to: path)
1591 defp local_mastodon_root_path(conn) do
1592 case get_session(conn, :return_to) do
1594 mastodon_api_path(conn, :index, ["getting-started"])
1597 delete_session(conn, :return_to)
1602 defp get_or_make_app do
1603 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1604 scopes = ["read", "write", "follow", "push"]
1606 with %App{} = app <- Repo.get_by(App, find_attrs) do
1608 if app.scopes == scopes do
1612 |> Ecto.Changeset.change(%{scopes: scopes})
1620 App.register_changeset(
1622 Map.put(find_attrs, :scopes, scopes)
1629 def logout(conn, _) do
1632 |> redirect(to: "/")
1635 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1636 Logger.debug("Unimplemented, returning unmodified relationship")
1638 with %User{} = target <- User.get_cached_by_id(id) do
1640 |> put_view(AccountView)
1641 |> render("relationship.json", %{user: user, target: target})
1645 def empty_array(conn, _) do
1646 Logger.debug("Unimplemented, returning an empty array")
1650 def empty_object(conn, _) do
1651 Logger.debug("Unimplemented, returning an empty object")
1655 def get_filters(%{assigns: %{user: user}} = conn, _) do
1656 filters = Filter.get_filters(user)
1657 res = FilterView.render("filters.json", filters: filters)
1662 %{assigns: %{user: user}} = conn,
1663 %{"phrase" => phrase, "context" => context} = params
1669 hide: Map.get(params, "irreversible", false),
1670 whole_word: Map.get(params, "boolean", true)
1674 {:ok, response} = Filter.create(query)
1675 res = FilterView.render("filter.json", filter: response)
1679 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1680 filter = Filter.get(filter_id, user)
1681 res = FilterView.render("filter.json", filter: filter)
1686 %{assigns: %{user: user}} = conn,
1687 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1691 filter_id: filter_id,
1694 hide: Map.get(params, "irreversible", nil),
1695 whole_word: Map.get(params, "boolean", true)
1699 {:ok, response} = Filter.update(query)
1700 res = FilterView.render("filter.json", filter: response)
1704 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1707 filter_id: filter_id
1710 {:ok, _} = Filter.delete(query)
1716 def errors(conn, {:error, %Changeset{} = changeset}) do
1719 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1720 |> Enum.map_join(", ", fn {_k, v} -> v end)
1724 |> json(%{error: error_message})
1727 def errors(conn, {:error, :not_found}) do
1730 |> json(%{error: "Record not found"})
1733 def errors(conn, _) do
1736 |> json("Something went wrong")
1739 def suggestions(%{assigns: %{user: user}} = conn, _) do
1740 suggestions = Config.get(:suggestions)
1742 if Keyword.get(suggestions, :enabled, false) do
1743 api = Keyword.get(suggestions, :third_party_engine, "")
1744 timeout = Keyword.get(suggestions, :timeout, 5000)
1745 limit = Keyword.get(suggestions, :limit, 23)
1747 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1749 user = user.nickname
1753 |> String.replace("{{host}}", host)
1754 |> String.replace("{{user}}", user)
1756 with {:ok, %{status: 200, body: body}} <-
1761 recv_timeout: timeout,
1765 {:ok, data} <- Jason.decode(body) do
1768 |> Enum.slice(0, limit)
1773 case User.get_or_fetch(x["acct"]) do
1774 {:ok, %User{id: id}} -> id
1780 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1783 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1789 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1796 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1797 with %Activity{} = activity <- Activity.get_by_id(status_id),
1798 true <- Visibility.visible_for_user?(activity, user) do
1802 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1812 def reports(%{assigns: %{user: user}} = conn, params) do
1813 case CommonAPI.report(user, params) do
1816 |> put_view(ReportView)
1817 |> try_render("report.json", %{activity: activity})
1821 |> put_status(:bad_request)
1822 |> json(%{error: err})
1826 def account_register(
1827 %{assigns: %{app: app}} = conn,
1828 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1836 "captcha_answer_data",
1840 |> Map.put("nickname", nickname)
1841 |> Map.put("fullname", params["fullname"] || nickname)
1842 |> Map.put("bio", params["bio"] || "")
1843 |> Map.put("confirm", params["password"])
1845 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1846 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1848 token_type: "Bearer",
1849 access_token: token.token,
1851 created_at: Token.Utils.format_created_at(token)
1857 |> json(Jason.encode!(errors))
1861 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1864 |> json(%{error: "Missing parameters"})
1867 def account_register(conn, _) do
1870 |> json(%{error: "Invalid credentials"})
1873 def conversations(%{assigns: %{user: user}} = conn, params) do
1874 participations = Participation.for_user_with_last_activity_id(user, params)
1877 Enum.map(participations, fn participation ->
1878 ConversationView.render("participation.json", %{participation: participation, user: user})
1882 |> add_link_headers(:conversations, participations)
1883 |> json(conversations)
1886 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1887 with %Participation{} = participation <-
1888 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1889 {:ok, participation} <- Participation.mark_as_read(participation) do
1890 participation_view =
1891 ConversationView.render("participation.json", %{participation: participation, user: user})
1894 |> json(participation_view)
1898 def try_render(conn, target, params)
1899 when is_binary(target) do
1900 res = render(conn, target, params)
1905 |> json(%{error: "Can't display this activity"})
1911 def try_render(conn, _, _) do
1914 |> json(%{error: "Can't display this activity"})
1917 defp present?(nil), do: false
1918 defp present?(false), do: false
1919 defp present?(_), do: true