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
53 @rate_limited_relations_actions ~w(follow unfollow)a
55 @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
56 post_status delete_status)a
60 {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
61 when action in ~w(reblog_status unreblog_status)a
66 {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
67 when action in ~w(fav_status unfav_status)a
72 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
75 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
76 plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
77 plug(RateLimiter, :app_account_creation when action == :account_register)
78 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
79 plug(RateLimiter, :password_reset when action == :password_reset)
80 plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
82 @local_mastodon_name "Mastodon-Local"
84 action_fallback(:errors)
86 def create_app(conn, params) do
87 scopes = Scopes.fetch_scopes(params, ["read"])
91 |> Map.drop(["scope", "scopes"])
92 |> Map.put("scopes", scopes)
94 with cs <- App.register_changeset(%App{}, app_attrs),
95 false <- cs.changes[:client_name] == @local_mastodon_name,
96 {:ok, app} <- Repo.insert(cs) do
99 |> render("show.json", %{app: app})
108 value_function \\ fn x -> {:ok, x} end
110 if Map.has_key?(params, params_field) do
111 case value_function.(params[params_field]) do
112 {:ok, new_value} -> Map.put(map, map_field, new_value)
120 def update_credentials(%{assigns: %{user: user}} = conn, params) do
125 |> add_if_present(params, "display_name", :name)
126 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
127 |> add_if_present(params, "avatar", :avatar, fn value ->
128 with %Plug.Upload{} <- value,
129 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
136 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
139 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
150 :skip_thread_containment
152 |> Enum.reduce(%{}, fn key, acc ->
153 add_if_present(acc, params, to_string(key), key, fn value ->
154 {:ok, ControllerHelper.truthy_param?(value)}
157 |> add_if_present(params, "default_scope", :default_scope)
158 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
159 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
161 |> add_if_present(params, "header", :banner, fn value ->
162 with %Plug.Upload{} <- value,
163 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
169 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
170 with %Plug.Upload{} <- value,
171 {:ok, object} <- ActivityPub.upload(value, type: :background) do
177 |> Map.put(:emoji, user_info_emojis)
179 info_cng = User.Info.profile_update(user.info, info_params)
181 with changeset <- User.update_changeset(user, user_params),
182 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
183 {:ok, user} <- User.update_and_set_cache(changeset) do
184 if original_user != user do
185 CommonAPI.update(user)
190 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
193 _e -> render_error(conn, :forbidden, "Invalid request")
197 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
198 change = Changeset.change(user, %{avatar: nil})
199 {:ok, user} = User.update_and_set_cache(change)
200 CommonAPI.update(user)
202 json(conn, %{url: nil})
205 def update_avatar(%{assigns: %{user: user}} = conn, params) do
206 {:ok, object} = ActivityPub.upload(params, type: :avatar)
207 change = Changeset.change(user, %{avatar: object.data})
208 {:ok, user} = User.update_and_set_cache(change)
209 CommonAPI.update(user)
210 %{"url" => [%{"href" => href} | _]} = object.data
212 json(conn, %{url: href})
215 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
216 with new_info <- %{"banner" => %{}},
217 info_cng <- User.Info.profile_update(user.info, new_info),
218 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
219 {:ok, user} <- User.update_and_set_cache(changeset) do
220 CommonAPI.update(user)
222 json(conn, %{url: nil})
226 def update_banner(%{assigns: %{user: user}} = conn, params) do
227 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
228 new_info <- %{"banner" => object.data},
229 info_cng <- User.Info.profile_update(user.info, new_info),
230 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
231 {:ok, user} <- User.update_and_set_cache(changeset) do
232 CommonAPI.update(user)
233 %{"url" => [%{"href" => href} | _]} = object.data
235 json(conn, %{url: href})
239 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
240 with new_info <- %{"background" => %{}},
241 info_cng <- User.Info.profile_update(user.info, new_info),
242 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
243 {:ok, _user} <- User.update_and_set_cache(changeset) do
244 json(conn, %{url: nil})
248 def update_background(%{assigns: %{user: user}} = conn, params) do
249 with {:ok, object} <- ActivityPub.upload(params, type: :background),
250 new_info <- %{"background" => object.data},
251 info_cng <- User.Info.profile_update(user.info, new_info),
252 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
253 {:ok, _user} <- User.update_and_set_cache(changeset) do
254 %{"url" => [%{"href" => href} | _]} = object.data
256 json(conn, %{url: href})
260 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
261 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
264 AccountView.render("account.json", %{
267 with_pleroma_settings: true,
268 with_chat_token: chat_token
274 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
275 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
278 |> render("short.json", %{app: app})
282 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
283 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
284 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
285 account = AccountView.render("account.json", %{user: user, for: for_user})
288 _e -> render_error(conn, :not_found, "Can't find user")
292 @mastodon_api_level "2.7.2"
294 def masto_instance(conn, _params) do
295 instance = Config.get(:instance)
299 title: Keyword.get(instance, :name),
300 description: Keyword.get(instance, :description),
301 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
302 email: Keyword.get(instance, :email),
304 streaming_api: Pleroma.Web.Endpoint.websocket_url()
306 stats: Stats.get_stats(),
307 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
309 registrations: Pleroma.Config.get([:instance, :registrations_open]),
310 # Extra (not present in Mastodon):
311 max_toot_chars: Keyword.get(instance, :limit),
312 poll_limits: Keyword.get(instance, :poll_limits)
318 def peers(conn, _params) do
319 json(conn, Stats.get_peers())
322 defp mastodonized_emoji do
323 Pleroma.Emoji.get_all()
324 |> Enum.map(fn {shortcode, relative_url, tags} ->
325 url = to_string(URI.merge(Web.base_url(), relative_url))
328 "shortcode" => shortcode,
330 "visible_in_picker" => true,
333 # Assuming that a comma is authorized in the category name
334 "category" => (tags -- ["Custom"]) |> Enum.join(",")
339 def custom_emojis(conn, _params) do
340 mastodon_emoji = mastodonized_emoji()
341 json(conn, mastodon_emoji)
344 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
347 |> Map.drop(["since_id", "max_id", "min_id"])
350 last = List.last(activities)
357 |> Map.get("limit", "20")
358 |> String.to_integer()
361 if length(activities) <= limit do
367 |> Enum.at(limit * -1)
371 {next_url, prev_url} =
375 Pleroma.Web.Endpoint,
378 Map.merge(params, %{max_id: max_id})
381 Pleroma.Web.Endpoint,
384 Map.merge(params, %{min_id: min_id})
390 Pleroma.Web.Endpoint,
392 Map.merge(params, %{max_id: max_id})
395 Pleroma.Web.Endpoint,
397 Map.merge(params, %{min_id: min_id})
403 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
409 def home_timeline(%{assigns: %{user: user}} = conn, params) do
412 |> Map.put("type", ["Create", "Announce"])
413 |> Map.put("blocking_user", user)
414 |> Map.put("muting_user", user)
415 |> Map.put("user", user)
418 [user.ap_id | user.following]
419 |> ActivityPub.fetch_activities(params)
423 |> add_link_headers(:home_timeline, activities)
424 |> put_view(StatusView)
425 |> render("index.json", %{activities: activities, for: user, as: :activity})
428 def public_timeline(%{assigns: %{user: user}} = conn, params) do
429 local_only = params["local"] in [true, "True", "true", "1"]
433 |> Map.put("type", ["Create", "Announce"])
434 |> Map.put("local_only", local_only)
435 |> Map.put("blocking_user", user)
436 |> Map.put("muting_user", user)
437 |> ActivityPub.fetch_public_activities()
441 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
442 |> put_view(StatusView)
443 |> render("index.json", %{activities: activities, for: user, as: :activity})
446 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
447 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"]) do
450 |> Map.put("tag", params["tagged"])
452 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
455 |> add_link_headers(:user_statuses, activities, params["id"])
456 |> put_view(StatusView)
457 |> render("index.json", %{
458 activities: activities,
465 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
468 |> Map.put("type", "Create")
469 |> Map.put("blocking_user", user)
470 |> Map.put("user", user)
471 |> Map.put(:visibility, "direct")
475 |> ActivityPub.fetch_activities_query(params)
476 |> Pagination.fetch_paginated(params)
479 |> add_link_headers(:dm_timeline, activities)
480 |> put_view(StatusView)
481 |> render("index.json", %{activities: activities, for: user, as: :activity})
484 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
485 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
486 true <- Visibility.visible_for_user?(activity, user) do
488 |> put_view(StatusView)
489 |> try_render("status.json", %{activity: activity, for: user})
493 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
494 with %Activity{} = activity <- Activity.get_by_id(id),
496 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
497 "blocking_user" => user,
501 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
503 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
504 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
510 activities: grouped_activities[true] || [],
514 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
519 activities: grouped_activities[false] || [],
523 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
530 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
531 with %Object{} = object <- Object.get_by_id(id),
532 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
533 true <- Visibility.visible_for_user?(activity, user) do
535 |> put_view(StatusView)
536 |> try_render("poll.json", %{object: object, for: user})
538 nil -> render_error(conn, :not_found, "Record not found")
539 false -> render_error(conn, :not_found, "Record not found")
543 defp get_cached_vote_or_vote(user, object, choices) do
544 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
547 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
548 case CommonAPI.vote(user, object, choices) do
549 {:error, _message} = res -> {:ignore, res}
550 res -> {:commit, res}
557 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
558 with %Object{} = object <- Object.get_by_id(id),
559 true <- object.data["type"] == "Question",
560 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
561 true <- Visibility.visible_for_user?(activity, user),
562 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
564 |> put_view(StatusView)
565 |> try_render("poll.json", %{object: object, for: user})
568 render_error(conn, :not_found, "Record not found")
571 render_error(conn, :not_found, "Record not found")
575 |> put_status(:unprocessable_entity)
576 |> json(%{error: message})
580 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
581 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
583 |> add_link_headers(:scheduled_statuses, scheduled_activities)
584 |> put_view(ScheduledActivityView)
585 |> render("index.json", %{scheduled_activities: scheduled_activities})
589 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
590 with %ScheduledActivity{} = scheduled_activity <-
591 ScheduledActivity.get(user, scheduled_activity_id) do
593 |> put_view(ScheduledActivityView)
594 |> render("show.json", %{scheduled_activity: scheduled_activity})
596 _ -> {:error, :not_found}
600 def update_scheduled_status(
601 %{assigns: %{user: user}} = conn,
602 %{"id" => scheduled_activity_id} = params
604 with %ScheduledActivity{} = scheduled_activity <-
605 ScheduledActivity.get(user, scheduled_activity_id),
606 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
608 |> put_view(ScheduledActivityView)
609 |> render("show.json", %{scheduled_activity: scheduled_activity})
611 nil -> {:error, :not_found}
616 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
617 with %ScheduledActivity{} = scheduled_activity <-
618 ScheduledActivity.get(user, scheduled_activity_id),
619 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
621 |> put_view(ScheduledActivityView)
622 |> render("show.json", %{scheduled_activity: scheduled_activity})
624 nil -> {:error, :not_found}
629 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
632 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
634 scheduled_at = params["scheduled_at"]
636 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
637 with {:ok, scheduled_activity} <-
638 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
640 |> put_view(ScheduledActivityView)
641 |> render("show.json", %{scheduled_activity: scheduled_activity})
644 params = Map.drop(params, ["scheduled_at"])
646 case CommonAPI.post(user, params) do
649 |> put_status(:unprocessable_entity)
650 |> json(%{error: message})
654 |> put_view(StatusView)
655 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
660 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
661 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
664 _e -> render_error(conn, :forbidden, "Can't delete this post")
668 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
669 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
670 %Activity{} = announce <- Activity.normalize(announce.data) do
672 |> put_view(StatusView)
673 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
677 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
678 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
679 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
681 |> put_view(StatusView)
682 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
686 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
687 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
688 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
690 |> put_view(StatusView)
691 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
695 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
696 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
697 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
699 |> put_view(StatusView)
700 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
704 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
705 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
707 |> put_view(StatusView)
708 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
712 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
713 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
715 |> put_view(StatusView)
716 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
720 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
721 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
722 %User{} = user <- User.get_cached_by_nickname(user.nickname),
723 true <- Visibility.visible_for_user?(activity, user),
724 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
726 |> put_view(StatusView)
727 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
731 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
732 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
733 %User{} = user <- User.get_cached_by_nickname(user.nickname),
734 true <- Visibility.visible_for_user?(activity, user),
735 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
737 |> put_view(StatusView)
738 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
742 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
743 activity = Activity.get_by_id(id)
745 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
747 |> put_view(StatusView)
748 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
752 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
753 activity = Activity.get_by_id(id)
755 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
757 |> put_view(StatusView)
758 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
762 def notifications(%{assigns: %{user: user}} = conn, params) do
763 notifications = MastodonAPI.get_notifications(user, params)
766 |> add_link_headers(:notifications, notifications)
767 |> put_view(NotificationView)
768 |> render("index.json", %{notifications: notifications, for: user})
771 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
772 with {:ok, notification} <- Notification.get(user, id) do
774 |> put_view(NotificationView)
775 |> render("show.json", %{notification: notification, for: user})
779 |> put_status(:forbidden)
780 |> json(%{"error" => reason})
784 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
785 Notification.clear(user)
789 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
790 with {:ok, _notif} <- Notification.dismiss(user, id) do
795 |> put_status(:forbidden)
796 |> json(%{"error" => reason})
800 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
801 Notification.destroy_multiple(user, ids)
805 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
807 q = from(u in User, where: u.id in ^id)
808 targets = Repo.all(q)
811 |> put_view(AccountView)
812 |> render("relationships.json", %{user: user, targets: targets})
815 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
816 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
818 def update_media(%{assigns: %{user: user}} = conn, data) do
819 with %Object{} = object <- Repo.get(Object, data["id"]),
820 true <- Object.authorize_mutation(object, user),
821 true <- is_binary(data["description"]),
822 description <- data["description"] do
823 new_data = %{object.data | "name" => description}
827 |> Object.change(%{data: new_data})
830 attachment_data = Map.put(new_data, "id", object.id)
833 |> put_view(StatusView)
834 |> render("attachment.json", %{attachment: attachment_data})
838 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
839 with {:ok, object} <-
842 actor: User.ap_id(user),
843 description: Map.get(data, "description")
845 attachment_data = Map.put(object.data, "id", object.id)
848 |> put_view(StatusView)
849 |> render("attachment.json", %{attachment: attachment_data})
853 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
854 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
855 %{} = attachment_data <- Map.put(object.data, "id", object.id),
856 %{type: type} = rendered <-
857 StatusView.render("attachment.json", %{attachment: attachment_data}) do
858 # Reject if not an image
859 if type == "image" do
861 # Save to the user's info
862 info_changeset = User.Info.mascot_update(user.info, rendered)
866 |> Ecto.Changeset.change()
867 |> Ecto.Changeset.put_embed(:info, info_changeset)
869 {:ok, _user} = User.update_and_set_cache(user_changeset)
874 render_error(conn, :unsupported_media_type, "mascots can only be images")
879 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
880 mascot = User.get_mascot(user)
886 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
887 with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id),
888 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
889 q = from(u in User, where: u.ap_id in ^likes)
893 |> Enum.filter(&(not User.blocks?(user, &1)))
896 |> put_view(AccountView)
897 |> render("accounts.json", %{for: user, users: users, as: :user})
903 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
904 with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id),
905 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
906 q = from(u in User, where: u.ap_id in ^announces)
910 |> Enum.filter(&(not User.blocks?(user, &1)))
913 |> put_view(AccountView)
914 |> render("accounts.json", %{for: user, users: users, as: :user})
920 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
921 local_only = params["local"] in [true, "True", "true", "1"]
924 [params["tag"], params["any"]]
928 |> Enum.map(&String.downcase(&1))
933 |> Enum.map(&String.downcase(&1))
938 |> Enum.map(&String.downcase(&1))
942 |> Map.put("type", "Create")
943 |> Map.put("local_only", local_only)
944 |> Map.put("blocking_user", user)
945 |> Map.put("muting_user", user)
946 |> Map.put("tag", tags)
947 |> Map.put("tag_all", tag_all)
948 |> Map.put("tag_reject", tag_reject)
949 |> ActivityPub.fetch_public_activities()
953 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
954 |> put_view(StatusView)
955 |> render("index.json", %{activities: activities, for: user, as: :activity})
958 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
959 with %User{} = user <- User.get_cached_by_id(id),
960 followers <- MastodonAPI.get_followers(user, params) do
963 for_user && user.id == for_user.id -> followers
964 user.info.hide_followers -> []
969 |> add_link_headers(:followers, followers, user)
970 |> put_view(AccountView)
971 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
975 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
976 with %User{} = user <- User.get_cached_by_id(id),
977 followers <- MastodonAPI.get_friends(user, params) do
980 for_user && user.id == for_user.id -> followers
981 user.info.hide_follows -> []
986 |> add_link_headers(:following, followers, user)
987 |> put_view(AccountView)
988 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
992 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
993 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
995 |> put_view(AccountView)
996 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
1000 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1001 with %User{} = follower <- User.get_cached_by_id(id),
1002 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
1004 |> put_view(AccountView)
1005 |> render("relationship.json", %{user: followed, target: follower})
1007 {:error, message} ->
1009 |> put_status(:forbidden)
1010 |> json(%{error: message})
1014 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1015 with %User{} = follower <- User.get_cached_by_id(id),
1016 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
1018 |> put_view(AccountView)
1019 |> render("relationship.json", %{user: followed, target: follower})
1021 {:error, message} ->
1023 |> put_status(:forbidden)
1024 |> json(%{error: message})
1028 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1029 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1030 {_, true} <- {:followed, follower.id != followed.id},
1031 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
1033 |> put_view(AccountView)
1034 |> render("relationship.json", %{user: follower, target: followed})
1037 {:error, :not_found}
1039 {:error, message} ->
1041 |> put_status(:forbidden)
1042 |> json(%{error: message})
1046 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1047 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1048 {_, true} <- {:followed, follower.id != followed.id},
1049 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1051 |> put_view(AccountView)
1052 |> render("account.json", %{user: followed, for: follower})
1055 {:error, :not_found}
1057 {:error, message} ->
1059 |> put_status(:forbidden)
1060 |> json(%{error: message})
1064 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1065 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1066 {_, true} <- {:followed, follower.id != followed.id},
1067 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1069 |> put_view(AccountView)
1070 |> render("relationship.json", %{user: follower, target: followed})
1073 {:error, :not_found}
1080 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1082 if Map.has_key?(params, "notifications"),
1083 do: params["notifications"] in [true, "True", "true", "1"],
1086 with %User{} = muted <- User.get_cached_by_id(id),
1087 {:ok, muter} <- User.mute(muter, muted, notifications) do
1089 |> put_view(AccountView)
1090 |> render("relationship.json", %{user: muter, target: muted})
1092 {:error, message} ->
1094 |> put_status(:forbidden)
1095 |> json(%{error: message})
1099 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1100 with %User{} = muted <- User.get_cached_by_id(id),
1101 {:ok, muter} <- User.unmute(muter, muted) do
1103 |> put_view(AccountView)
1104 |> render("relationship.json", %{user: muter, target: muted})
1106 {:error, message} ->
1108 |> put_status(:forbidden)
1109 |> json(%{error: message})
1113 def mutes(%{assigns: %{user: user}} = conn, _) do
1114 with muted_accounts <- User.muted_users(user) do
1115 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1120 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1121 with %User{} = blocked <- User.get_cached_by_id(id),
1122 {:ok, blocker} <- User.block(blocker, blocked),
1123 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1125 |> put_view(AccountView)
1126 |> render("relationship.json", %{user: blocker, target: blocked})
1128 {:error, message} ->
1130 |> put_status(:forbidden)
1131 |> json(%{error: message})
1135 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1136 with %User{} = blocked <- User.get_cached_by_id(id),
1137 {:ok, blocker} <- User.unblock(blocker, blocked),
1138 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1140 |> put_view(AccountView)
1141 |> render("relationship.json", %{user: blocker, target: blocked})
1143 {:error, message} ->
1145 |> put_status(:forbidden)
1146 |> json(%{error: message})
1150 def blocks(%{assigns: %{user: user}} = conn, _) do
1151 with blocked_accounts <- User.blocked_users(user) do
1152 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1157 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1158 json(conn, info.domain_blocks || [])
1161 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1162 User.block_domain(blocker, domain)
1166 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1167 User.unblock_domain(blocker, domain)
1171 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1172 with %User{} = subscription_target <- User.get_cached_by_id(id),
1173 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1175 |> put_view(AccountView)
1176 |> render("relationship.json", %{user: user, target: subscription_target})
1178 {:error, message} ->
1180 |> put_status(:forbidden)
1181 |> json(%{error: message})
1185 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1186 with %User{} = subscription_target <- User.get_cached_by_id(id),
1187 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1189 |> put_view(AccountView)
1190 |> render("relationship.json", %{user: user, target: subscription_target})
1192 {:error, message} ->
1194 |> put_status(:forbidden)
1195 |> json(%{error: message})
1199 def favourites(%{assigns: %{user: user}} = conn, params) do
1202 |> Map.put("type", "Create")
1203 |> Map.put("favorited_by", user.ap_id)
1204 |> Map.put("blocking_user", user)
1207 ActivityPub.fetch_activities([], params)
1211 |> add_link_headers(:favourites, activities)
1212 |> put_view(StatusView)
1213 |> render("index.json", %{activities: activities, for: user, as: :activity})
1216 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1217 with %User{} = user <- User.get_by_id(id),
1218 false <- user.info.hide_favorites do
1221 |> Map.put("type", "Create")
1222 |> Map.put("favorited_by", user.ap_id)
1223 |> Map.put("blocking_user", for_user)
1227 ["https://www.w3.org/ns/activitystreams#Public"] ++
1228 [for_user.ap_id | for_user.following]
1230 ["https://www.w3.org/ns/activitystreams#Public"]
1235 |> ActivityPub.fetch_activities(params)
1239 |> add_link_headers(:favourites, activities)
1240 |> put_view(StatusView)
1241 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1243 nil -> {:error, :not_found}
1244 true -> render_error(conn, :forbidden, "Can't get favorites")
1248 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1249 user = User.get_cached_by_id(user.id)
1252 Bookmark.for_user_query(user.id)
1253 |> Pagination.fetch_paginated(params)
1257 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1260 |> add_link_headers(:bookmarks, bookmarks)
1261 |> put_view(StatusView)
1262 |> render("index.json", %{activities: activities, for: user, as: :activity})
1265 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1266 lists = Pleroma.List.for_user(user, opts)
1267 res = ListView.render("lists.json", lists: lists)
1271 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1272 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1273 res = ListView.render("list.json", list: list)
1276 _e -> render_error(conn, :not_found, "Record not found")
1280 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1281 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1282 res = ListView.render("lists.json", lists: lists)
1286 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1287 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1288 {:ok, _list} <- Pleroma.List.delete(list) do
1292 json(conn, dgettext("errors", "error"))
1296 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1297 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1298 res = ListView.render("list.json", list: list)
1303 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1305 |> Enum.each(fn account_id ->
1306 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1307 %User{} = followed <- User.get_cached_by_id(account_id) do
1308 Pleroma.List.follow(list, followed)
1315 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1317 |> Enum.each(fn account_id ->
1318 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1319 %User{} = followed <- User.get_cached_by_id(account_id) do
1320 Pleroma.List.unfollow(list, followed)
1327 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1328 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1329 {:ok, users} = Pleroma.List.get_following(list) do
1331 |> put_view(AccountView)
1332 |> render("accounts.json", %{for: user, users: users, as: :user})
1336 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1337 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1338 {:ok, list} <- Pleroma.List.rename(list, title) do
1339 res = ListView.render("list.json", list: list)
1343 json(conn, dgettext("errors", "error"))
1347 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1348 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1351 |> Map.put("type", "Create")
1352 |> Map.put("blocking_user", user)
1353 |> Map.put("muting_user", user)
1355 # we must filter the following list for the user to avoid leaking statuses the user
1356 # does not actually have permission to see (for more info, peruse security issue #270).
1359 |> Enum.filter(fn x -> x in user.following end)
1360 |> ActivityPub.fetch_activities_bounded(following, params)
1364 |> put_view(StatusView)
1365 |> render("index.json", %{activities: activities, for: user, as: :activity})
1367 _e -> render_error(conn, :forbidden, "Error.")
1371 def index(%{assigns: %{user: user}} = conn, _params) do
1372 token = get_session(conn, :oauth_token)
1375 mastodon_emoji = mastodonized_emoji()
1377 limit = Config.get([:instance, :limit])
1380 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1385 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1386 access_token: token,
1388 domain: Pleroma.Web.Endpoint.host(),
1391 unfollow_modal: false,
1394 auto_play_gif: false,
1395 display_sensitive_media: false,
1396 reduce_motion: false,
1397 max_toot_chars: limit,
1398 mascot: User.get_mascot(user)["url"]
1400 poll_limits: Config.get([:instance, :poll_limits]),
1402 delete_others_notice: present?(user.info.is_moderator),
1403 admin: present?(user.info.is_admin)
1407 default_privacy: user.info.default_scope,
1408 default_sensitive: false,
1409 allow_content_types: Config.get([:instance, :allowed_post_formats])
1411 media_attachments: %{
1412 accept_content_types: [
1428 user.info.settings ||
1458 push_subscription: nil,
1460 custom_emojis: mastodon_emoji,
1466 |> put_layout(false)
1467 |> put_view(MastodonView)
1468 |> render("index.html", %{initial_state: initial_state})
1471 |> put_session(:return_to, conn.request_path)
1472 |> redirect(to: "/web/login")
1476 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1477 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1479 with changeset <- Ecto.Changeset.change(user),
1480 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1481 {:ok, _user} <- User.update_and_set_cache(changeset) do
1486 |> put_status(:internal_server_error)
1487 |> json(%{error: inspect(e)})
1491 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1492 redirect(conn, to: local_mastodon_root_path(conn))
1495 @doc "Local Mastodon FE login init action"
1496 def login(conn, %{"code" => auth_token}) do
1497 with {:ok, app} <- get_or_make_app(),
1498 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1499 {:ok, token} <- Token.exchange_token(app, auth) do
1501 |> put_session(:oauth_token, token.token)
1502 |> redirect(to: local_mastodon_root_path(conn))
1506 @doc "Local Mastodon FE callback action"
1507 def login(conn, _) do
1508 with {:ok, app} <- get_or_make_app() do
1513 response_type: "code",
1514 client_id: app.client_id,
1516 scope: Enum.join(app.scopes, " ")
1519 redirect(conn, to: path)
1523 defp local_mastodon_root_path(conn) do
1524 case get_session(conn, :return_to) do
1526 mastodon_api_path(conn, :index, ["getting-started"])
1529 delete_session(conn, :return_to)
1534 defp get_or_make_app do
1535 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1536 scopes = ["read", "write", "follow", "push"]
1538 with %App{} = app <- Repo.get_by(App, find_attrs) do
1540 if app.scopes == scopes do
1544 |> Ecto.Changeset.change(%{scopes: scopes})
1552 App.register_changeset(
1554 Map.put(find_attrs, :scopes, scopes)
1561 def logout(conn, _) do
1564 |> redirect(to: "/")
1567 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1568 Logger.debug("Unimplemented, returning unmodified relationship")
1570 with %User{} = target <- User.get_cached_by_id(id) do
1572 |> put_view(AccountView)
1573 |> render("relationship.json", %{user: user, target: target})
1577 def empty_array(conn, _) do
1578 Logger.debug("Unimplemented, returning an empty array")
1582 def empty_object(conn, _) do
1583 Logger.debug("Unimplemented, returning an empty object")
1587 def get_filters(%{assigns: %{user: user}} = conn, _) do
1588 filters = Filter.get_filters(user)
1589 res = FilterView.render("filters.json", filters: filters)
1594 %{assigns: %{user: user}} = conn,
1595 %{"phrase" => phrase, "context" => context} = params
1601 hide: Map.get(params, "irreversible", false),
1602 whole_word: Map.get(params, "boolean", true)
1606 {:ok, response} = Filter.create(query)
1607 res = FilterView.render("filter.json", filter: response)
1611 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1612 filter = Filter.get(filter_id, user)
1613 res = FilterView.render("filter.json", filter: filter)
1618 %{assigns: %{user: user}} = conn,
1619 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1623 filter_id: filter_id,
1626 hide: Map.get(params, "irreversible", nil),
1627 whole_word: Map.get(params, "boolean", true)
1631 {:ok, response} = Filter.update(query)
1632 res = FilterView.render("filter.json", filter: response)
1636 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1639 filter_id: filter_id
1642 {:ok, _} = Filter.delete(query)
1648 def errors(conn, {:error, %Changeset{} = changeset}) do
1651 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1652 |> Enum.map_join(", ", fn {_k, v} -> v end)
1655 |> put_status(:unprocessable_entity)
1656 |> json(%{error: error_message})
1659 def errors(conn, {:error, :not_found}) do
1660 render_error(conn, :not_found, "Record not found")
1663 def errors(conn, {:error, error_message}) do
1665 |> put_status(:bad_request)
1666 |> json(%{error: error_message})
1669 def errors(conn, _) do
1671 |> put_status(:internal_server_error)
1672 |> json(dgettext("errors", "Something went wrong"))
1675 def suggestions(%{assigns: %{user: user}} = conn, _) do
1676 suggestions = Config.get(:suggestions)
1678 if Keyword.get(suggestions, :enabled, false) do
1679 api = Keyword.get(suggestions, :third_party_engine, "")
1680 timeout = Keyword.get(suggestions, :timeout, 5000)
1681 limit = Keyword.get(suggestions, :limit, 23)
1683 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1685 user = user.nickname
1689 |> String.replace("{{host}}", host)
1690 |> String.replace("{{user}}", user)
1692 with {:ok, %{status: 200, body: body}} <-
1697 recv_timeout: timeout,
1701 {:ok, data} <- Jason.decode(body) do
1704 |> Enum.slice(0, limit)
1709 case User.get_or_fetch(x["acct"]) do
1710 {:ok, %User{id: id}} -> id
1716 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1719 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1725 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1732 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1733 with %Activity{} = activity <- Activity.get_by_id(status_id),
1734 true <- Visibility.visible_for_user?(activity, user) do
1738 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1748 def reports(%{assigns: %{user: user}} = conn, params) do
1749 case CommonAPI.report(user, params) do
1752 |> put_view(ReportView)
1753 |> try_render("report.json", %{activity: activity})
1757 |> put_status(:bad_request)
1758 |> json(%{error: err})
1762 def account_register(
1763 %{assigns: %{app: app}} = conn,
1764 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1772 "captcha_answer_data",
1776 |> Map.put("nickname", nickname)
1777 |> Map.put("fullname", params["fullname"] || nickname)
1778 |> Map.put("bio", params["bio"] || "")
1779 |> Map.put("confirm", params["password"])
1781 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1782 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1784 token_type: "Bearer",
1785 access_token: token.token,
1787 created_at: Token.Utils.format_created_at(token)
1792 |> put_status(:bad_request)
1797 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1798 render_error(conn, :bad_request, "Missing parameters")
1801 def account_register(conn, _) do
1802 render_error(conn, :forbidden, "Invalid credentials")
1805 def conversations(%{assigns: %{user: user}} = conn, params) do
1806 participations = Participation.for_user_with_last_activity_id(user, params)
1809 Enum.map(participations, fn participation ->
1810 ConversationView.render("participation.json", %{participation: participation, user: user})
1814 |> add_link_headers(:conversations, participations)
1815 |> json(conversations)
1818 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1819 with %Participation{} = participation <-
1820 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1821 {:ok, participation} <- Participation.mark_as_read(participation) do
1822 participation_view =
1823 ConversationView.render("participation.json", %{participation: participation, user: user})
1826 |> json(participation_view)
1830 def password_reset(conn, params) do
1831 nickname_or_email = params["email"] || params["nickname"]
1833 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1835 |> put_status(:no_content)
1838 {:error, "unknown user"} ->
1839 send_resp(conn, :not_found, "")
1842 send_resp(conn, :bad_request, "")
1846 def account_confirmation_resend(conn, params) do
1847 nickname_or_email = params["email"] || params["nickname"]
1849 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1850 {:ok, _} <- User.try_send_confirmation_email(user) do
1852 |> json_response(:no_content, "")
1856 def try_render(conn, target, params)
1857 when is_binary(target) do
1858 case render(conn, target, params) do
1859 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1864 def try_render(conn, _, _) do
1865 render_error(conn, :not_implemented, "Can't display this activity")
1868 defp present?(nil), do: false
1869 defp present?(false), do: false
1870 defp present?(_), do: true