1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
6 use Pleroma.Web, :controller
11 alias Pleroma.Conversation.Participation
13 alias Pleroma.Formatter
15 alias Pleroma.Notification
17 alias Pleroma.Pagination
18 alias Pleroma.Plugs.RateLimiter
20 alias Pleroma.ScheduledActivity
24 alias Pleroma.Web.ActivityPub.ActivityPub
25 alias Pleroma.Web.ActivityPub.Visibility
26 alias Pleroma.Web.CommonAPI
27 alias Pleroma.Web.MastodonAPI.AccountView
28 alias Pleroma.Web.MastodonAPI.AppView
29 alias Pleroma.Web.MastodonAPI.ConversationView
30 alias Pleroma.Web.MastodonAPI.FilterView
31 alias Pleroma.Web.MastodonAPI.ListView
32 alias Pleroma.Web.MastodonAPI.MastodonAPI
33 alias Pleroma.Web.MastodonAPI.MastodonView
34 alias Pleroma.Web.MastodonAPI.NotificationView
35 alias Pleroma.Web.MastodonAPI.ReportView
36 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
37 alias Pleroma.Web.MastodonAPI.StatusView
38 alias Pleroma.Web.MediaProxy
39 alias Pleroma.Web.OAuth.App
40 alias Pleroma.Web.OAuth.Authorization
41 alias Pleroma.Web.OAuth.Scopes
42 alias Pleroma.Web.OAuth.Token
43 alias Pleroma.Web.TwitterAPI.TwitterAPI
45 alias Pleroma.Web.ControllerHelper
50 @rate_limited_relations_actions ~w(follow unfollow)a
52 @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
53 post_status delete_status)a
57 {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
58 when action in ~w(reblog_status unreblog_status)a
63 {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
64 when action in ~w(fav_status unfav_status)a
69 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
72 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
73 plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
74 plug(RateLimiter, :app_account_creation when action == :account_register)
75 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
76 plug(RateLimiter, :password_reset when action == :password_reset)
78 @local_mastodon_name "Mastodon-Local"
80 action_fallback(:errors)
82 def create_app(conn, params) do
83 scopes = Scopes.fetch_scopes(params, ["read"])
87 |> Map.drop(["scope", "scopes"])
88 |> Map.put("scopes", scopes)
90 with cs <- App.register_changeset(%App{}, app_attrs),
91 false <- cs.changes[:client_name] == @local_mastodon_name,
92 {:ok, app} <- Repo.insert(cs) do
95 |> render("show.json", %{app: app})
104 value_function \\ fn x -> {:ok, x} end
106 if Map.has_key?(params, params_field) do
107 case value_function.(params[params_field]) do
108 {:ok, new_value} -> Map.put(map, map_field, new_value)
116 def update_credentials(%{assigns: %{user: user}} = conn, params) do
121 |> add_if_present(params, "display_name", :name)
122 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
123 |> add_if_present(params, "avatar", :avatar, fn value ->
124 with %Plug.Upload{} <- value,
125 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
132 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
135 ((user.info.emoji || []) ++ Formatter.get_emoji_map(emojis_text))
146 :skip_thread_containment
148 |> Enum.reduce(%{}, fn key, acc ->
149 add_if_present(acc, params, to_string(key), key, fn value ->
150 {:ok, ControllerHelper.truthy_param?(value)}
153 |> add_if_present(params, "default_scope", :default_scope)
154 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
155 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
157 |> add_if_present(params, "header", :banner, fn value ->
158 with %Plug.Upload{} <- value,
159 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
165 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
166 with %Plug.Upload{} <- value,
167 {:ok, object} <- ActivityPub.upload(value, type: :background) do
173 |> Map.put(:emoji, user_info_emojis)
175 info_cng = User.Info.profile_update(user.info, info_params)
177 with changeset <- User.update_changeset(user, user_params),
178 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
179 {:ok, user} <- User.update_and_set_cache(changeset) do
180 if original_user != user do
181 CommonAPI.update(user)
186 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
189 _e -> render_error(conn, :forbidden, "Invalid request")
193 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
194 change = Changeset.change(user, %{avatar: nil})
195 {:ok, user} = User.update_and_set_cache(change)
196 CommonAPI.update(user)
198 json(conn, %{url: nil})
201 def update_avatar(%{assigns: %{user: user}} = conn, params) do
202 {:ok, object} = ActivityPub.upload(params, type: :avatar)
203 change = Changeset.change(user, %{avatar: object.data})
204 {:ok, user} = User.update_and_set_cache(change)
205 CommonAPI.update(user)
206 %{"url" => [%{"href" => href} | _]} = object.data
208 json(conn, %{url: href})
211 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
212 with new_info <- %{"banner" => %{}},
213 info_cng <- User.Info.profile_update(user.info, new_info),
214 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
215 {:ok, user} <- User.update_and_set_cache(changeset) do
216 CommonAPI.update(user)
218 json(conn, %{url: nil})
222 def update_banner(%{assigns: %{user: user}} = conn, params) do
223 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
224 new_info <- %{"banner" => object.data},
225 info_cng <- User.Info.profile_update(user.info, new_info),
226 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
227 {:ok, user} <- User.update_and_set_cache(changeset) do
228 CommonAPI.update(user)
229 %{"url" => [%{"href" => href} | _]} = object.data
231 json(conn, %{url: href})
235 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
236 with new_info <- %{"background" => %{}},
237 info_cng <- User.Info.profile_update(user.info, new_info),
238 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
239 {:ok, _user} <- User.update_and_set_cache(changeset) do
240 json(conn, %{url: nil})
244 def update_background(%{assigns: %{user: user}} = conn, params) do
245 with {:ok, object} <- ActivityPub.upload(params, type: :background),
246 new_info <- %{"background" => object.data},
247 info_cng <- User.Info.profile_update(user.info, new_info),
248 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
249 {:ok, _user} <- User.update_and_set_cache(changeset) do
250 %{"url" => [%{"href" => href} | _]} = object.data
252 json(conn, %{url: href})
256 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
257 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
260 AccountView.render("account.json", %{
263 with_pleroma_settings: true,
264 with_chat_token: chat_token
270 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
271 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
274 |> render("short.json", %{app: app})
278 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
279 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
280 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
281 account = AccountView.render("account.json", %{user: user, for: for_user})
284 _e -> render_error(conn, :not_found, "Can't find user")
288 @mastodon_api_level "2.7.2"
290 def masto_instance(conn, _params) do
291 instance = Config.get(:instance)
295 title: Keyword.get(instance, :name),
296 description: Keyword.get(instance, :description),
297 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
298 email: Keyword.get(instance, :email),
300 streaming_api: Pleroma.Web.Endpoint.websocket_url()
302 stats: Stats.get_stats(),
303 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
305 registrations: Pleroma.Config.get([:instance, :registrations_open]),
306 # Extra (not present in Mastodon):
307 max_toot_chars: Keyword.get(instance, :limit),
308 poll_limits: Keyword.get(instance, :poll_limits)
314 def peers(conn, _params) do
315 json(conn, Stats.get_peers())
318 defp mastodonized_emoji do
319 Pleroma.Emoji.get_all()
320 |> Enum.map(fn {shortcode, relative_url, tags} ->
321 url = to_string(URI.merge(Web.base_url(), relative_url))
324 "shortcode" => shortcode,
326 "visible_in_picker" => true,
329 # Assuming that a comma is authorized in the category name
330 "category" => (tags -- ["Custom"]) |> Enum.join(",")
335 def custom_emojis(conn, _params) do
336 mastodon_emoji = mastodonized_emoji()
337 json(conn, mastodon_emoji)
340 defp add_link_headers(conn, method, activities, param \\ nil, params \\ %{}) do
343 |> Map.drop(["since_id", "max_id", "min_id"])
346 last = List.last(activities)
353 |> Map.get("limit", "20")
354 |> String.to_integer()
357 if length(activities) <= limit do
363 |> Enum.at(limit * -1)
367 {next_url, prev_url} =
371 Pleroma.Web.Endpoint,
374 Map.merge(params, %{max_id: max_id})
377 Pleroma.Web.Endpoint,
380 Map.merge(params, %{min_id: min_id})
386 Pleroma.Web.Endpoint,
388 Map.merge(params, %{max_id: max_id})
391 Pleroma.Web.Endpoint,
393 Map.merge(params, %{min_id: min_id})
399 |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
405 def home_timeline(%{assigns: %{user: user}} = conn, params) do
408 |> Map.put("type", ["Create", "Announce"])
409 |> Map.put("blocking_user", user)
410 |> Map.put("muting_user", user)
411 |> Map.put("user", user)
414 [user.ap_id | user.following]
415 |> ActivityPub.fetch_activities(params)
419 |> add_link_headers(:home_timeline, activities)
420 |> put_view(StatusView)
421 |> render("index.json", %{activities: activities, for: user, as: :activity})
424 def public_timeline(%{assigns: %{user: user}} = conn, params) do
425 local_only = params["local"] in [true, "True", "true", "1"]
429 |> Map.put("type", ["Create", "Announce"])
430 |> Map.put("local_only", local_only)
431 |> Map.put("blocking_user", user)
432 |> Map.put("muting_user", user)
433 |> ActivityPub.fetch_public_activities()
437 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
438 |> put_view(StatusView)
439 |> render("index.json", %{activities: activities, for: user, as: :activity})
442 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
443 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"]) do
446 |> Map.put("tag", params["tagged"])
448 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
451 |> add_link_headers(:user_statuses, activities, params["id"])
452 |> put_view(StatusView)
453 |> render("index.json", %{
454 activities: activities,
461 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
464 |> Map.put("type", "Create")
465 |> Map.put("blocking_user", user)
466 |> Map.put("user", user)
467 |> Map.put(:visibility, "direct")
471 |> ActivityPub.fetch_activities_query(params)
472 |> Pagination.fetch_paginated(params)
475 |> add_link_headers(:dm_timeline, activities)
476 |> put_view(StatusView)
477 |> render("index.json", %{activities: activities, for: user, as: :activity})
480 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
481 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
482 true <- Visibility.visible_for_user?(activity, user) do
484 |> put_view(StatusView)
485 |> try_render("status.json", %{activity: activity, for: user})
489 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
490 with %Activity{} = activity <- Activity.get_by_id(id),
492 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
493 "blocking_user" => user,
497 activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
499 activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
500 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
506 activities: grouped_activities[true] || [],
510 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
515 activities: grouped_activities[false] || [],
519 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
526 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
527 with %Object{} = object <- Object.get_by_id(id),
528 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
529 true <- Visibility.visible_for_user?(activity, user) do
531 |> put_view(StatusView)
532 |> try_render("poll.json", %{object: object, for: user})
534 nil -> render_error(conn, :not_found, "Record not found")
535 false -> render_error(conn, :not_found, "Record not found")
539 defp get_cached_vote_or_vote(user, object, choices) do
540 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
543 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
544 case CommonAPI.vote(user, object, choices) do
545 {:error, _message} = res -> {:ignore, res}
546 res -> {:commit, res}
553 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
554 with %Object{} = object <- Object.get_by_id(id),
555 true <- object.data["type"] == "Question",
556 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
557 true <- Visibility.visible_for_user?(activity, user),
558 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
560 |> put_view(StatusView)
561 |> try_render("poll.json", %{object: object, for: user})
564 render_error(conn, :not_found, "Record not found")
567 render_error(conn, :not_found, "Record not found")
571 |> put_status(:unprocessable_entity)
572 |> json(%{error: message})
576 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
577 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
579 |> add_link_headers(:scheduled_statuses, scheduled_activities)
580 |> put_view(ScheduledActivityView)
581 |> render("index.json", %{scheduled_activities: scheduled_activities})
585 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
586 with %ScheduledActivity{} = scheduled_activity <-
587 ScheduledActivity.get(user, scheduled_activity_id) do
589 |> put_view(ScheduledActivityView)
590 |> render("show.json", %{scheduled_activity: scheduled_activity})
592 _ -> {:error, :not_found}
596 def update_scheduled_status(
597 %{assigns: %{user: user}} = conn,
598 %{"id" => scheduled_activity_id} = params
600 with %ScheduledActivity{} = scheduled_activity <-
601 ScheduledActivity.get(user, scheduled_activity_id),
602 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
604 |> put_view(ScheduledActivityView)
605 |> render("show.json", %{scheduled_activity: scheduled_activity})
607 nil -> {:error, :not_found}
612 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
613 with %ScheduledActivity{} = scheduled_activity <-
614 ScheduledActivity.get(user, scheduled_activity_id),
615 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
617 |> put_view(ScheduledActivityView)
618 |> render("show.json", %{scheduled_activity: scheduled_activity})
620 nil -> {:error, :not_found}
625 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
628 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
630 scheduled_at = params["scheduled_at"]
632 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
633 with {:ok, scheduled_activity} <-
634 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
636 |> put_view(ScheduledActivityView)
637 |> render("show.json", %{scheduled_activity: scheduled_activity})
640 params = Map.drop(params, ["scheduled_at"])
642 case CommonAPI.post(user, params) do
645 |> put_status(:unprocessable_entity)
646 |> json(%{error: message})
650 |> put_view(StatusView)
651 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
656 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
657 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
660 _e -> render_error(conn, :forbidden, "Can't delete this post")
664 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
665 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
666 %Activity{} = announce <- Activity.normalize(announce.data) do
668 |> put_view(StatusView)
669 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
673 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
674 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
675 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
677 |> put_view(StatusView)
678 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
682 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
683 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
684 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
686 |> put_view(StatusView)
687 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
691 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
692 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
693 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
695 |> put_view(StatusView)
696 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
700 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
701 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
703 |> put_view(StatusView)
704 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
708 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
709 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
711 |> put_view(StatusView)
712 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
716 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
717 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
718 %User{} = user <- User.get_cached_by_nickname(user.nickname),
719 true <- Visibility.visible_for_user?(activity, user),
720 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
722 |> put_view(StatusView)
723 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
727 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
728 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
729 %User{} = user <- User.get_cached_by_nickname(user.nickname),
730 true <- Visibility.visible_for_user?(activity, user),
731 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
733 |> put_view(StatusView)
734 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
738 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
739 activity = Activity.get_by_id(id)
741 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
743 |> put_view(StatusView)
744 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
748 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
749 activity = Activity.get_by_id(id)
751 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
753 |> put_view(StatusView)
754 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
758 def notifications(%{assigns: %{user: user}} = conn, params) do
759 notifications = MastodonAPI.get_notifications(user, params)
762 |> add_link_headers(:notifications, notifications)
763 |> put_view(NotificationView)
764 |> render("index.json", %{notifications: notifications, for: user})
767 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
768 with {:ok, notification} <- Notification.get(user, id) do
770 |> put_view(NotificationView)
771 |> render("show.json", %{notification: notification, for: user})
775 |> put_status(:forbidden)
776 |> json(%{"error" => reason})
780 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
781 Notification.clear(user)
785 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
786 with {:ok, _notif} <- Notification.dismiss(user, id) do
791 |> put_status(:forbidden)
792 |> json(%{"error" => reason})
796 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
797 Notification.destroy_multiple(user, ids)
801 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
803 q = from(u in User, where: u.id in ^id)
804 targets = Repo.all(q)
807 |> put_view(AccountView)
808 |> render("relationships.json", %{user: user, targets: targets})
811 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
812 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
814 def update_media(%{assigns: %{user: user}} = conn, data) do
815 with %Object{} = object <- Repo.get(Object, data["id"]),
816 true <- Object.authorize_mutation(object, user),
817 true <- is_binary(data["description"]),
818 description <- data["description"] do
819 new_data = %{object.data | "name" => description}
823 |> Object.change(%{data: new_data})
826 attachment_data = Map.put(new_data, "id", object.id)
829 |> put_view(StatusView)
830 |> render("attachment.json", %{attachment: attachment_data})
834 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
835 with {:ok, object} <-
838 actor: User.ap_id(user),
839 description: Map.get(data, "description")
841 attachment_data = Map.put(object.data, "id", object.id)
844 |> put_view(StatusView)
845 |> render("attachment.json", %{attachment: attachment_data})
849 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
850 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
851 %{} = attachment_data <- Map.put(object.data, "id", object.id),
852 %{type: type} = rendered <-
853 StatusView.render("attachment.json", %{attachment: attachment_data}) do
854 # Reject if not an image
855 if type == "image" do
857 # Save to the user's info
858 info_changeset = User.Info.mascot_update(user.info, rendered)
862 |> Ecto.Changeset.change()
863 |> Ecto.Changeset.put_embed(:info, info_changeset)
865 {:ok, _user} = User.update_and_set_cache(user_changeset)
870 render_error(conn, :unsupported_media_type, "mascots can only be images")
875 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
876 mascot = User.get_mascot(user)
882 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
883 with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id),
884 %Object{data: %{"likes" => likes}} <- Object.normalize(object) do
885 q = from(u in User, where: u.ap_id in ^likes)
888 users = if is_nil(user) do
891 Enum.filter(users, &(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{data: %{"object" => object}} <- Activity.get_by_id(id),
904 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
905 q = from(u in User, where: u.ap_id in ^announces)
908 users = if is_nil(user) do
911 Enum.filter(users, &(not User.blocks?(user, &1)))
915 |> put_view(AccountView)
916 |> render("accounts.json", %{for: user, users: users, as: :user})
922 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
923 local_only = params["local"] in [true, "True", "true", "1"]
926 [params["tag"], params["any"]]
930 |> Enum.map(&String.downcase(&1))
935 |> Enum.map(&String.downcase(&1))
940 |> Enum.map(&String.downcase(&1))
944 |> Map.put("type", "Create")
945 |> Map.put("local_only", local_only)
946 |> Map.put("blocking_user", user)
947 |> Map.put("muting_user", user)
948 |> Map.put("tag", tags)
949 |> Map.put("tag_all", tag_all)
950 |> Map.put("tag_reject", tag_reject)
951 |> ActivityPub.fetch_public_activities()
955 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
956 |> put_view(StatusView)
957 |> render("index.json", %{activities: activities, for: user, as: :activity})
960 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
961 with %User{} = user <- User.get_cached_by_id(id),
962 followers <- MastodonAPI.get_followers(user, params) do
965 for_user && user.id == for_user.id -> followers
966 user.info.hide_followers -> []
971 |> add_link_headers(:followers, followers, user)
972 |> put_view(AccountView)
973 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
977 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
978 with %User{} = user <- User.get_cached_by_id(id),
979 followers <- MastodonAPI.get_friends(user, params) do
982 for_user && user.id == for_user.id -> followers
983 user.info.hide_follows -> []
988 |> add_link_headers(:following, followers, user)
989 |> put_view(AccountView)
990 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
994 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
995 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
997 |> put_view(AccountView)
998 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
1002 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1003 with %User{} = follower <- User.get_cached_by_id(id),
1004 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
1006 |> put_view(AccountView)
1007 |> render("relationship.json", %{user: followed, target: follower})
1009 {:error, message} ->
1011 |> put_status(:forbidden)
1012 |> json(%{error: message})
1016 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1017 with %User{} = follower <- User.get_cached_by_id(id),
1018 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
1020 |> put_view(AccountView)
1021 |> render("relationship.json", %{user: followed, target: follower})
1023 {:error, message} ->
1025 |> put_status(:forbidden)
1026 |> json(%{error: message})
1030 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1031 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1032 {_, true} <- {:followed, follower.id != followed.id},
1033 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
1035 |> put_view(AccountView)
1036 |> render("relationship.json", %{user: follower, target: followed})
1039 {:error, :not_found}
1041 {:error, message} ->
1043 |> put_status(:forbidden)
1044 |> json(%{error: message})
1048 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1049 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1050 {_, true} <- {:followed, follower.id != followed.id},
1051 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1053 |> put_view(AccountView)
1054 |> render("account.json", %{user: followed, for: follower})
1057 {:error, :not_found}
1059 {:error, message} ->
1061 |> put_status(:forbidden)
1062 |> json(%{error: message})
1066 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1067 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1068 {_, true} <- {:followed, follower.id != followed.id},
1069 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1071 |> put_view(AccountView)
1072 |> render("relationship.json", %{user: follower, target: followed})
1075 {:error, :not_found}
1082 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1084 if Map.has_key?(params, "notifications"),
1085 do: params["notifications"] in [true, "True", "true", "1"],
1088 with %User{} = muted <- User.get_cached_by_id(id),
1089 {:ok, muter} <- User.mute(muter, muted, notifications) do
1091 |> put_view(AccountView)
1092 |> render("relationship.json", %{user: muter, target: muted})
1094 {:error, message} ->
1096 |> put_status(:forbidden)
1097 |> json(%{error: message})
1101 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1102 with %User{} = muted <- User.get_cached_by_id(id),
1103 {:ok, muter} <- User.unmute(muter, muted) do
1105 |> put_view(AccountView)
1106 |> render("relationship.json", %{user: muter, target: muted})
1108 {:error, message} ->
1110 |> put_status(:forbidden)
1111 |> json(%{error: message})
1115 def mutes(%{assigns: %{user: user}} = conn, _) do
1116 with muted_accounts <- User.muted_users(user) do
1117 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1122 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1123 with %User{} = blocked <- User.get_cached_by_id(id),
1124 {:ok, blocker} <- User.block(blocker, blocked),
1125 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1127 |> put_view(AccountView)
1128 |> render("relationship.json", %{user: blocker, target: blocked})
1130 {:error, message} ->
1132 |> put_status(:forbidden)
1133 |> json(%{error: message})
1137 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1138 with %User{} = blocked <- User.get_cached_by_id(id),
1139 {:ok, blocker} <- User.unblock(blocker, blocked),
1140 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1142 |> put_view(AccountView)
1143 |> render("relationship.json", %{user: blocker, target: blocked})
1145 {:error, message} ->
1147 |> put_status(:forbidden)
1148 |> json(%{error: message})
1152 def blocks(%{assigns: %{user: user}} = conn, _) do
1153 with blocked_accounts <- User.blocked_users(user) do
1154 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1159 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1160 json(conn, info.domain_blocks || [])
1163 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1164 User.block_domain(blocker, domain)
1168 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1169 User.unblock_domain(blocker, domain)
1173 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1174 with %User{} = subscription_target <- User.get_cached_by_id(id),
1175 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1177 |> put_view(AccountView)
1178 |> render("relationship.json", %{user: user, target: subscription_target})
1180 {:error, message} ->
1182 |> put_status(:forbidden)
1183 |> json(%{error: message})
1187 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1188 with %User{} = subscription_target <- User.get_cached_by_id(id),
1189 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1191 |> put_view(AccountView)
1192 |> render("relationship.json", %{user: user, target: subscription_target})
1194 {:error, message} ->
1196 |> put_status(:forbidden)
1197 |> json(%{error: message})
1201 def favourites(%{assigns: %{user: user}} = conn, params) do
1204 |> Map.put("type", "Create")
1205 |> Map.put("favorited_by", user.ap_id)
1206 |> Map.put("blocking_user", user)
1209 ActivityPub.fetch_activities([], params)
1213 |> add_link_headers(:favourites, activities)
1214 |> put_view(StatusView)
1215 |> render("index.json", %{activities: activities, for: user, as: :activity})
1218 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1219 with %User{} = user <- User.get_by_id(id),
1220 false <- user.info.hide_favorites do
1223 |> Map.put("type", "Create")
1224 |> Map.put("favorited_by", user.ap_id)
1225 |> Map.put("blocking_user", for_user)
1229 ["https://www.w3.org/ns/activitystreams#Public"] ++
1230 [for_user.ap_id | for_user.following]
1232 ["https://www.w3.org/ns/activitystreams#Public"]
1237 |> ActivityPub.fetch_activities(params)
1241 |> add_link_headers(:favourites, activities)
1242 |> put_view(StatusView)
1243 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1245 nil -> {:error, :not_found}
1246 true -> render_error(conn, :forbidden, "Can't get favorites")
1250 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1251 user = User.get_cached_by_id(user.id)
1254 Bookmark.for_user_query(user.id)
1255 |> Pagination.fetch_paginated(params)
1259 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1262 |> add_link_headers(:bookmarks, bookmarks)
1263 |> put_view(StatusView)
1264 |> render("index.json", %{activities: activities, for: user, as: :activity})
1267 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1268 lists = Pleroma.List.for_user(user, opts)
1269 res = ListView.render("lists.json", lists: lists)
1273 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1274 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1275 res = ListView.render("list.json", list: list)
1278 _e -> render_error(conn, :not_found, "Record not found")
1282 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1283 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1284 res = ListView.render("lists.json", lists: lists)
1288 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1289 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1290 {:ok, _list} <- Pleroma.List.delete(list) do
1294 json(conn, dgettext("errors", "error"))
1298 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1299 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1300 res = ListView.render("list.json", list: list)
1305 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1307 |> Enum.each(fn account_id ->
1308 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1309 %User{} = followed <- User.get_cached_by_id(account_id) do
1310 Pleroma.List.follow(list, followed)
1317 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1319 |> Enum.each(fn account_id ->
1320 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1321 %User{} = followed <- User.get_cached_by_id(account_id) do
1322 Pleroma.List.unfollow(list, followed)
1329 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1330 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1331 {:ok, users} = Pleroma.List.get_following(list) do
1333 |> put_view(AccountView)
1334 |> render("accounts.json", %{for: user, users: users, as: :user})
1338 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1339 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1340 {:ok, list} <- Pleroma.List.rename(list, title) do
1341 res = ListView.render("list.json", list: list)
1345 json(conn, dgettext("errors", "error"))
1349 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1350 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1353 |> Map.put("type", "Create")
1354 |> Map.put("blocking_user", user)
1355 |> Map.put("muting_user", user)
1357 # we must filter the following list for the user to avoid leaking statuses the user
1358 # does not actually have permission to see (for more info, peruse security issue #270).
1361 |> Enum.filter(fn x -> x in user.following end)
1362 |> ActivityPub.fetch_activities_bounded(following, params)
1366 |> put_view(StatusView)
1367 |> render("index.json", %{activities: activities, for: user, as: :activity})
1369 _e -> render_error(conn, :forbidden, "Error.")
1373 def index(%{assigns: %{user: user}} = conn, _params) do
1374 token = get_session(conn, :oauth_token)
1377 mastodon_emoji = mastodonized_emoji()
1379 limit = Config.get([:instance, :limit])
1382 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1387 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1388 access_token: token,
1390 domain: Pleroma.Web.Endpoint.host(),
1393 unfollow_modal: false,
1396 auto_play_gif: false,
1397 display_sensitive_media: false,
1398 reduce_motion: false,
1399 max_toot_chars: limit,
1400 mascot: User.get_mascot(user)["url"]
1402 poll_limits: Config.get([:instance, :poll_limits]),
1404 delete_others_notice: present?(user.info.is_moderator),
1405 admin: present?(user.info.is_admin)
1409 default_privacy: user.info.default_scope,
1410 default_sensitive: false,
1411 allow_content_types: Config.get([:instance, :allowed_post_formats])
1413 media_attachments: %{
1414 accept_content_types: [
1430 user.info.settings ||
1460 push_subscription: nil,
1462 custom_emojis: mastodon_emoji,
1468 |> put_layout(false)
1469 |> put_view(MastodonView)
1470 |> render("index.html", %{initial_state: initial_state})
1473 |> put_session(:return_to, conn.request_path)
1474 |> redirect(to: "/web/login")
1478 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1479 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1481 with changeset <- Ecto.Changeset.change(user),
1482 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1483 {:ok, _user} <- User.update_and_set_cache(changeset) do
1488 |> put_status(:internal_server_error)
1489 |> json(%{error: inspect(e)})
1493 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1494 redirect(conn, to: local_mastodon_root_path(conn))
1497 @doc "Local Mastodon FE login init action"
1498 def login(conn, %{"code" => auth_token}) do
1499 with {:ok, app} <- get_or_make_app(),
1500 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1501 {:ok, token} <- Token.exchange_token(app, auth) do
1503 |> put_session(:oauth_token, token.token)
1504 |> redirect(to: local_mastodon_root_path(conn))
1508 @doc "Local Mastodon FE callback action"
1509 def login(conn, _) do
1510 with {:ok, app} <- get_or_make_app() do
1515 response_type: "code",
1516 client_id: app.client_id,
1518 scope: Enum.join(app.scopes, " ")
1521 redirect(conn, to: path)
1525 defp local_mastodon_root_path(conn) do
1526 case get_session(conn, :return_to) do
1528 mastodon_api_path(conn, :index, ["getting-started"])
1531 delete_session(conn, :return_to)
1536 defp get_or_make_app do
1537 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1538 scopes = ["read", "write", "follow", "push"]
1540 with %App{} = app <- Repo.get_by(App, find_attrs) do
1542 if app.scopes == scopes do
1546 |> Ecto.Changeset.change(%{scopes: scopes})
1554 App.register_changeset(
1556 Map.put(find_attrs, :scopes, scopes)
1563 def logout(conn, _) do
1566 |> redirect(to: "/")
1569 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1570 Logger.debug("Unimplemented, returning unmodified relationship")
1572 with %User{} = target <- User.get_cached_by_id(id) do
1574 |> put_view(AccountView)
1575 |> render("relationship.json", %{user: user, target: target})
1579 def empty_array(conn, _) do
1580 Logger.debug("Unimplemented, returning an empty array")
1584 def empty_object(conn, _) do
1585 Logger.debug("Unimplemented, returning an empty object")
1589 def get_filters(%{assigns: %{user: user}} = conn, _) do
1590 filters = Filter.get_filters(user)
1591 res = FilterView.render("filters.json", filters: filters)
1596 %{assigns: %{user: user}} = conn,
1597 %{"phrase" => phrase, "context" => context} = params
1603 hide: Map.get(params, "irreversible", false),
1604 whole_word: Map.get(params, "boolean", true)
1608 {:ok, response} = Filter.create(query)
1609 res = FilterView.render("filter.json", filter: response)
1613 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1614 filter = Filter.get(filter_id, user)
1615 res = FilterView.render("filter.json", filter: filter)
1620 %{assigns: %{user: user}} = conn,
1621 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1625 filter_id: filter_id,
1628 hide: Map.get(params, "irreversible", nil),
1629 whole_word: Map.get(params, "boolean", true)
1633 {:ok, response} = Filter.update(query)
1634 res = FilterView.render("filter.json", filter: response)
1638 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1641 filter_id: filter_id
1644 {:ok, _} = Filter.delete(query)
1650 def errors(conn, {:error, %Changeset{} = changeset}) do
1653 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1654 |> Enum.map_join(", ", fn {_k, v} -> v end)
1657 |> put_status(:unprocessable_entity)
1658 |> json(%{error: error_message})
1661 def errors(conn, {:error, :not_found}) do
1662 render_error(conn, :not_found, "Record not found")
1665 def errors(conn, {:error, error_message}) do
1667 |> put_status(:bad_request)
1668 |> json(%{error: error_message})
1671 def errors(conn, _) do
1673 |> put_status(:internal_server_error)
1674 |> json(dgettext("errors", "Something went wrong"))
1677 def suggestions(%{assigns: %{user: user}} = conn, _) do
1678 suggestions = Config.get(:suggestions)
1680 if Keyword.get(suggestions, :enabled, false) do
1681 api = Keyword.get(suggestions, :third_party_engine, "")
1682 timeout = Keyword.get(suggestions, :timeout, 5000)
1683 limit = Keyword.get(suggestions, :limit, 23)
1685 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1687 user = user.nickname
1691 |> String.replace("{{host}}", host)
1692 |> String.replace("{{user}}", user)
1694 with {:ok, %{status: 200, body: body}} <-
1699 recv_timeout: timeout,
1703 {:ok, data} <- Jason.decode(body) do
1706 |> Enum.slice(0, limit)
1711 case User.get_or_fetch(x["acct"]) do
1712 {:ok, %User{id: id}} -> id
1718 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1721 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1727 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1734 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1735 with %Activity{} = activity <- Activity.get_by_id(status_id),
1736 true <- Visibility.visible_for_user?(activity, user) do
1740 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1750 def reports(%{assigns: %{user: user}} = conn, params) do
1751 case CommonAPI.report(user, params) do
1754 |> put_view(ReportView)
1755 |> try_render("report.json", %{activity: activity})
1759 |> put_status(:bad_request)
1760 |> json(%{error: err})
1764 def account_register(
1765 %{assigns: %{app: app}} = conn,
1766 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1774 "captcha_answer_data",
1778 |> Map.put("nickname", nickname)
1779 |> Map.put("fullname", params["fullname"] || nickname)
1780 |> Map.put("bio", params["bio"] || "")
1781 |> Map.put("confirm", params["password"])
1783 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1784 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1786 token_type: "Bearer",
1787 access_token: token.token,
1789 created_at: Token.Utils.format_created_at(token)
1794 |> put_status(:bad_request)
1799 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1800 render_error(conn, :bad_request, "Missing parameters")
1803 def account_register(conn, _) do
1804 render_error(conn, :forbidden, "Invalid credentials")
1807 def conversations(%{assigns: %{user: user}} = conn, params) do
1808 participations = Participation.for_user_with_last_activity_id(user, params)
1811 Enum.map(participations, fn participation ->
1812 ConversationView.render("participation.json", %{participation: participation, user: user})
1816 |> add_link_headers(:conversations, participations)
1817 |> json(conversations)
1820 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1821 with %Participation{} = participation <-
1822 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1823 {:ok, participation} <- Participation.mark_as_read(participation) do
1824 participation_view =
1825 ConversationView.render("participation.json", %{participation: participation, user: user})
1828 |> json(participation_view)
1832 def password_reset(conn, params) do
1833 nickname_or_email = params["email"] || params["nickname"]
1835 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1837 |> put_status(:no_content)
1840 {:error, "unknown user"} ->
1841 send_resp(conn, :not_found, "")
1844 send_resp(conn, :bad_request, "")
1848 def try_render(conn, target, params)
1849 when is_binary(target) do
1850 case render(conn, target, params) do
1851 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1856 def try_render(conn, _, _) do
1857 render_error(conn, :not_implemented, "Can't display this activity")
1860 defp present?(nil), do: false
1861 defp present?(false), do: false
1862 defp present?(_), do: true