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
8 import Pleroma.Web.ControllerHelper, only: [json_response: 3]
11 alias Pleroma.Activity
12 alias Pleroma.Bookmark
14 alias Pleroma.Conversation.Participation
16 alias Pleroma.Formatter
18 alias Pleroma.Notification
20 alias Pleroma.Pagination
21 alias Pleroma.Plugs.RateLimiter
23 alias Pleroma.ScheduledActivity
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.NotificationView
38 alias Pleroma.Web.MastodonAPI.ReportView
39 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
40 alias Pleroma.Web.MastodonAPI.StatusView
41 alias Pleroma.Web.MediaProxy
42 alias Pleroma.Web.OAuth.App
43 alias Pleroma.Web.OAuth.Authorization
44 alias Pleroma.Web.OAuth.Scopes
45 alias Pleroma.Web.OAuth.Token
46 alias Pleroma.Web.TwitterAPI.TwitterAPI
48 alias Pleroma.Web.ControllerHelper
52 require Pleroma.Constants
54 @rate_limited_relations_actions ~w(follow unfollow)a
56 @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
57 post_status delete_status)a
61 {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
62 when action in ~w(reblog_status unreblog_status)a
67 {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
68 when action in ~w(fav_status unfav_status)a
73 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
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)
83 @local_mastodon_name "Mastodon-Local"
85 action_fallback(:errors)
87 def create_app(conn, params) do
88 scopes = Scopes.fetch_scopes(params, ["read"])
92 |> Map.drop(["scope", "scopes"])
93 |> Map.put("scopes", scopes)
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
100 |> render("show.json", %{app: app})
109 value_function \\ fn x -> {:ok, x} end
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)
121 def update_credentials(%{assigns: %{user: user}} = conn, params) do
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
137 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
141 |> Map.get(:emoji, [])
142 |> Enum.concat(Formatter.get_emoji_map(emojis_text))
153 :skip_thread_containment
155 |> Enum.reduce(%{}, fn key, acc ->
156 add_if_present(acc, params, to_string(key), key, fn value ->
157 {:ok, ControllerHelper.truthy_param?(value)}
160 |> add_if_present(params, "default_scope", :default_scope)
161 |> add_if_present(params, "fields", :fields, fn fields ->
162 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
166 |> add_if_present(params, "fields", :raw_fields)
167 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
168 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
170 |> add_if_present(params, "header", :banner, fn value ->
171 with %Plug.Upload{} <- value,
172 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
178 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
179 with %Plug.Upload{} <- value,
180 {:ok, object} <- ActivityPub.upload(value, type: :background) do
186 |> Map.put(:emoji, user_info_emojis)
188 info_cng = User.Info.profile_update(user.info, info_params)
190 with changeset <- User.update_changeset(user, user_params),
191 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
192 {:ok, user} <- User.update_and_set_cache(changeset) do
193 if original_user != user do
194 CommonAPI.update(user)
199 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
202 _e -> render_error(conn, :forbidden, "Invalid request")
206 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
207 change = Changeset.change(user, %{avatar: nil})
208 {:ok, user} = User.update_and_set_cache(change)
209 CommonAPI.update(user)
211 json(conn, %{url: nil})
214 def update_avatar(%{assigns: %{user: user}} = conn, params) do
215 {:ok, object} = ActivityPub.upload(params, type: :avatar)
216 change = Changeset.change(user, %{avatar: object.data})
217 {:ok, user} = User.update_and_set_cache(change)
218 CommonAPI.update(user)
219 %{"url" => [%{"href" => href} | _]} = object.data
221 json(conn, %{url: href})
224 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
225 with new_info <- %{"banner" => %{}},
226 info_cng <- User.Info.profile_update(user.info, new_info),
227 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
228 {:ok, user} <- User.update_and_set_cache(changeset) do
229 CommonAPI.update(user)
231 json(conn, %{url: nil})
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 info_cng <- User.Info.profile_update(user.info, new_info),
239 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
240 {:ok, user} <- User.update_and_set_cache(changeset) do
241 CommonAPI.update(user)
242 %{"url" => [%{"href" => href} | _]} = object.data
244 json(conn, %{url: href})
248 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
249 with new_info <- %{"background" => %{}},
250 info_cng <- User.Info.profile_update(user.info, new_info),
251 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
252 {:ok, _user} <- User.update_and_set_cache(changeset) do
253 json(conn, %{url: nil})
257 def update_background(%{assigns: %{user: user}} = conn, params) do
258 with {:ok, object} <- ActivityPub.upload(params, type: :background),
259 new_info <- %{"background" => object.data},
260 info_cng <- User.Info.profile_update(user.info, new_info),
261 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
262 {:ok, _user} <- User.update_and_set_cache(changeset) do
263 %{"url" => [%{"href" => href} | _]} = object.data
265 json(conn, %{url: href})
269 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
270 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
273 AccountView.render("account.json", %{
276 with_pleroma_settings: true,
277 with_chat_token: chat_token
283 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
284 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
287 |> render("short.json", %{app: app})
291 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
292 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
293 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
294 account = AccountView.render("account.json", %{user: user, for: for_user})
297 _e -> render_error(conn, :not_found, "Can't find user")
301 @mastodon_api_level "2.7.2"
303 def masto_instance(conn, _params) do
304 instance = Config.get(:instance)
308 title: Keyword.get(instance, :name),
309 description: Keyword.get(instance, :description),
310 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
311 email: Keyword.get(instance, :email),
313 streaming_api: Pleroma.Web.Endpoint.websocket_url()
315 stats: Stats.get_stats(),
316 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
318 registrations: Pleroma.Config.get([:instance, :registrations_open]),
319 # Extra (not present in Mastodon):
320 max_toot_chars: Keyword.get(instance, :limit),
321 poll_limits: Keyword.get(instance, :poll_limits)
327 def peers(conn, _params) do
328 json(conn, Stats.get_peers())
331 defp mastodonized_emoji do
332 Pleroma.Emoji.get_all()
333 |> Enum.map(fn {shortcode, relative_url, tags} ->
334 url = to_string(URI.merge(Web.base_url(), relative_url))
337 "shortcode" => shortcode,
339 "visible_in_picker" => true,
342 # Assuming that a comma is authorized in the category name
343 "category" => (tags -- ["Custom"]) |> Enum.join(",")
348 def custom_emojis(conn, _params) do
349 mastodon_emoji = mastodonized_emoji()
350 json(conn, mastodon_emoji)
353 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
356 |> Map.drop(["since_id", "max_id", "min_id"])
359 last = List.last(activities)
366 |> Map.get("limit", "20")
367 |> String.to_integer()
370 if length(activities) <= limit do
376 |> Enum.at(limit * -1)
380 {next_url, prev_url} =
384 Pleroma.Web.Endpoint,
387 Map.merge(params, %{max_id: max_id})
390 Pleroma.Web.Endpoint,
393 Map.merge(params, %{min_id: min_id})
399 Pleroma.Web.Endpoint,
401 Map.merge(params, %{max_id: max_id})
404 Pleroma.Web.Endpoint,
406 Map.merge(params, %{min_id: min_id})
412 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
418 def home_timeline(%{assigns: %{user: user}} = conn, params) do
421 |> Map.put("type", ["Create", "Announce"])
422 |> Map.put("blocking_user", user)
423 |> Map.put("muting_user", user)
424 |> Map.put("user", user)
427 [user.ap_id | user.following]
428 |> ActivityPub.fetch_activities(params)
432 |> add_link_headers(:home_timeline, activities)
433 |> put_view(StatusView)
434 |> render("index.json", %{activities: activities, for: user, as: :activity})
437 def public_timeline(%{assigns: %{user: user}} = conn, params) do
438 local_only = params["local"] in [true, "True", "true", "1"]
442 |> Map.put("type", ["Create", "Announce"])
443 |> Map.put("local_only", local_only)
444 |> Map.put("blocking_user", user)
445 |> Map.put("muting_user", user)
446 |> Map.put("user", user)
447 |> ActivityPub.fetch_public_activities()
451 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
452 |> put_view(StatusView)
453 |> render("index.json", %{activities: activities, for: user, as: :activity})
456 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
457 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"]) do
460 |> Map.put("tag", params["tagged"])
462 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
465 |> add_link_headers(:user_statuses, activities, params["id"])
466 |> put_view(StatusView)
467 |> render("index.json", %{
468 activities: activities,
475 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
478 |> Map.put("type", "Create")
479 |> Map.put("blocking_user", user)
480 |> Map.put("user", user)
481 |> Map.put(:visibility, "direct")
485 |> ActivityPub.fetch_activities_query(params)
486 |> Pagination.fetch_paginated(params)
489 |> add_link_headers(:dm_timeline, activities)
490 |> put_view(StatusView)
491 |> render("index.json", %{activities: activities, for: user, as: :activity})
494 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
495 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
496 true <- Visibility.visible_for_user?(activity, user) do
498 |> put_view(StatusView)
499 |> try_render("status.json", %{activity: activity, for: user})
503 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
504 with %Activity{} = activity <- Activity.get_by_id(id),
506 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
507 "blocking_user" => user,
509 "exclude_id" => activity.id
511 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
517 activities: grouped_activities[true] || [],
521 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
526 activities: grouped_activities[false] || [],
530 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
537 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
538 with %Object{} = object <- Object.get_by_id(id),
539 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
540 true <- Visibility.visible_for_user?(activity, user) do
542 |> put_view(StatusView)
543 |> try_render("poll.json", %{object: object, for: user})
545 error when is_nil(error) or error == false ->
546 render_error(conn, :not_found, "Record not found")
550 defp get_cached_vote_or_vote(user, object, choices) do
551 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
554 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
555 case CommonAPI.vote(user, object, choices) do
556 {:error, _message} = res -> {:ignore, res}
557 res -> {:commit, res}
564 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
565 with %Object{} = object <- Object.get_by_id(id),
566 true <- object.data["type"] == "Question",
567 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
568 true <- Visibility.visible_for_user?(activity, user),
569 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
571 |> put_view(StatusView)
572 |> try_render("poll.json", %{object: object, for: user})
575 render_error(conn, :not_found, "Record not found")
578 render_error(conn, :not_found, "Record not found")
582 |> put_status(:unprocessable_entity)
583 |> json(%{error: message})
587 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
588 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
590 |> add_link_headers(:scheduled_statuses, scheduled_activities)
591 |> put_view(ScheduledActivityView)
592 |> render("index.json", %{scheduled_activities: scheduled_activities})
596 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
597 with %ScheduledActivity{} = scheduled_activity <-
598 ScheduledActivity.get(user, scheduled_activity_id) do
600 |> put_view(ScheduledActivityView)
601 |> render("show.json", %{scheduled_activity: scheduled_activity})
603 _ -> {:error, :not_found}
607 def update_scheduled_status(
608 %{assigns: %{user: user}} = conn,
609 %{"id" => scheduled_activity_id} = params
611 with %ScheduledActivity{} = scheduled_activity <-
612 ScheduledActivity.get(user, scheduled_activity_id),
613 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
615 |> put_view(ScheduledActivityView)
616 |> render("show.json", %{scheduled_activity: scheduled_activity})
618 nil -> {:error, :not_found}
623 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
624 with %ScheduledActivity{} = scheduled_activity <-
625 ScheduledActivity.get(user, scheduled_activity_id),
626 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
628 |> put_view(ScheduledActivityView)
629 |> render("show.json", %{scheduled_activity: scheduled_activity})
631 nil -> {:error, :not_found}
636 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
639 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
641 scheduled_at = params["scheduled_at"]
643 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
644 with {:ok, scheduled_activity} <-
645 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
647 |> put_view(ScheduledActivityView)
648 |> render("show.json", %{scheduled_activity: scheduled_activity})
651 params = Map.drop(params, ["scheduled_at"])
653 case CommonAPI.post(user, params) do
656 |> put_status(:unprocessable_entity)
657 |> json(%{error: message})
661 |> put_view(StatusView)
662 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
667 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
668 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
671 _e -> render_error(conn, :forbidden, "Can't delete this post")
675 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
676 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
677 %Activity{} = announce <- Activity.normalize(announce.data) do
679 |> put_view(StatusView)
680 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
684 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
685 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
686 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
688 |> put_view(StatusView)
689 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
693 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
694 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
695 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
697 |> put_view(StatusView)
698 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
702 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
703 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
704 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
706 |> put_view(StatusView)
707 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
711 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
712 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
714 |> put_view(StatusView)
715 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
719 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
720 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
722 |> put_view(StatusView)
723 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
727 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
728 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
729 %User{} = user <- User.get_cached_by_nickname(user.nickname),
730 true <- Visibility.visible_for_user?(activity, user),
731 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
733 |> put_view(StatusView)
734 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
738 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
739 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
740 %User{} = user <- User.get_cached_by_nickname(user.nickname),
741 true <- Visibility.visible_for_user?(activity, user),
742 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
744 |> put_view(StatusView)
745 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
749 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
750 activity = Activity.get_by_id(id)
752 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
754 |> put_view(StatusView)
755 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
759 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
760 activity = Activity.get_by_id(id)
762 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
764 |> put_view(StatusView)
765 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
769 def notifications(%{assigns: %{user: user}} = conn, params) do
770 notifications = MastodonAPI.get_notifications(user, params)
773 |> add_link_headers(:notifications, notifications)
774 |> put_view(NotificationView)
775 |> render("index.json", %{notifications: notifications, for: user})
778 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
779 with {:ok, notification} <- Notification.get(user, id) do
781 |> put_view(NotificationView)
782 |> render("show.json", %{notification: notification, for: user})
786 |> put_status(:forbidden)
787 |> json(%{"error" => reason})
791 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
792 Notification.clear(user)
796 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
797 with {:ok, _notif} <- Notification.dismiss(user, id) do
802 |> put_status(:forbidden)
803 |> json(%{"error" => reason})
807 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
808 Notification.destroy_multiple(user, ids)
812 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
814 q = from(u in User, where: u.id in ^id)
815 targets = Repo.all(q)
818 |> put_view(AccountView)
819 |> render("relationships.json", %{user: user, targets: targets})
822 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
823 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
825 def update_media(%{assigns: %{user: user}} = conn, data) do
826 with %Object{} = object <- Repo.get(Object, data["id"]),
827 true <- Object.authorize_mutation(object, user),
828 true <- is_binary(data["description"]),
829 description <- data["description"] do
830 new_data = %{object.data | "name" => description}
834 |> Object.change(%{data: new_data})
837 attachment_data = Map.put(new_data, "id", object.id)
840 |> put_view(StatusView)
841 |> render("attachment.json", %{attachment: attachment_data})
845 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
846 with {:ok, object} <-
849 actor: User.ap_id(user),
850 description: Map.get(data, "description")
852 attachment_data = Map.put(object.data, "id", object.id)
855 |> put_view(StatusView)
856 |> render("attachment.json", %{attachment: attachment_data})
860 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
861 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
862 %{} = attachment_data <- Map.put(object.data, "id", object.id),
863 %{type: type} = rendered <-
864 StatusView.render("attachment.json", %{attachment: attachment_data}) do
865 # Reject if not an image
866 if type == "image" do
868 # Save to the user's info
869 info_changeset = User.Info.mascot_update(user.info, rendered)
873 |> Ecto.Changeset.change()
874 |> Ecto.Changeset.put_embed(:info, info_changeset)
876 {:ok, _user} = User.update_and_set_cache(user_changeset)
881 render_error(conn, :unsupported_media_type, "mascots can only be images")
886 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
887 mascot = User.get_mascot(user)
893 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
894 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
895 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
896 q = from(u in User, where: u.ap_id in ^likes)
900 |> Enum.filter(&(not User.blocks?(user, &1)))
903 |> put_view(AccountView)
904 |> render("accounts.json", %{for: user, users: users, as: :user})
910 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
911 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
912 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
913 q = from(u in User, where: u.ap_id in ^announces)
917 |> Enum.filter(&(not User.blocks?(user, &1)))
920 |> put_view(AccountView)
921 |> render("accounts.json", %{for: user, users: users, as: :user})
927 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
928 local_only = params["local"] in [true, "True", "true", "1"]
931 [params["tag"], params["any"]]
935 |> Enum.map(&String.downcase(&1))
940 |> Enum.map(&String.downcase(&1))
945 |> Enum.map(&String.downcase(&1))
949 |> Map.put("type", "Create")
950 |> Map.put("local_only", local_only)
951 |> Map.put("blocking_user", user)
952 |> Map.put("muting_user", user)
953 |> Map.put("user", user)
954 |> Map.put("tag", tags)
955 |> Map.put("tag_all", tag_all)
956 |> Map.put("tag_reject", tag_reject)
957 |> ActivityPub.fetch_public_activities()
961 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
962 |> put_view(StatusView)
963 |> render("index.json", %{activities: activities, for: user, as: :activity})
966 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
967 with %User{} = user <- User.get_cached_by_id(id),
968 followers <- MastodonAPI.get_followers(user, params) do
971 for_user && user.id == for_user.id -> followers
972 user.info.hide_followers -> []
977 |> add_link_headers(:followers, followers, user)
978 |> put_view(AccountView)
979 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
983 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
984 with %User{} = user <- User.get_cached_by_id(id),
985 followers <- MastodonAPI.get_friends(user, params) do
988 for_user && user.id == for_user.id -> followers
989 user.info.hide_follows -> []
994 |> add_link_headers(:following, followers, user)
995 |> put_view(AccountView)
996 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
1000 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
1001 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
1003 |> put_view(AccountView)
1004 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
1008 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1009 with %User{} = follower <- User.get_cached_by_id(id),
1010 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
1012 |> put_view(AccountView)
1013 |> render("relationship.json", %{user: followed, target: follower})
1015 {:error, message} ->
1017 |> put_status(:forbidden)
1018 |> json(%{error: message})
1022 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1023 with %User{} = follower <- User.get_cached_by_id(id),
1024 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
1026 |> put_view(AccountView)
1027 |> render("relationship.json", %{user: followed, target: follower})
1029 {:error, message} ->
1031 |> put_status(:forbidden)
1032 |> json(%{error: message})
1036 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1037 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1038 {_, true} <- {:followed, follower.id != followed.id},
1039 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
1041 |> put_view(AccountView)
1042 |> render("relationship.json", %{user: follower, target: followed})
1045 {:error, :not_found}
1047 {:error, message} ->
1049 |> put_status(:forbidden)
1050 |> json(%{error: message})
1054 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1055 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1056 {_, true} <- {:followed, follower.id != followed.id},
1057 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1059 |> put_view(AccountView)
1060 |> render("account.json", %{user: followed, for: follower})
1063 {:error, :not_found}
1065 {:error, message} ->
1067 |> put_status(:forbidden)
1068 |> json(%{error: message})
1072 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1073 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1074 {_, true} <- {:followed, follower.id != followed.id},
1075 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1077 |> put_view(AccountView)
1078 |> render("relationship.json", %{user: follower, target: followed})
1081 {:error, :not_found}
1088 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1090 if Map.has_key?(params, "notifications"),
1091 do: params["notifications"] in [true, "True", "true", "1"],
1094 with %User{} = muted <- User.get_cached_by_id(id),
1095 {:ok, muter} <- User.mute(muter, muted, notifications) do
1097 |> put_view(AccountView)
1098 |> render("relationship.json", %{user: muter, target: muted})
1100 {:error, message} ->
1102 |> put_status(:forbidden)
1103 |> json(%{error: message})
1107 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1108 with %User{} = muted <- User.get_cached_by_id(id),
1109 {:ok, muter} <- User.unmute(muter, muted) do
1111 |> put_view(AccountView)
1112 |> render("relationship.json", %{user: muter, target: muted})
1114 {:error, message} ->
1116 |> put_status(:forbidden)
1117 |> json(%{error: message})
1121 def mutes(%{assigns: %{user: user}} = conn, _) do
1122 with muted_accounts <- User.muted_users(user) do
1123 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1128 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1129 with %User{} = blocked <- User.get_cached_by_id(id),
1130 {:ok, blocker} <- User.block(blocker, blocked),
1131 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1133 |> put_view(AccountView)
1134 |> render("relationship.json", %{user: blocker, target: blocked})
1136 {:error, message} ->
1138 |> put_status(:forbidden)
1139 |> json(%{error: message})
1143 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1144 with %User{} = blocked <- User.get_cached_by_id(id),
1145 {:ok, blocker} <- User.unblock(blocker, blocked),
1146 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1148 |> put_view(AccountView)
1149 |> render("relationship.json", %{user: blocker, target: blocked})
1151 {:error, message} ->
1153 |> put_status(:forbidden)
1154 |> json(%{error: message})
1158 def blocks(%{assigns: %{user: user}} = conn, _) do
1159 with blocked_accounts <- User.blocked_users(user) do
1160 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1165 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1166 json(conn, info.domain_blocks || [])
1169 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1170 User.block_domain(blocker, domain)
1174 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1175 User.unblock_domain(blocker, domain)
1179 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1180 with %User{} = subscription_target <- User.get_cached_by_id(id),
1181 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1183 |> put_view(AccountView)
1184 |> render("relationship.json", %{user: user, target: subscription_target})
1186 {:error, message} ->
1188 |> put_status(:forbidden)
1189 |> json(%{error: message})
1193 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1194 with %User{} = subscription_target <- User.get_cached_by_id(id),
1195 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1197 |> put_view(AccountView)
1198 |> render("relationship.json", %{user: user, target: subscription_target})
1200 {:error, message} ->
1202 |> put_status(:forbidden)
1203 |> json(%{error: message})
1207 def favourites(%{assigns: %{user: user}} = conn, params) do
1210 |> Map.put("type", "Create")
1211 |> Map.put("favorited_by", user.ap_id)
1212 |> Map.put("blocking_user", user)
1215 ActivityPub.fetch_activities([], params)
1219 |> add_link_headers(:favourites, activities)
1220 |> put_view(StatusView)
1221 |> render("index.json", %{activities: activities, for: user, as: :activity})
1224 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1225 with %User{} = user <- User.get_by_id(id),
1226 false <- user.info.hide_favorites do
1229 |> Map.put("type", "Create")
1230 |> Map.put("favorited_by", user.ap_id)
1231 |> Map.put("blocking_user", for_user)
1235 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1237 [Pleroma.Constants.as_public()]
1242 |> ActivityPub.fetch_activities(params)
1246 |> add_link_headers(:favourites, activities)
1247 |> put_view(StatusView)
1248 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1250 nil -> {:error, :not_found}
1251 true -> render_error(conn, :forbidden, "Can't get favorites")
1255 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1256 user = User.get_cached_by_id(user.id)
1259 Bookmark.for_user_query(user.id)
1260 |> Pagination.fetch_paginated(params)
1264 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1267 |> add_link_headers(:bookmarks, bookmarks)
1268 |> put_view(StatusView)
1269 |> render("index.json", %{activities: activities, for: user, as: :activity})
1272 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1273 lists = Pleroma.List.for_user(user, opts)
1274 res = ListView.render("lists.json", lists: lists)
1278 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1279 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1280 res = ListView.render("list.json", list: list)
1283 _e -> render_error(conn, :not_found, "Record not found")
1287 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1288 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1289 res = ListView.render("lists.json", lists: lists)
1293 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1294 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1295 {:ok, _list} <- Pleroma.List.delete(list) do
1299 json(conn, dgettext("errors", "error"))
1303 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1304 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1305 res = ListView.render("list.json", list: list)
1310 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1312 |> Enum.each(fn account_id ->
1313 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1314 %User{} = followed <- User.get_cached_by_id(account_id) do
1315 Pleroma.List.follow(list, followed)
1322 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1324 |> Enum.each(fn account_id ->
1325 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1326 %User{} = followed <- User.get_cached_by_id(account_id) do
1327 Pleroma.List.unfollow(list, followed)
1334 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1335 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1336 {:ok, users} = Pleroma.List.get_following(list) do
1338 |> put_view(AccountView)
1339 |> render("accounts.json", %{for: user, users: users, as: :user})
1343 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1344 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1345 {:ok, list} <- Pleroma.List.rename(list, title) do
1346 res = ListView.render("list.json", list: list)
1350 json(conn, dgettext("errors", "error"))
1354 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1355 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1358 |> Map.put("type", "Create")
1359 |> Map.put("blocking_user", user)
1360 |> Map.put("user", user)
1361 |> Map.put("muting_user", user)
1363 # we must filter the following list for the user to avoid leaking statuses the user
1364 # does not actually have permission to see (for more info, peruse security issue #270).
1367 |> Enum.filter(fn x -> x in user.following end)
1368 |> ActivityPub.fetch_activities_bounded(following, params)
1372 |> put_view(StatusView)
1373 |> render("index.json", %{activities: activities, for: user, as: :activity})
1375 _e -> render_error(conn, :forbidden, "Error.")
1379 def index(%{assigns: %{user: user}} = conn, _params) do
1380 token = get_session(conn, :oauth_token)
1383 mastodon_emoji = mastodonized_emoji()
1385 limit = Config.get([:instance, :limit])
1388 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1393 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1394 access_token: token,
1396 domain: Pleroma.Web.Endpoint.host(),
1399 unfollow_modal: false,
1402 auto_play_gif: false,
1403 display_sensitive_media: false,
1404 reduce_motion: false,
1405 max_toot_chars: limit,
1406 mascot: User.get_mascot(user)["url"]
1408 poll_limits: Config.get([:instance, :poll_limits]),
1410 delete_others_notice: present?(user.info.is_moderator),
1411 admin: present?(user.info.is_admin)
1415 default_privacy: user.info.default_scope,
1416 default_sensitive: false,
1417 allow_content_types: Config.get([:instance, :allowed_post_formats])
1419 media_attachments: %{
1420 accept_content_types: [
1436 user.info.settings ||
1466 push_subscription: nil,
1468 custom_emojis: mastodon_emoji,
1474 |> put_layout(false)
1475 |> put_view(MastodonView)
1476 |> render("index.html", %{initial_state: initial_state})
1479 |> put_session(:return_to, conn.request_path)
1480 |> redirect(to: "/web/login")
1484 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1485 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1487 with changeset <- Ecto.Changeset.change(user),
1488 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1489 {:ok, _user} <- User.update_and_set_cache(changeset) do
1494 |> put_status(:internal_server_error)
1495 |> json(%{error: inspect(e)})
1499 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1500 redirect(conn, to: local_mastodon_root_path(conn))
1503 @doc "Local Mastodon FE login init action"
1504 def login(conn, %{"code" => auth_token}) do
1505 with {:ok, app} <- get_or_make_app(),
1506 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1507 {:ok, token} <- Token.exchange_token(app, auth) do
1509 |> put_session(:oauth_token, token.token)
1510 |> redirect(to: local_mastodon_root_path(conn))
1514 @doc "Local Mastodon FE callback action"
1515 def login(conn, _) do
1516 with {:ok, app} <- get_or_make_app() do
1521 response_type: "code",
1522 client_id: app.client_id,
1524 scope: Enum.join(app.scopes, " ")
1527 redirect(conn, to: path)
1531 defp local_mastodon_root_path(conn) do
1532 case get_session(conn, :return_to) do
1534 mastodon_api_path(conn, :index, ["getting-started"])
1537 delete_session(conn, :return_to)
1542 defp get_or_make_app do
1543 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1544 scopes = ["read", "write", "follow", "push"]
1546 with %App{} = app <- Repo.get_by(App, find_attrs) do
1548 if app.scopes == scopes do
1552 |> Ecto.Changeset.change(%{scopes: scopes})
1560 App.register_changeset(
1562 Map.put(find_attrs, :scopes, scopes)
1569 def logout(conn, _) do
1572 |> redirect(to: "/")
1575 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1576 Logger.debug("Unimplemented, returning unmodified relationship")
1578 with %User{} = target <- User.get_cached_by_id(id) do
1580 |> put_view(AccountView)
1581 |> render("relationship.json", %{user: user, target: target})
1585 def empty_array(conn, _) do
1586 Logger.debug("Unimplemented, returning an empty array")
1590 def empty_object(conn, _) do
1591 Logger.debug("Unimplemented, returning an empty object")
1595 def get_filters(%{assigns: %{user: user}} = conn, _) do
1596 filters = Filter.get_filters(user)
1597 res = FilterView.render("filters.json", filters: filters)
1602 %{assigns: %{user: user}} = conn,
1603 %{"phrase" => phrase, "context" => context} = params
1609 hide: Map.get(params, "irreversible", false),
1610 whole_word: Map.get(params, "boolean", true)
1614 {:ok, response} = Filter.create(query)
1615 res = FilterView.render("filter.json", filter: response)
1619 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1620 filter = Filter.get(filter_id, user)
1621 res = FilterView.render("filter.json", filter: filter)
1626 %{assigns: %{user: user}} = conn,
1627 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1631 filter_id: filter_id,
1634 hide: Map.get(params, "irreversible", nil),
1635 whole_word: Map.get(params, "boolean", true)
1639 {:ok, response} = Filter.update(query)
1640 res = FilterView.render("filter.json", filter: response)
1644 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1647 filter_id: filter_id
1650 {:ok, _} = Filter.delete(query)
1656 def errors(conn, {:error, %Changeset{} = changeset}) do
1659 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1660 |> Enum.map_join(", ", fn {_k, v} -> v end)
1663 |> put_status(:unprocessable_entity)
1664 |> json(%{error: error_message})
1667 def errors(conn, {:error, :not_found}) do
1668 render_error(conn, :not_found, "Record not found")
1671 def errors(conn, {:error, error_message}) do
1673 |> put_status(:bad_request)
1674 |> json(%{error: error_message})
1677 def errors(conn, _) do
1679 |> put_status(:internal_server_error)
1680 |> json(dgettext("errors", "Something went wrong"))
1683 def suggestions(%{assigns: %{user: user}} = conn, _) do
1684 suggestions = Config.get(:suggestions)
1686 if Keyword.get(suggestions, :enabled, false) do
1687 api = Keyword.get(suggestions, :third_party_engine, "")
1688 timeout = Keyword.get(suggestions, :timeout, 5000)
1689 limit = Keyword.get(suggestions, :limit, 23)
1691 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1693 user = user.nickname
1697 |> String.replace("{{host}}", host)
1698 |> String.replace("{{user}}", user)
1700 with {:ok, %{status: 200, body: body}} <-
1701 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1702 {:ok, data} <- Jason.decode(body) do
1705 |> Enum.slice(0, limit)
1708 |> Map.put("id", fetch_suggestion_id(x))
1709 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1710 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1716 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1723 defp fetch_suggestion_id(attrs) do
1724 case User.get_or_fetch(attrs["acct"]) do
1725 {:ok, %User{id: id}} -> id
1730 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1731 with %Activity{} = activity <- Activity.get_by_id(status_id),
1732 true <- Visibility.visible_for_user?(activity, user) do
1736 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1746 def reports(%{assigns: %{user: user}} = conn, params) do
1747 case CommonAPI.report(user, params) do
1750 |> put_view(ReportView)
1751 |> try_render("report.json", %{activity: activity})
1755 |> put_status(:bad_request)
1756 |> json(%{error: err})
1760 def account_register(
1761 %{assigns: %{app: app}} = conn,
1762 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1770 "captcha_answer_data",
1774 |> Map.put("nickname", nickname)
1775 |> Map.put("fullname", params["fullname"] || nickname)
1776 |> Map.put("bio", params["bio"] || "")
1777 |> Map.put("confirm", params["password"])
1779 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1780 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1782 token_type: "Bearer",
1783 access_token: token.token,
1785 created_at: Token.Utils.format_created_at(token)
1790 |> put_status(:bad_request)
1795 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1796 render_error(conn, :bad_request, "Missing parameters")
1799 def account_register(conn, _) do
1800 render_error(conn, :forbidden, "Invalid credentials")
1803 def conversations(%{assigns: %{user: user}} = conn, params) do
1804 participations = Participation.for_user_with_last_activity_id(user, params)
1807 Enum.map(participations, fn participation ->
1808 ConversationView.render("participation.json", %{participation: participation, user: user})
1812 |> add_link_headers(:conversations, participations)
1813 |> json(conversations)
1816 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1817 with %Participation{} = participation <-
1818 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1819 {:ok, participation} <- Participation.mark_as_read(participation) do
1820 participation_view =
1821 ConversationView.render("participation.json", %{participation: participation, user: user})
1824 |> json(participation_view)
1828 def password_reset(conn, params) do
1829 nickname_or_email = params["email"] || params["nickname"]
1831 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1833 |> put_status(:no_content)
1836 {:error, "unknown user"} ->
1837 send_resp(conn, :not_found, "")
1840 send_resp(conn, :bad_request, "")
1844 def account_confirmation_resend(conn, params) do
1845 nickname_or_email = params["email"] || params["nickname"]
1847 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1848 {:ok, _} <- User.try_send_confirmation_email(user) do
1850 |> json_response(:no_content, "")
1854 def try_render(conn, target, params)
1855 when is_binary(target) do
1856 case render(conn, target, params) do
1857 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1862 def try_render(conn, _, _) do
1863 render_error(conn, :not_implemented, "Can't display this activity")
1866 defp present?(nil), do: false
1867 defp present?(false), do: false
1868 defp present?(_), do: true