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)
893 Enum.filter(users, &(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)
915 Enum.filter(users, &(not User.blocks?(user, &1)))
919 |> put_view(AccountView)
920 |> render("accounts.json", %{for: user, users: users, as: :user})
926 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
927 local_only = params["local"] in [true, "True", "true", "1"]
930 [params["tag"], params["any"]]
934 |> Enum.map(&String.downcase(&1))
939 |> Enum.map(&String.downcase(&1))
944 |> Enum.map(&String.downcase(&1))
948 |> Map.put("type", "Create")
949 |> Map.put("local_only", local_only)
950 |> Map.put("blocking_user", user)
951 |> Map.put("muting_user", user)
952 |> Map.put("tag", tags)
953 |> Map.put("tag_all", tag_all)
954 |> Map.put("tag_reject", tag_reject)
955 |> ActivityPub.fetch_public_activities()
959 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
960 |> put_view(StatusView)
961 |> render("index.json", %{activities: activities, for: user, as: :activity})
964 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
965 with %User{} = user <- User.get_cached_by_id(id),
966 followers <- MastodonAPI.get_followers(user, params) do
969 for_user && user.id == for_user.id -> followers
970 user.info.hide_followers -> []
975 |> add_link_headers(:followers, followers, user)
976 |> put_view(AccountView)
977 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
981 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
982 with %User{} = user <- User.get_cached_by_id(id),
983 followers <- MastodonAPI.get_friends(user, params) do
986 for_user && user.id == for_user.id -> followers
987 user.info.hide_follows -> []
992 |> add_link_headers(:following, followers, user)
993 |> put_view(AccountView)
994 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
998 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
999 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
1001 |> put_view(AccountView)
1002 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
1006 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1007 with %User{} = follower <- User.get_cached_by_id(id),
1008 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
1010 |> put_view(AccountView)
1011 |> render("relationship.json", %{user: followed, target: follower})
1013 {:error, message} ->
1015 |> put_status(:forbidden)
1016 |> json(%{error: message})
1020 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1021 with %User{} = follower <- User.get_cached_by_id(id),
1022 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
1024 |> put_view(AccountView)
1025 |> render("relationship.json", %{user: followed, target: follower})
1027 {:error, message} ->
1029 |> put_status(:forbidden)
1030 |> json(%{error: message})
1034 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1035 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1036 {_, true} <- {:followed, follower.id != followed.id},
1037 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
1039 |> put_view(AccountView)
1040 |> render("relationship.json", %{user: follower, target: followed})
1043 {:error, :not_found}
1045 {:error, message} ->
1047 |> put_status(:forbidden)
1048 |> json(%{error: message})
1052 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1053 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1054 {_, true} <- {:followed, follower.id != followed.id},
1055 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1057 |> put_view(AccountView)
1058 |> render("account.json", %{user: followed, for: follower})
1061 {:error, :not_found}
1063 {:error, message} ->
1065 |> put_status(:forbidden)
1066 |> json(%{error: message})
1070 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1071 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1072 {_, true} <- {:followed, follower.id != followed.id},
1073 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1075 |> put_view(AccountView)
1076 |> render("relationship.json", %{user: follower, target: followed})
1079 {:error, :not_found}
1086 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1088 if Map.has_key?(params, "notifications"),
1089 do: params["notifications"] in [true, "True", "true", "1"],
1092 with %User{} = muted <- User.get_cached_by_id(id),
1093 {:ok, muter} <- User.mute(muter, muted, notifications) do
1095 |> put_view(AccountView)
1096 |> render("relationship.json", %{user: muter, target: muted})
1098 {:error, message} ->
1100 |> put_status(:forbidden)
1101 |> json(%{error: message})
1105 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1106 with %User{} = muted <- User.get_cached_by_id(id),
1107 {:ok, muter} <- User.unmute(muter, muted) do
1109 |> put_view(AccountView)
1110 |> render("relationship.json", %{user: muter, target: muted})
1112 {:error, message} ->
1114 |> put_status(:forbidden)
1115 |> json(%{error: message})
1119 def mutes(%{assigns: %{user: user}} = conn, _) do
1120 with muted_accounts <- User.muted_users(user) do
1121 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1126 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1127 with %User{} = blocked <- User.get_cached_by_id(id),
1128 {:ok, blocker} <- User.block(blocker, blocked),
1129 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1131 |> put_view(AccountView)
1132 |> render("relationship.json", %{user: blocker, target: blocked})
1134 {:error, message} ->
1136 |> put_status(:forbidden)
1137 |> json(%{error: message})
1141 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1142 with %User{} = blocked <- User.get_cached_by_id(id),
1143 {:ok, blocker} <- User.unblock(blocker, blocked),
1144 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1146 |> put_view(AccountView)
1147 |> render("relationship.json", %{user: blocker, target: blocked})
1149 {:error, message} ->
1151 |> put_status(:forbidden)
1152 |> json(%{error: message})
1156 def blocks(%{assigns: %{user: user}} = conn, _) do
1157 with blocked_accounts <- User.blocked_users(user) do
1158 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1163 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1164 json(conn, info.domain_blocks || [])
1167 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1168 User.block_domain(blocker, domain)
1172 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1173 User.unblock_domain(blocker, domain)
1177 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1178 with %User{} = subscription_target <- User.get_cached_by_id(id),
1179 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1181 |> put_view(AccountView)
1182 |> render("relationship.json", %{user: user, target: subscription_target})
1184 {:error, message} ->
1186 |> put_status(:forbidden)
1187 |> json(%{error: message})
1191 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1192 with %User{} = subscription_target <- User.get_cached_by_id(id),
1193 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1195 |> put_view(AccountView)
1196 |> render("relationship.json", %{user: user, target: subscription_target})
1198 {:error, message} ->
1200 |> put_status(:forbidden)
1201 |> json(%{error: message})
1205 def favourites(%{assigns: %{user: user}} = conn, params) do
1208 |> Map.put("type", "Create")
1209 |> Map.put("favorited_by", user.ap_id)
1210 |> Map.put("blocking_user", user)
1213 ActivityPub.fetch_activities([], params)
1217 |> add_link_headers(:favourites, activities)
1218 |> put_view(StatusView)
1219 |> render("index.json", %{activities: activities, for: user, as: :activity})
1222 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1223 with %User{} = user <- User.get_by_id(id),
1224 false <- user.info.hide_favorites do
1227 |> Map.put("type", "Create")
1228 |> Map.put("favorited_by", user.ap_id)
1229 |> Map.put("blocking_user", for_user)
1233 ["https://www.w3.org/ns/activitystreams#Public"] ++
1234 [for_user.ap_id | for_user.following]
1236 ["https://www.w3.org/ns/activitystreams#Public"]
1241 |> ActivityPub.fetch_activities(params)
1245 |> add_link_headers(:favourites, activities)
1246 |> put_view(StatusView)
1247 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1249 nil -> {:error, :not_found}
1250 true -> render_error(conn, :forbidden, "Can't get favorites")
1254 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1255 user = User.get_cached_by_id(user.id)
1258 Bookmark.for_user_query(user.id)
1259 |> Pagination.fetch_paginated(params)
1263 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1266 |> add_link_headers(:bookmarks, bookmarks)
1267 |> put_view(StatusView)
1268 |> render("index.json", %{activities: activities, for: user, as: :activity})
1271 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1272 lists = Pleroma.List.for_user(user, opts)
1273 res = ListView.render("lists.json", lists: lists)
1277 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1278 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1279 res = ListView.render("list.json", list: list)
1282 _e -> render_error(conn, :not_found, "Record not found")
1286 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1287 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1288 res = ListView.render("lists.json", lists: lists)
1292 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1293 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1294 {:ok, _list} <- Pleroma.List.delete(list) do
1298 json(conn, dgettext("errors", "error"))
1302 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1303 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1304 res = ListView.render("list.json", list: list)
1309 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1311 |> Enum.each(fn account_id ->
1312 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1313 %User{} = followed <- User.get_cached_by_id(account_id) do
1314 Pleroma.List.follow(list, followed)
1321 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1323 |> Enum.each(fn account_id ->
1324 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1325 %User{} = followed <- User.get_cached_by_id(account_id) do
1326 Pleroma.List.unfollow(list, followed)
1333 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1334 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1335 {:ok, users} = Pleroma.List.get_following(list) do
1337 |> put_view(AccountView)
1338 |> render("accounts.json", %{for: user, users: users, as: :user})
1342 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1343 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1344 {:ok, list} <- Pleroma.List.rename(list, title) do
1345 res = ListView.render("list.json", list: list)
1349 json(conn, dgettext("errors", "error"))
1353 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1354 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1357 |> Map.put("type", "Create")
1358 |> Map.put("blocking_user", user)
1359 |> Map.put("muting_user", user)
1361 # we must filter the following list for the user to avoid leaking statuses the user
1362 # does not actually have permission to see (for more info, peruse security issue #270).
1365 |> Enum.filter(fn x -> x in user.following end)
1366 |> ActivityPub.fetch_activities_bounded(following, params)
1370 |> put_view(StatusView)
1371 |> render("index.json", %{activities: activities, for: user, as: :activity})
1373 _e -> render_error(conn, :forbidden, "Error.")
1377 def index(%{assigns: %{user: user}} = conn, _params) do
1378 token = get_session(conn, :oauth_token)
1381 mastodon_emoji = mastodonized_emoji()
1383 limit = Config.get([:instance, :limit])
1386 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1391 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1392 access_token: token,
1394 domain: Pleroma.Web.Endpoint.host(),
1397 unfollow_modal: false,
1400 auto_play_gif: false,
1401 display_sensitive_media: false,
1402 reduce_motion: false,
1403 max_toot_chars: limit,
1404 mascot: User.get_mascot(user)["url"]
1406 poll_limits: Config.get([:instance, :poll_limits]),
1408 delete_others_notice: present?(user.info.is_moderator),
1409 admin: present?(user.info.is_admin)
1413 default_privacy: user.info.default_scope,
1414 default_sensitive: false,
1415 allow_content_types: Config.get([:instance, :allowed_post_formats])
1417 media_attachments: %{
1418 accept_content_types: [
1434 user.info.settings ||
1464 push_subscription: nil,
1466 custom_emojis: mastodon_emoji,
1472 |> put_layout(false)
1473 |> put_view(MastodonView)
1474 |> render("index.html", %{initial_state: initial_state})
1477 |> put_session(:return_to, conn.request_path)
1478 |> redirect(to: "/web/login")
1482 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1483 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1485 with changeset <- Ecto.Changeset.change(user),
1486 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1487 {:ok, _user} <- User.update_and_set_cache(changeset) do
1492 |> put_status(:internal_server_error)
1493 |> json(%{error: inspect(e)})
1497 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1498 redirect(conn, to: local_mastodon_root_path(conn))
1501 @doc "Local Mastodon FE login init action"
1502 def login(conn, %{"code" => auth_token}) do
1503 with {:ok, app} <- get_or_make_app(),
1504 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1505 {:ok, token} <- Token.exchange_token(app, auth) do
1507 |> put_session(:oauth_token, token.token)
1508 |> redirect(to: local_mastodon_root_path(conn))
1512 @doc "Local Mastodon FE callback action"
1513 def login(conn, _) do
1514 with {:ok, app} <- get_or_make_app() do
1519 response_type: "code",
1520 client_id: app.client_id,
1522 scope: Enum.join(app.scopes, " ")
1525 redirect(conn, to: path)
1529 defp local_mastodon_root_path(conn) do
1530 case get_session(conn, :return_to) do
1532 mastodon_api_path(conn, :index, ["getting-started"])
1535 delete_session(conn, :return_to)
1540 defp get_or_make_app do
1541 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1542 scopes = ["read", "write", "follow", "push"]
1544 with %App{} = app <- Repo.get_by(App, find_attrs) do
1546 if app.scopes == scopes do
1550 |> Ecto.Changeset.change(%{scopes: scopes})
1558 App.register_changeset(
1560 Map.put(find_attrs, :scopes, scopes)
1567 def logout(conn, _) do
1570 |> redirect(to: "/")
1573 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1574 Logger.debug("Unimplemented, returning unmodified relationship")
1576 with %User{} = target <- User.get_cached_by_id(id) do
1578 |> put_view(AccountView)
1579 |> render("relationship.json", %{user: user, target: target})
1583 def empty_array(conn, _) do
1584 Logger.debug("Unimplemented, returning an empty array")
1588 def empty_object(conn, _) do
1589 Logger.debug("Unimplemented, returning an empty object")
1593 def get_filters(%{assigns: %{user: user}} = conn, _) do
1594 filters = Filter.get_filters(user)
1595 res = FilterView.render("filters.json", filters: filters)
1600 %{assigns: %{user: user}} = conn,
1601 %{"phrase" => phrase, "context" => context} = params
1607 hide: Map.get(params, "irreversible", false),
1608 whole_word: Map.get(params, "boolean", true)
1612 {:ok, response} = Filter.create(query)
1613 res = FilterView.render("filter.json", filter: response)
1617 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1618 filter = Filter.get(filter_id, user)
1619 res = FilterView.render("filter.json", filter: filter)
1624 %{assigns: %{user: user}} = conn,
1625 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1629 filter_id: filter_id,
1632 hide: Map.get(params, "irreversible", nil),
1633 whole_word: Map.get(params, "boolean", true)
1637 {:ok, response} = Filter.update(query)
1638 res = FilterView.render("filter.json", filter: response)
1642 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1645 filter_id: filter_id
1648 {:ok, _} = Filter.delete(query)
1654 def errors(conn, {:error, %Changeset{} = changeset}) do
1657 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1658 |> Enum.map_join(", ", fn {_k, v} -> v end)
1661 |> put_status(:unprocessable_entity)
1662 |> json(%{error: error_message})
1665 def errors(conn, {:error, :not_found}) do
1666 render_error(conn, :not_found, "Record not found")
1669 def errors(conn, {:error, error_message}) do
1671 |> put_status(:bad_request)
1672 |> json(%{error: error_message})
1675 def errors(conn, _) do
1677 |> put_status(:internal_server_error)
1678 |> json(dgettext("errors", "Something went wrong"))
1681 def suggestions(%{assigns: %{user: user}} = conn, _) do
1682 suggestions = Config.get(:suggestions)
1684 if Keyword.get(suggestions, :enabled, false) do
1685 api = Keyword.get(suggestions, :third_party_engine, "")
1686 timeout = Keyword.get(suggestions, :timeout, 5000)
1687 limit = Keyword.get(suggestions, :limit, 23)
1689 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1691 user = user.nickname
1695 |> String.replace("{{host}}", host)
1696 |> String.replace("{{user}}", user)
1698 with {:ok, %{status: 200, body: body}} <-
1703 recv_timeout: timeout,
1707 {:ok, data} <- Jason.decode(body) do
1710 |> Enum.slice(0, limit)
1715 case User.get_or_fetch(x["acct"]) do
1716 {:ok, %User{id: id}} -> id
1722 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1725 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1731 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1738 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1739 with %Activity{} = activity <- Activity.get_by_id(status_id),
1740 true <- Visibility.visible_for_user?(activity, user) do
1744 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1754 def reports(%{assigns: %{user: user}} = conn, params) do
1755 case CommonAPI.report(user, params) do
1758 |> put_view(ReportView)
1759 |> try_render("report.json", %{activity: activity})
1763 |> put_status(:bad_request)
1764 |> json(%{error: err})
1768 def account_register(
1769 %{assigns: %{app: app}} = conn,
1770 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1778 "captcha_answer_data",
1782 |> Map.put("nickname", nickname)
1783 |> Map.put("fullname", params["fullname"] || nickname)
1784 |> Map.put("bio", params["bio"] || "")
1785 |> Map.put("confirm", params["password"])
1787 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1788 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1790 token_type: "Bearer",
1791 access_token: token.token,
1793 created_at: Token.Utils.format_created_at(token)
1798 |> put_status(:bad_request)
1803 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1804 render_error(conn, :bad_request, "Missing parameters")
1807 def account_register(conn, _) do
1808 render_error(conn, :forbidden, "Invalid credentials")
1811 def conversations(%{assigns: %{user: user}} = conn, params) do
1812 participations = Participation.for_user_with_last_activity_id(user, params)
1815 Enum.map(participations, fn participation ->
1816 ConversationView.render("participation.json", %{participation: participation, user: user})
1820 |> add_link_headers(:conversations, participations)
1821 |> json(conversations)
1824 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1825 with %Participation{} = participation <-
1826 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1827 {:ok, participation} <- Participation.mark_as_read(participation) do
1828 participation_view =
1829 ConversationView.render("participation.json", %{participation: participation, user: user})
1832 |> json(participation_view)
1836 def password_reset(conn, params) do
1837 nickname_or_email = params["email"] || params["nickname"]
1839 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1841 |> put_status(:no_content)
1844 {:error, "unknown user"} ->
1845 send_resp(conn, :not_found, "")
1848 send_resp(conn, :bad_request, "")
1852 def try_render(conn, target, params)
1853 when is_binary(target) do
1854 case render(conn, target, params) do
1855 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1860 def try_render(conn, _, _) do
1861 render_error(conn, :not_implemented, "Can't display this activity")
1864 defp present?(nil), do: false
1865 defp present?(false), do: false
1866 defp present?(_), do: true