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 |> Map.put("user", user)
439 |> ActivityPub.fetch_public_activities()
443 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
444 |> put_view(StatusView)
445 |> render("index.json", %{activities: activities, for: user, as: :activity})
448 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
449 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"]) do
452 |> Map.put("tag", params["tagged"])
454 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
457 |> add_link_headers(:user_statuses, activities, params["id"])
458 |> put_view(StatusView)
459 |> render("index.json", %{
460 activities: activities,
467 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
470 |> Map.put("type", "Create")
471 |> Map.put("blocking_user", user)
472 |> Map.put("user", user)
473 |> Map.put(:visibility, "direct")
477 |> ActivityPub.fetch_activities_query(params)
478 |> Pagination.fetch_paginated(params)
481 |> add_link_headers(:dm_timeline, activities)
482 |> put_view(StatusView)
483 |> render("index.json", %{activities: activities, for: user, as: :activity})
486 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
487 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
488 true <- Visibility.visible_for_user?(activity, user) do
490 |> put_view(StatusView)
491 |> try_render("status.json", %{activity: activity, for: user})
495 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
496 with %Activity{} = activity <- Activity.get_by_id(id),
498 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
499 "blocking_user" => user,
501 "exclude_id" => activity.id
503 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
509 activities: grouped_activities[true] || [],
513 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
518 activities: grouped_activities[false] || [],
522 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
529 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
530 with %Object{} = object <- Object.get_by_id(id),
531 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
532 true <- Visibility.visible_for_user?(activity, user) do
534 |> put_view(StatusView)
535 |> try_render("poll.json", %{object: object, for: user})
537 error when is_nil(error) or error == false ->
538 render_error(conn, :not_found, "Record not found")
542 defp get_cached_vote_or_vote(user, object, choices) do
543 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
546 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
547 case CommonAPI.vote(user, object, choices) do
548 {:error, _message} = res -> {:ignore, res}
549 res -> {:commit, res}
556 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
557 with %Object{} = object <- Object.get_by_id(id),
558 true <- object.data["type"] == "Question",
559 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
560 true <- Visibility.visible_for_user?(activity, user),
561 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
563 |> put_view(StatusView)
564 |> try_render("poll.json", %{object: object, for: user})
567 render_error(conn, :not_found, "Record not found")
570 render_error(conn, :not_found, "Record not found")
574 |> put_status(:unprocessable_entity)
575 |> json(%{error: message})
579 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
580 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
582 |> add_link_headers(:scheduled_statuses, scheduled_activities)
583 |> put_view(ScheduledActivityView)
584 |> render("index.json", %{scheduled_activities: scheduled_activities})
588 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
589 with %ScheduledActivity{} = scheduled_activity <-
590 ScheduledActivity.get(user, scheduled_activity_id) do
592 |> put_view(ScheduledActivityView)
593 |> render("show.json", %{scheduled_activity: scheduled_activity})
595 _ -> {:error, :not_found}
599 def update_scheduled_status(
600 %{assigns: %{user: user}} = conn,
601 %{"id" => scheduled_activity_id} = params
603 with %ScheduledActivity{} = scheduled_activity <-
604 ScheduledActivity.get(user, scheduled_activity_id),
605 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
607 |> put_view(ScheduledActivityView)
608 |> render("show.json", %{scheduled_activity: scheduled_activity})
610 nil -> {:error, :not_found}
615 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
616 with %ScheduledActivity{} = scheduled_activity <-
617 ScheduledActivity.get(user, scheduled_activity_id),
618 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
620 |> put_view(ScheduledActivityView)
621 |> render("show.json", %{scheduled_activity: scheduled_activity})
623 nil -> {:error, :not_found}
628 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
631 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
633 scheduled_at = params["scheduled_at"]
635 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
636 with {:ok, scheduled_activity} <-
637 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
639 |> put_view(ScheduledActivityView)
640 |> render("show.json", %{scheduled_activity: scheduled_activity})
643 params = Map.drop(params, ["scheduled_at"])
645 case CommonAPI.post(user, params) do
648 |> put_status(:unprocessable_entity)
649 |> json(%{error: message})
653 |> put_view(StatusView)
654 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
659 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
660 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
663 _e -> render_error(conn, :forbidden, "Can't delete this post")
667 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
668 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
669 %Activity{} = announce <- Activity.normalize(announce.data) do
671 |> put_view(StatusView)
672 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
676 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
677 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
678 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
680 |> put_view(StatusView)
681 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
685 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
686 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
687 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
689 |> put_view(StatusView)
690 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
694 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
695 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
696 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
698 |> put_view(StatusView)
699 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
703 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
704 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
706 |> put_view(StatusView)
707 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
711 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
712 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
714 |> put_view(StatusView)
715 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
719 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
720 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
721 %User{} = user <- User.get_cached_by_nickname(user.nickname),
722 true <- Visibility.visible_for_user?(activity, user),
723 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
725 |> put_view(StatusView)
726 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
730 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
731 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
732 %User{} = user <- User.get_cached_by_nickname(user.nickname),
733 true <- Visibility.visible_for_user?(activity, user),
734 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
736 |> put_view(StatusView)
737 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
741 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
742 activity = Activity.get_by_id(id)
744 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
746 |> put_view(StatusView)
747 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
751 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
752 activity = Activity.get_by_id(id)
754 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
756 |> put_view(StatusView)
757 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
761 def notifications(%{assigns: %{user: user}} = conn, params) do
762 notifications = MastodonAPI.get_notifications(user, params)
765 |> add_link_headers(:notifications, notifications)
766 |> put_view(NotificationView)
767 |> render("index.json", %{notifications: notifications, for: user})
770 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
771 with {:ok, notification} <- Notification.get(user, id) do
773 |> put_view(NotificationView)
774 |> render("show.json", %{notification: notification, for: user})
778 |> put_status(:forbidden)
779 |> json(%{"error" => reason})
783 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
784 Notification.clear(user)
788 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
789 with {:ok, _notif} <- Notification.dismiss(user, id) do
794 |> put_status(:forbidden)
795 |> json(%{"error" => reason})
799 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
800 Notification.destroy_multiple(user, ids)
804 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
806 q = from(u in User, where: u.id in ^id)
807 targets = Repo.all(q)
810 |> put_view(AccountView)
811 |> render("relationships.json", %{user: user, targets: targets})
814 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
815 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
817 def update_media(%{assigns: %{user: user}} = conn, data) do
818 with %Object{} = object <- Repo.get(Object, data["id"]),
819 true <- Object.authorize_mutation(object, user),
820 true <- is_binary(data["description"]),
821 description <- data["description"] do
822 new_data = %{object.data | "name" => description}
826 |> Object.change(%{data: new_data})
829 attachment_data = Map.put(new_data, "id", object.id)
832 |> put_view(StatusView)
833 |> render("attachment.json", %{attachment: attachment_data})
837 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
838 with {:ok, object} <-
841 actor: User.ap_id(user),
842 description: Map.get(data, "description")
844 attachment_data = Map.put(object.data, "id", object.id)
847 |> put_view(StatusView)
848 |> render("attachment.json", %{attachment: attachment_data})
852 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
853 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
854 %{} = attachment_data <- Map.put(object.data, "id", object.id),
855 %{type: type} = rendered <-
856 StatusView.render("attachment.json", %{attachment: attachment_data}) do
857 # Reject if not an image
858 if type == "image" do
860 # Save to the user's info
861 info_changeset = User.Info.mascot_update(user.info, rendered)
865 |> Ecto.Changeset.change()
866 |> Ecto.Changeset.put_embed(:info, info_changeset)
868 {:ok, _user} = User.update_and_set_cache(user_changeset)
873 render_error(conn, :unsupported_media_type, "mascots can only be images")
878 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
879 mascot = User.get_mascot(user)
885 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
886 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
887 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
888 q = from(u in User, where: u.ap_id in ^likes)
892 |> Enum.filter(&(not User.blocks?(user, &1)))
895 |> put_view(AccountView)
896 |> render("accounts.json", %{for: user, users: users, as: :user})
902 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
903 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
904 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
905 q = from(u in User, where: u.ap_id in ^announces)
909 |> Enum.filter(&(not User.blocks?(user, &1)))
912 |> put_view(AccountView)
913 |> render("accounts.json", %{for: user, users: users, as: :user})
919 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
920 local_only = params["local"] in [true, "True", "true", "1"]
923 [params["tag"], params["any"]]
927 |> Enum.map(&String.downcase(&1))
932 |> Enum.map(&String.downcase(&1))
937 |> Enum.map(&String.downcase(&1))
941 |> Map.put("type", "Create")
942 |> Map.put("local_only", local_only)
943 |> Map.put("blocking_user", user)
944 |> Map.put("muting_user", user)
945 |> Map.put("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 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1229 [Pleroma.Constants.as_public()]
1234 |> ActivityPub.fetch_activities(params)
1238 |> add_link_headers(:favourites, activities)
1239 |> put_view(StatusView)
1240 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1242 nil -> {:error, :not_found}
1243 true -> render_error(conn, :forbidden, "Can't get favorites")
1247 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1248 user = User.get_cached_by_id(user.id)
1251 Bookmark.for_user_query(user.id)
1252 |> Pagination.fetch_paginated(params)
1256 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1259 |> add_link_headers(:bookmarks, bookmarks)
1260 |> put_view(StatusView)
1261 |> render("index.json", %{activities: activities, for: user, as: :activity})
1264 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1265 lists = Pleroma.List.for_user(user, opts)
1266 res = ListView.render("lists.json", lists: lists)
1270 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1271 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1272 res = ListView.render("list.json", list: list)
1275 _e -> render_error(conn, :not_found, "Record not found")
1279 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1280 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1281 res = ListView.render("lists.json", lists: lists)
1285 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1286 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1287 {:ok, _list} <- Pleroma.List.delete(list) do
1291 json(conn, dgettext("errors", "error"))
1295 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1296 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1297 res = ListView.render("list.json", list: list)
1302 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1304 |> Enum.each(fn account_id ->
1305 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1306 %User{} = followed <- User.get_cached_by_id(account_id) do
1307 Pleroma.List.follow(list, followed)
1314 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1316 |> Enum.each(fn account_id ->
1317 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1318 %User{} = followed <- User.get_cached_by_id(account_id) do
1319 Pleroma.List.unfollow(list, followed)
1326 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1327 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1328 {:ok, users} = Pleroma.List.get_following(list) do
1330 |> put_view(AccountView)
1331 |> render("accounts.json", %{for: user, users: users, as: :user})
1335 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1336 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1337 {:ok, list} <- Pleroma.List.rename(list, title) do
1338 res = ListView.render("list.json", list: list)
1342 json(conn, dgettext("errors", "error"))
1346 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1347 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1350 |> Map.put("type", "Create")
1351 |> Map.put("blocking_user", user)
1352 |> Map.put("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}} <-
1693 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1694 {:ok, data} <- Jason.decode(body) do
1697 |> Enum.slice(0, limit)
1700 |> Map.put("id", fetch_suggestion_id(x))
1701 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1702 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1708 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1715 defp fetch_suggestion_id(attrs) do
1716 case User.get_or_fetch(attrs["acct"]) do
1717 {:ok, %User{id: id}} -> id
1722 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1723 with %Activity{} = activity <- Activity.get_by_id(status_id),
1724 true <- Visibility.visible_for_user?(activity, user) do
1728 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1738 def reports(%{assigns: %{user: user}} = conn, params) do
1739 case CommonAPI.report(user, params) do
1742 |> put_view(ReportView)
1743 |> try_render("report.json", %{activity: activity})
1747 |> put_status(:bad_request)
1748 |> json(%{error: err})
1752 def account_register(
1753 %{assigns: %{app: app}} = conn,
1754 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1762 "captcha_answer_data",
1766 |> Map.put("nickname", nickname)
1767 |> Map.put("fullname", params["fullname"] || nickname)
1768 |> Map.put("bio", params["bio"] || "")
1769 |> Map.put("confirm", params["password"])
1771 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1772 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1774 token_type: "Bearer",
1775 access_token: token.token,
1777 created_at: Token.Utils.format_created_at(token)
1782 |> put_status(:bad_request)
1787 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1788 render_error(conn, :bad_request, "Missing parameters")
1791 def account_register(conn, _) do
1792 render_error(conn, :forbidden, "Invalid credentials")
1795 def conversations(%{assigns: %{user: user}} = conn, params) do
1796 participations = Participation.for_user_with_last_activity_id(user, params)
1799 Enum.map(participations, fn participation ->
1800 ConversationView.render("participation.json", %{participation: participation, user: user})
1804 |> add_link_headers(:conversations, participations)
1805 |> json(conversations)
1808 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1809 with %Participation{} = participation <-
1810 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1811 {:ok, participation} <- Participation.mark_as_read(participation) do
1812 participation_view =
1813 ConversationView.render("participation.json", %{participation: participation, user: user})
1816 |> json(participation_view)
1820 def password_reset(conn, params) do
1821 nickname_or_email = params["email"] || params["nickname"]
1823 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1825 |> put_status(:no_content)
1828 {:error, "unknown user"} ->
1829 send_resp(conn, :not_found, "")
1832 send_resp(conn, :bad_request, "")
1836 def account_confirmation_resend(conn, params) do
1837 nickname_or_email = params["email"] || params["nickname"]
1839 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1840 {:ok, _} <- User.try_send_confirmation_email(user) do
1842 |> json_response(:no_content, "")
1846 def try_render(conn, target, params)
1847 when is_binary(target) do
1848 case render(conn, target, params) do
1849 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1854 def try_render(conn, _, _) do
1855 render_error(conn, :not_implemented, "Can't display this activity")
1858 defp present?(nil), do: false
1859 defp present?(false), do: false
1860 defp present?(_), do: true