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"] || "")
140 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
151 :skip_thread_containment
153 |> Enum.reduce(%{}, fn key, acc ->
154 add_if_present(acc, params, to_string(key), key, fn value ->
155 {:ok, ControllerHelper.truthy_param?(value)}
158 |> add_if_present(params, "default_scope", :default_scope)
159 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
160 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
162 |> add_if_present(params, "header", :banner, fn value ->
163 with %Plug.Upload{} <- value,
164 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
170 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
171 with %Plug.Upload{} <- value,
172 {:ok, object} <- ActivityPub.upload(value, type: :background) do
178 |> Map.put(:emoji, user_info_emojis)
180 info_cng = User.Info.profile_update(user.info, info_params)
182 with changeset <- User.update_changeset(user, user_params),
183 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
184 {:ok, user} <- User.update_and_set_cache(changeset) do
185 if original_user != user do
186 CommonAPI.update(user)
191 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
194 _e -> render_error(conn, :forbidden, "Invalid request")
198 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
199 change = Changeset.change(user, %{avatar: nil})
200 {:ok, user} = User.update_and_set_cache(change)
201 CommonAPI.update(user)
203 json(conn, %{url: nil})
206 def update_avatar(%{assigns: %{user: user}} = conn, params) do
207 {:ok, object} = ActivityPub.upload(params, type: :avatar)
208 change = Changeset.change(user, %{avatar: object.data})
209 {:ok, user} = User.update_and_set_cache(change)
210 CommonAPI.update(user)
211 %{"url" => [%{"href" => href} | _]} = object.data
213 json(conn, %{url: href})
216 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
217 with new_info <- %{"banner" => %{}},
218 info_cng <- User.Info.profile_update(user.info, new_info),
219 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
220 {:ok, user} <- User.update_and_set_cache(changeset) do
221 CommonAPI.update(user)
223 json(conn, %{url: nil})
227 def update_banner(%{assigns: %{user: user}} = conn, params) do
228 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
229 new_info <- %{"banner" => object.data},
230 info_cng <- User.Info.profile_update(user.info, new_info),
231 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
232 {:ok, user} <- User.update_and_set_cache(changeset) do
233 CommonAPI.update(user)
234 %{"url" => [%{"href" => href} | _]} = object.data
236 json(conn, %{url: href})
240 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
241 with new_info <- %{"background" => %{}},
242 info_cng <- User.Info.profile_update(user.info, new_info),
243 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
244 {:ok, _user} <- User.update_and_set_cache(changeset) do
245 json(conn, %{url: nil})
249 def update_background(%{assigns: %{user: user}} = conn, params) do
250 with {:ok, object} <- ActivityPub.upload(params, type: :background),
251 new_info <- %{"background" => object.data},
252 info_cng <- User.Info.profile_update(user.info, new_info),
253 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
254 {:ok, _user} <- User.update_and_set_cache(changeset) do
255 %{"url" => [%{"href" => href} | _]} = object.data
257 json(conn, %{url: href})
261 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
262 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
265 AccountView.render("account.json", %{
268 with_pleroma_settings: true,
269 with_chat_token: chat_token
275 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
276 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
279 |> render("short.json", %{app: app})
283 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
284 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
285 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
286 account = AccountView.render("account.json", %{user: user, for: for_user})
289 _e -> render_error(conn, :not_found, "Can't find user")
293 @mastodon_api_level "2.7.2"
295 def masto_instance(conn, _params) do
296 instance = Config.get(:instance)
300 title: Keyword.get(instance, :name),
301 description: Keyword.get(instance, :description),
302 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
303 email: Keyword.get(instance, :email),
305 streaming_api: Pleroma.Web.Endpoint.websocket_url()
307 stats: Stats.get_stats(),
308 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
310 registrations: Pleroma.Config.get([:instance, :registrations_open]),
311 # Extra (not present in Mastodon):
312 max_toot_chars: Keyword.get(instance, :limit),
313 poll_limits: Keyword.get(instance, :poll_limits)
319 def peers(conn, _params) do
320 json(conn, Stats.get_peers())
323 defp mastodonized_emoji do
324 Pleroma.Emoji.get_all()
325 |> Enum.map(fn {shortcode, relative_url, tags} ->
326 url = to_string(URI.merge(Web.base_url(), relative_url))
329 "shortcode" => shortcode,
331 "visible_in_picker" => true,
334 # Assuming that a comma is authorized in the category name
335 "category" => (tags -- ["Custom"]) |> Enum.join(",")
340 def custom_emojis(conn, _params) do
341 mastodon_emoji = mastodonized_emoji()
342 json(conn, mastodon_emoji)
345 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
348 |> Map.drop(["since_id", "max_id", "min_id"])
351 last = List.last(activities)
358 |> Map.get("limit", "20")
359 |> String.to_integer()
362 if length(activities) <= limit do
368 |> Enum.at(limit * -1)
372 {next_url, prev_url} =
376 Pleroma.Web.Endpoint,
379 Map.merge(params, %{max_id: max_id})
382 Pleroma.Web.Endpoint,
385 Map.merge(params, %{min_id: min_id})
391 Pleroma.Web.Endpoint,
393 Map.merge(params, %{max_id: max_id})
396 Pleroma.Web.Endpoint,
398 Map.merge(params, %{min_id: min_id})
404 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
410 def home_timeline(%{assigns: %{user: user}} = conn, params) do
413 |> Map.put("type", ["Create", "Announce"])
414 |> Map.put("blocking_user", user)
415 |> Map.put("muting_user", user)
416 |> Map.put("user", user)
419 [user.ap_id | user.following]
420 |> ActivityPub.fetch_activities(params)
424 |> add_link_headers(:home_timeline, activities)
425 |> put_view(StatusView)
426 |> render("index.json", %{activities: activities, for: user, as: :activity})
429 def public_timeline(%{assigns: %{user: user}} = conn, params) do
430 local_only = params["local"] in [true, "True", "true", "1"]
434 |> Map.put("type", ["Create", "Announce"])
435 |> Map.put("local_only", local_only)
436 |> Map.put("blocking_user", user)
437 |> Map.put("muting_user", user)
438 |> ActivityPub.fetch_public_activities()
442 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
443 |> put_view(StatusView)
444 |> render("index.json", %{activities: activities, for: user, as: :activity})
447 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
448 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"]) do
451 |> Map.put("tag", params["tagged"])
453 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
456 |> add_link_headers(:user_statuses, activities, params["id"])
457 |> put_view(StatusView)
458 |> render("index.json", %{
459 activities: activities,
466 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
469 |> Map.put("type", "Create")
470 |> Map.put("blocking_user", user)
471 |> Map.put("user", user)
472 |> Map.put(:visibility, "direct")
476 |> ActivityPub.fetch_activities_query(params)
477 |> Pagination.fetch_paginated(params)
480 |> add_link_headers(:dm_timeline, activities)
481 |> put_view(StatusView)
482 |> render("index.json", %{activities: activities, for: user, as: :activity})
485 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
486 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
487 true <- Visibility.visible_for_user?(activity, user) do
489 |> put_view(StatusView)
490 |> try_render("status.json", %{activity: activity, for: user})
494 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
495 with %Activity{} = activity <- Activity.get_by_id(id),
497 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
498 "blocking_user" => user,
502 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
504 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
505 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
511 activities: grouped_activities[true] || [],
515 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
520 activities: grouped_activities[false] || [],
524 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
531 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
532 with %Object{} = object <- Object.get_by_id(id),
533 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
534 true <- Visibility.visible_for_user?(activity, user) do
536 |> put_view(StatusView)
537 |> try_render("poll.json", %{object: object, for: user})
539 nil -> render_error(conn, :not_found, "Record not found")
540 false -> render_error(conn, :not_found, "Record not found")
544 defp get_cached_vote_or_vote(user, object, choices) do
545 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
548 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
549 case CommonAPI.vote(user, object, choices) do
550 {:error, _message} = res -> {:ignore, res}
551 res -> {:commit, res}
558 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
559 with %Object{} = object <- Object.get_by_id(id),
560 true <- object.data["type"] == "Question",
561 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
562 true <- Visibility.visible_for_user?(activity, user),
563 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
565 |> put_view(StatusView)
566 |> try_render("poll.json", %{object: object, for: user})
569 render_error(conn, :not_found, "Record not found")
572 render_error(conn, :not_found, "Record not found")
576 |> put_status(:unprocessable_entity)
577 |> json(%{error: message})
581 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
582 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
584 |> add_link_headers(:scheduled_statuses, scheduled_activities)
585 |> put_view(ScheduledActivityView)
586 |> render("index.json", %{scheduled_activities: scheduled_activities})
590 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
591 with %ScheduledActivity{} = scheduled_activity <-
592 ScheduledActivity.get(user, scheduled_activity_id) do
594 |> put_view(ScheduledActivityView)
595 |> render("show.json", %{scheduled_activity: scheduled_activity})
597 _ -> {:error, :not_found}
601 def update_scheduled_status(
602 %{assigns: %{user: user}} = conn,
603 %{"id" => scheduled_activity_id} = params
605 with %ScheduledActivity{} = scheduled_activity <-
606 ScheduledActivity.get(user, scheduled_activity_id),
607 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
609 |> put_view(ScheduledActivityView)
610 |> render("show.json", %{scheduled_activity: scheduled_activity})
612 nil -> {:error, :not_found}
617 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
618 with %ScheduledActivity{} = scheduled_activity <-
619 ScheduledActivity.get(user, scheduled_activity_id),
620 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
622 |> put_view(ScheduledActivityView)
623 |> render("show.json", %{scheduled_activity: scheduled_activity})
625 nil -> {:error, :not_found}
630 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
633 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
635 scheduled_at = params["scheduled_at"]
637 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
638 with {:ok, scheduled_activity} <-
639 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
641 |> put_view(ScheduledActivityView)
642 |> render("show.json", %{scheduled_activity: scheduled_activity})
645 params = Map.drop(params, ["scheduled_at"])
647 case CommonAPI.post(user, params) do
650 |> put_status(:unprocessable_entity)
651 |> json(%{error: message})
655 |> put_view(StatusView)
656 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
661 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
662 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
665 _e -> render_error(conn, :forbidden, "Can't delete this post")
669 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
670 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
671 %Activity{} = announce <- Activity.normalize(announce.data) do
673 |> put_view(StatusView)
674 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
678 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
679 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
680 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
682 |> put_view(StatusView)
683 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
687 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
688 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
689 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
691 |> put_view(StatusView)
692 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
696 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
697 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
698 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
700 |> put_view(StatusView)
701 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
705 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
706 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
708 |> put_view(StatusView)
709 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
713 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
714 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
716 |> put_view(StatusView)
717 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
721 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
722 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
723 %User{} = user <- User.get_cached_by_nickname(user.nickname),
724 true <- Visibility.visible_for_user?(activity, user),
725 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
727 |> put_view(StatusView)
728 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
732 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
733 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
734 %User{} = user <- User.get_cached_by_nickname(user.nickname),
735 true <- Visibility.visible_for_user?(activity, user),
736 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
738 |> put_view(StatusView)
739 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
743 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
744 activity = Activity.get_by_id(id)
746 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
748 |> put_view(StatusView)
749 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
753 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
754 activity = Activity.get_by_id(id)
756 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
758 |> put_view(StatusView)
759 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
763 def notifications(%{assigns: %{user: user}} = conn, params) do
764 notifications = MastodonAPI.get_notifications(user, params)
767 |> add_link_headers(:notifications, notifications)
768 |> put_view(NotificationView)
769 |> render("index.json", %{notifications: notifications, for: user})
772 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
773 with {:ok, notification} <- Notification.get(user, id) do
775 |> put_view(NotificationView)
776 |> render("show.json", %{notification: notification, for: user})
780 |> put_status(:forbidden)
781 |> json(%{"error" => reason})
785 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
786 Notification.clear(user)
790 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
791 with {:ok, _notif} <- Notification.dismiss(user, id) do
796 |> put_status(:forbidden)
797 |> json(%{"error" => reason})
801 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
802 Notification.destroy_multiple(user, ids)
806 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
808 q = from(u in User, where: u.id in ^id)
809 targets = Repo.all(q)
812 |> put_view(AccountView)
813 |> render("relationships.json", %{user: user, targets: targets})
816 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
817 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
819 def update_media(%{assigns: %{user: user}} = conn, data) do
820 with %Object{} = object <- Repo.get(Object, data["id"]),
821 true <- Object.authorize_mutation(object, user),
822 true <- is_binary(data["description"]),
823 description <- data["description"] do
824 new_data = %{object.data | "name" => description}
828 |> Object.change(%{data: new_data})
831 attachment_data = Map.put(new_data, "id", object.id)
834 |> put_view(StatusView)
835 |> render("attachment.json", %{attachment: attachment_data})
839 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
840 with {:ok, object} <-
843 actor: User.ap_id(user),
844 description: Map.get(data, "description")
846 attachment_data = Map.put(object.data, "id", object.id)
849 |> put_view(StatusView)
850 |> render("attachment.json", %{attachment: attachment_data})
854 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
855 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
856 %{} = attachment_data <- Map.put(object.data, "id", object.id),
857 %{type: type} = rendered <-
858 StatusView.render("attachment.json", %{attachment: attachment_data}) do
859 # Reject if not an image
860 if type == "image" do
862 # Save to the user's info
863 info_changeset = User.Info.mascot_update(user.info, rendered)
867 |> Ecto.Changeset.change()
868 |> Ecto.Changeset.put_embed(:info, info_changeset)
870 {:ok, _user} = User.update_and_set_cache(user_changeset)
875 render_error(conn, :unsupported_media_type, "mascots can only be images")
880 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
881 mascot = User.get_mascot(user)
887 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
888 with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id),
889 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
890 q = from(u in User, where: u.ap_id in ^likes)
894 |> Enum.filter(&(not User.blocks?(user, &1)))
897 |> put_view(AccountView)
898 |> render("accounts.json", %{for: user, users: users, as: :user})
904 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
905 with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id),
906 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
907 q = from(u in User, where: u.ap_id in ^announces)
911 |> Enum.filter(&(not User.blocks?(user, &1)))
914 |> put_view(AccountView)
915 |> render("accounts.json", %{for: user, users: users, as: :user})
921 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
922 local_only = params["local"] in [true, "True", "true", "1"]
925 [params["tag"], params["any"]]
929 |> Enum.map(&String.downcase(&1))
934 |> Enum.map(&String.downcase(&1))
939 |> Enum.map(&String.downcase(&1))
943 |> Map.put("type", "Create")
944 |> Map.put("local_only", local_only)
945 |> Map.put("blocking_user", user)
946 |> Map.put("muting_user", user)
947 |> Map.put("tag", tags)
948 |> Map.put("tag_all", tag_all)
949 |> Map.put("tag_reject", tag_reject)
950 |> ActivityPub.fetch_public_activities()
954 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
955 |> put_view(StatusView)
956 |> render("index.json", %{activities: activities, for: user, as: :activity})
959 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
960 with %User{} = user <- User.get_cached_by_id(id),
961 followers <- MastodonAPI.get_followers(user, params) do
964 for_user && user.id == for_user.id -> followers
965 user.info.hide_followers -> []
970 |> add_link_headers(:followers, followers, user)
971 |> put_view(AccountView)
972 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
976 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
977 with %User{} = user <- User.get_cached_by_id(id),
978 followers <- MastodonAPI.get_friends(user, params) do
981 for_user && user.id == for_user.id -> followers
982 user.info.hide_follows -> []
987 |> add_link_headers(:following, followers, user)
988 |> put_view(AccountView)
989 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
993 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
994 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
996 |> put_view(AccountView)
997 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
1001 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1002 with %User{} = follower <- User.get_cached_by_id(id),
1003 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
1005 |> put_view(AccountView)
1006 |> render("relationship.json", %{user: followed, target: follower})
1008 {:error, message} ->
1010 |> put_status(:forbidden)
1011 |> json(%{error: message})
1015 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1016 with %User{} = follower <- User.get_cached_by_id(id),
1017 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
1019 |> put_view(AccountView)
1020 |> render("relationship.json", %{user: followed, target: follower})
1022 {:error, message} ->
1024 |> put_status(:forbidden)
1025 |> json(%{error: message})
1029 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1030 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1031 {_, true} <- {:followed, follower.id != followed.id},
1032 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
1034 |> put_view(AccountView)
1035 |> render("relationship.json", %{user: follower, target: followed})
1038 {:error, :not_found}
1040 {:error, message} ->
1042 |> put_status(:forbidden)
1043 |> json(%{error: message})
1047 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1048 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1049 {_, true} <- {:followed, follower.id != followed.id},
1050 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1052 |> put_view(AccountView)
1053 |> render("account.json", %{user: followed, for: follower})
1056 {:error, :not_found}
1058 {:error, message} ->
1060 |> put_status(:forbidden)
1061 |> json(%{error: message})
1065 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1066 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1067 {_, true} <- {:followed, follower.id != followed.id},
1068 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1070 |> put_view(AccountView)
1071 |> render("relationship.json", %{user: follower, target: followed})
1074 {:error, :not_found}
1081 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1083 if Map.has_key?(params, "notifications"),
1084 do: params["notifications"] in [true, "True", "true", "1"],
1087 with %User{} = muted <- User.get_cached_by_id(id),
1088 {:ok, muter} <- User.mute(muter, muted, notifications) do
1090 |> put_view(AccountView)
1091 |> render("relationship.json", %{user: muter, target: muted})
1093 {:error, message} ->
1095 |> put_status(:forbidden)
1096 |> json(%{error: message})
1100 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1101 with %User{} = muted <- User.get_cached_by_id(id),
1102 {:ok, muter} <- User.unmute(muter, muted) do
1104 |> put_view(AccountView)
1105 |> render("relationship.json", %{user: muter, target: muted})
1107 {:error, message} ->
1109 |> put_status(:forbidden)
1110 |> json(%{error: message})
1114 def mutes(%{assigns: %{user: user}} = conn, _) do
1115 with muted_accounts <- User.muted_users(user) do
1116 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1121 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1122 with %User{} = blocked <- User.get_cached_by_id(id),
1123 {:ok, blocker} <- User.block(blocker, blocked),
1124 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1126 |> put_view(AccountView)
1127 |> render("relationship.json", %{user: blocker, target: blocked})
1129 {:error, message} ->
1131 |> put_status(:forbidden)
1132 |> json(%{error: message})
1136 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1137 with %User{} = blocked <- User.get_cached_by_id(id),
1138 {:ok, blocker} <- User.unblock(blocker, blocked),
1139 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1141 |> put_view(AccountView)
1142 |> render("relationship.json", %{user: blocker, target: blocked})
1144 {:error, message} ->
1146 |> put_status(:forbidden)
1147 |> json(%{error: message})
1151 def blocks(%{assigns: %{user: user}} = conn, _) do
1152 with blocked_accounts <- User.blocked_users(user) do
1153 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1158 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1159 json(conn, info.domain_blocks || [])
1162 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1163 User.block_domain(blocker, domain)
1167 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1168 User.unblock_domain(blocker, domain)
1172 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1173 with %User{} = subscription_target <- User.get_cached_by_id(id),
1174 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1176 |> put_view(AccountView)
1177 |> render("relationship.json", %{user: user, target: subscription_target})
1179 {:error, message} ->
1181 |> put_status(:forbidden)
1182 |> json(%{error: message})
1186 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1187 with %User{} = subscription_target <- User.get_cached_by_id(id),
1188 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1190 |> put_view(AccountView)
1191 |> render("relationship.json", %{user: user, target: subscription_target})
1193 {:error, message} ->
1195 |> put_status(:forbidden)
1196 |> json(%{error: message})
1200 def favourites(%{assigns: %{user: user}} = conn, params) do
1203 |> Map.put("type", "Create")
1204 |> Map.put("favorited_by", user.ap_id)
1205 |> Map.put("blocking_user", user)
1208 ActivityPub.fetch_activities([], params)
1212 |> add_link_headers(:favourites, activities)
1213 |> put_view(StatusView)
1214 |> render("index.json", %{activities: activities, for: user, as: :activity})
1217 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1218 with %User{} = user <- User.get_by_id(id),
1219 false <- user.info.hide_favorites do
1222 |> Map.put("type", "Create")
1223 |> Map.put("favorited_by", user.ap_id)
1224 |> Map.put("blocking_user", for_user)
1228 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1230 [Pleroma.Constants.as_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