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)
889 |> Enum.filter(&(not User.blocks?(user, &1)))
892 |> put_view(AccountView)
893 |> render("accounts.json", %{for: user, users: users, as: :user})
899 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
900 with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id),
901 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
902 q = from(u in User, where: u.ap_id in ^announces)
906 |> Enum.filter(&(not User.blocks?(user, &1)))
909 |> put_view(AccountView)
910 |> render("accounts.json", %{for: user, users: users, as: :user})
916 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
917 local_only = params["local"] in [true, "True", "true", "1"]
920 [params["tag"], params["any"]]
924 |> Enum.map(&String.downcase(&1))
929 |> Enum.map(&String.downcase(&1))
934 |> Enum.map(&String.downcase(&1))
938 |> Map.put("type", "Create")
939 |> Map.put("local_only", local_only)
940 |> Map.put("blocking_user", user)
941 |> Map.put("muting_user", user)
942 |> Map.put("tag", tags)
943 |> Map.put("tag_all", tag_all)
944 |> Map.put("tag_reject", tag_reject)
945 |> ActivityPub.fetch_public_activities()
949 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
950 |> put_view(StatusView)
951 |> render("index.json", %{activities: activities, for: user, as: :activity})
954 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
955 with %User{} = user <- User.get_cached_by_id(id),
956 followers <- MastodonAPI.get_followers(user, params) do
959 for_user && user.id == for_user.id -> followers
960 user.info.hide_followers -> []
965 |> add_link_headers(:followers, followers, user)
966 |> put_view(AccountView)
967 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
971 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
972 with %User{} = user <- User.get_cached_by_id(id),
973 followers <- MastodonAPI.get_friends(user, params) do
976 for_user && user.id == for_user.id -> followers
977 user.info.hide_follows -> []
982 |> add_link_headers(:following, followers, user)
983 |> put_view(AccountView)
984 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
988 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
989 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
991 |> put_view(AccountView)
992 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
996 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
997 with %User{} = follower <- User.get_cached_by_id(id),
998 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
1000 |> put_view(AccountView)
1001 |> render("relationship.json", %{user: followed, target: follower})
1003 {:error, message} ->
1005 |> put_status(:forbidden)
1006 |> json(%{error: message})
1010 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1011 with %User{} = follower <- User.get_cached_by_id(id),
1012 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
1014 |> put_view(AccountView)
1015 |> render("relationship.json", %{user: followed, target: follower})
1017 {:error, message} ->
1019 |> put_status(:forbidden)
1020 |> json(%{error: message})
1024 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1025 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1026 {_, true} <- {:followed, follower.id != followed.id},
1027 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
1029 |> put_view(AccountView)
1030 |> render("relationship.json", %{user: follower, target: followed})
1033 {:error, :not_found}
1035 {:error, message} ->
1037 |> put_status(:forbidden)
1038 |> json(%{error: message})
1042 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1043 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1044 {_, true} <- {:followed, follower.id != followed.id},
1045 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1047 |> put_view(AccountView)
1048 |> render("account.json", %{user: followed, for: follower})
1051 {:error, :not_found}
1053 {:error, message} ->
1055 |> put_status(:forbidden)
1056 |> json(%{error: message})
1060 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1061 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1062 {_, true} <- {:followed, follower.id != followed.id},
1063 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1065 |> put_view(AccountView)
1066 |> render("relationship.json", %{user: follower, target: followed})
1069 {:error, :not_found}
1076 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1078 if Map.has_key?(params, "notifications"),
1079 do: params["notifications"] in [true, "True", "true", "1"],
1082 with %User{} = muted <- User.get_cached_by_id(id),
1083 {:ok, muter} <- User.mute(muter, muted, notifications) do
1085 |> put_view(AccountView)
1086 |> render("relationship.json", %{user: muter, target: muted})
1088 {:error, message} ->
1090 |> put_status(:forbidden)
1091 |> json(%{error: message})
1095 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1096 with %User{} = muted <- User.get_cached_by_id(id),
1097 {:ok, muter} <- User.unmute(muter, muted) do
1099 |> put_view(AccountView)
1100 |> render("relationship.json", %{user: muter, target: muted})
1102 {:error, message} ->
1104 |> put_status(:forbidden)
1105 |> json(%{error: message})
1109 def mutes(%{assigns: %{user: user}} = conn, _) do
1110 with muted_accounts <- User.muted_users(user) do
1111 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1116 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1117 with %User{} = blocked <- User.get_cached_by_id(id),
1118 {:ok, blocker} <- User.block(blocker, blocked),
1119 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1121 |> put_view(AccountView)
1122 |> render("relationship.json", %{user: blocker, target: blocked})
1124 {:error, message} ->
1126 |> put_status(:forbidden)
1127 |> json(%{error: message})
1131 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1132 with %User{} = blocked <- User.get_cached_by_id(id),
1133 {:ok, blocker} <- User.unblock(blocker, blocked),
1134 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1136 |> put_view(AccountView)
1137 |> render("relationship.json", %{user: blocker, target: blocked})
1139 {:error, message} ->
1141 |> put_status(:forbidden)
1142 |> json(%{error: message})
1146 def blocks(%{assigns: %{user: user}} = conn, _) do
1147 with blocked_accounts <- User.blocked_users(user) do
1148 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1153 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1154 json(conn, info.domain_blocks || [])
1157 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1158 User.block_domain(blocker, domain)
1162 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1163 User.unblock_domain(blocker, domain)
1167 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1168 with %User{} = subscription_target <- User.get_cached_by_id(id),
1169 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1171 |> put_view(AccountView)
1172 |> render("relationship.json", %{user: user, target: subscription_target})
1174 {:error, message} ->
1176 |> put_status(:forbidden)
1177 |> json(%{error: message})
1181 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1182 with %User{} = subscription_target <- User.get_cached_by_id(id),
1183 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1185 |> put_view(AccountView)
1186 |> render("relationship.json", %{user: user, target: subscription_target})
1188 {:error, message} ->
1190 |> put_status(:forbidden)
1191 |> json(%{error: message})
1195 def favourites(%{assigns: %{user: user}} = conn, params) do
1198 |> Map.put("type", "Create")
1199 |> Map.put("favorited_by", user.ap_id)
1200 |> Map.put("blocking_user", user)
1203 ActivityPub.fetch_activities([], params)
1207 |> add_link_headers(:favourites, activities)
1208 |> put_view(StatusView)
1209 |> render("index.json", %{activities: activities, for: user, as: :activity})
1212 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1213 with %User{} = user <- User.get_by_id(id),
1214 false <- user.info.hide_favorites do
1217 |> Map.put("type", "Create")
1218 |> Map.put("favorited_by", user.ap_id)
1219 |> Map.put("blocking_user", for_user)
1223 ["https://www.w3.org/ns/activitystreams#Public"] ++
1224 [for_user.ap_id | for_user.following]
1226 ["https://www.w3.org/ns/activitystreams#Public"]
1231 |> ActivityPub.fetch_activities(params)
1235 |> add_link_headers(:favourites, activities)
1236 |> put_view(StatusView)
1237 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1239 nil -> {:error, :not_found}
1240 true -> render_error(conn, :forbidden, "Can't get favorites")
1244 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1245 user = User.get_cached_by_id(user.id)
1248 Bookmark.for_user_query(user.id)
1249 |> Pagination.fetch_paginated(params)
1253 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1256 |> add_link_headers(:bookmarks, bookmarks)
1257 |> put_view(StatusView)
1258 |> render("index.json", %{activities: activities, for: user, as: :activity})
1261 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1262 lists = Pleroma.List.for_user(user, opts)
1263 res = ListView.render("lists.json", lists: lists)
1267 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1268 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1269 res = ListView.render("list.json", list: list)
1272 _e -> render_error(conn, :not_found, "Record not found")
1276 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1277 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1278 res = ListView.render("lists.json", lists: lists)
1282 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1283 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1284 {:ok, _list} <- Pleroma.List.delete(list) do
1288 json(conn, dgettext("errors", "error"))
1292 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1293 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1294 res = ListView.render("list.json", list: list)
1299 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1301 |> Enum.each(fn account_id ->
1302 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1303 %User{} = followed <- User.get_cached_by_id(account_id) do
1304 Pleroma.List.follow(list, followed)
1311 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1313 |> Enum.each(fn account_id ->
1314 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1315 %User{} = followed <- User.get_cached_by_id(account_id) do
1316 Pleroma.List.unfollow(list, followed)
1323 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1324 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1325 {:ok, users} = Pleroma.List.get_following(list) do
1327 |> put_view(AccountView)
1328 |> render("accounts.json", %{for: user, users: users, as: :user})
1332 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1333 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1334 {:ok, list} <- Pleroma.List.rename(list, title) do
1335 res = ListView.render("list.json", list: list)
1339 json(conn, dgettext("errors", "error"))
1343 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1344 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1347 |> Map.put("type", "Create")
1348 |> Map.put("blocking_user", user)
1349 |> Map.put("muting_user", user)
1351 # we must filter the following list for the user to avoid leaking statuses the user
1352 # does not actually have permission to see (for more info, peruse security issue #270).
1355 |> Enum.filter(fn x -> x in user.following end)
1356 |> ActivityPub.fetch_activities_bounded(following, params)
1360 |> put_view(StatusView)
1361 |> render("index.json", %{activities: activities, for: user, as: :activity})
1363 _e -> render_error(conn, :forbidden, "Error.")
1367 def index(%{assigns: %{user: user}} = conn, _params) do
1368 token = get_session(conn, :oauth_token)
1371 mastodon_emoji = mastodonized_emoji()
1373 limit = Config.get([:instance, :limit])
1376 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1381 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1382 access_token: token,
1384 domain: Pleroma.Web.Endpoint.host(),
1387 unfollow_modal: false,
1390 auto_play_gif: false,
1391 display_sensitive_media: false,
1392 reduce_motion: false,
1393 max_toot_chars: limit,
1394 mascot: User.get_mascot(user)["url"]
1396 poll_limits: Config.get([:instance, :poll_limits]),
1398 delete_others_notice: present?(user.info.is_moderator),
1399 admin: present?(user.info.is_admin)
1403 default_privacy: user.info.default_scope,
1404 default_sensitive: false,
1405 allow_content_types: Config.get([:instance, :allowed_post_formats])
1407 media_attachments: %{
1408 accept_content_types: [
1424 user.info.settings ||
1454 push_subscription: nil,
1456 custom_emojis: mastodon_emoji,
1462 |> put_layout(false)
1463 |> put_view(MastodonView)
1464 |> render("index.html", %{initial_state: initial_state})
1467 |> put_session(:return_to, conn.request_path)
1468 |> redirect(to: "/web/login")
1472 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1473 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1475 with changeset <- Ecto.Changeset.change(user),
1476 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1477 {:ok, _user} <- User.update_and_set_cache(changeset) do
1482 |> put_status(:internal_server_error)
1483 |> json(%{error: inspect(e)})
1487 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1488 redirect(conn, to: local_mastodon_root_path(conn))
1491 @doc "Local Mastodon FE login init action"
1492 def login(conn, %{"code" => auth_token}) do
1493 with {:ok, app} <- get_or_make_app(),
1494 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1495 {:ok, token} <- Token.exchange_token(app, auth) do
1497 |> put_session(:oauth_token, token.token)
1498 |> redirect(to: local_mastodon_root_path(conn))
1502 @doc "Local Mastodon FE callback action"
1503 def login(conn, _) do
1504 with {:ok, app} <- get_or_make_app() do
1509 response_type: "code",
1510 client_id: app.client_id,
1512 scope: Enum.join(app.scopes, " ")
1515 redirect(conn, to: path)
1519 defp local_mastodon_root_path(conn) do
1520 case get_session(conn, :return_to) do
1522 mastodon_api_path(conn, :index, ["getting-started"])
1525 delete_session(conn, :return_to)
1530 defp get_or_make_app do
1531 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1532 scopes = ["read", "write", "follow", "push"]
1534 with %App{} = app <- Repo.get_by(App, find_attrs) do
1536 if app.scopes == scopes do
1540 |> Ecto.Changeset.change(%{scopes: scopes})
1548 App.register_changeset(
1550 Map.put(find_attrs, :scopes, scopes)
1557 def logout(conn, _) do
1560 |> redirect(to: "/")
1563 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1564 Logger.debug("Unimplemented, returning unmodified relationship")
1566 with %User{} = target <- User.get_cached_by_id(id) do
1568 |> put_view(AccountView)
1569 |> render("relationship.json", %{user: user, target: target})
1573 def empty_array(conn, _) do
1574 Logger.debug("Unimplemented, returning an empty array")
1578 def empty_object(conn, _) do
1579 Logger.debug("Unimplemented, returning an empty object")
1583 def get_filters(%{assigns: %{user: user}} = conn, _) do
1584 filters = Filter.get_filters(user)
1585 res = FilterView.render("filters.json", filters: filters)
1590 %{assigns: %{user: user}} = conn,
1591 %{"phrase" => phrase, "context" => context} = params
1597 hide: Map.get(params, "irreversible", false),
1598 whole_word: Map.get(params, "boolean", true)
1602 {:ok, response} = Filter.create(query)
1603 res = FilterView.render("filter.json", filter: response)
1607 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1608 filter = Filter.get(filter_id, user)
1609 res = FilterView.render("filter.json", filter: filter)
1614 %{assigns: %{user: user}} = conn,
1615 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1619 filter_id: filter_id,
1622 hide: Map.get(params, "irreversible", nil),
1623 whole_word: Map.get(params, "boolean", true)
1627 {:ok, response} = Filter.update(query)
1628 res = FilterView.render("filter.json", filter: response)
1632 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1635 filter_id: filter_id
1638 {:ok, _} = Filter.delete(query)
1644 def errors(conn, {:error, %Changeset{} = changeset}) do
1647 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1648 |> Enum.map_join(", ", fn {_k, v} -> v end)
1651 |> put_status(:unprocessable_entity)
1652 |> json(%{error: error_message})
1655 def errors(conn, {:error, :not_found}) do
1656 render_error(conn, :not_found, "Record not found")
1659 def errors(conn, {:error, error_message}) do
1661 |> put_status(:bad_request)
1662 |> json(%{error: error_message})
1665 def errors(conn, _) do
1667 |> put_status(:internal_server_error)
1668 |> json(dgettext("errors", "Something went wrong"))
1671 def suggestions(%{assigns: %{user: user}} = conn, _) do
1672 suggestions = Config.get(:suggestions)
1674 if Keyword.get(suggestions, :enabled, false) do
1675 api = Keyword.get(suggestions, :third_party_engine, "")
1676 timeout = Keyword.get(suggestions, :timeout, 5000)
1677 limit = Keyword.get(suggestions, :limit, 23)
1679 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1681 user = user.nickname
1685 |> String.replace("{{host}}", host)
1686 |> String.replace("{{user}}", user)
1688 with {:ok, %{status: 200, body: body}} <-
1693 recv_timeout: timeout,
1697 {:ok, data} <- Jason.decode(body) do
1700 |> Enum.slice(0, limit)
1705 case User.get_or_fetch(x["acct"]) do
1706 {:ok, %User{id: id}} -> id
1712 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1715 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1721 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1728 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1729 with %Activity{} = activity <- Activity.get_by_id(status_id),
1730 true <- Visibility.visible_for_user?(activity, user) do
1734 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1744 def reports(%{assigns: %{user: user}} = conn, params) do
1745 case CommonAPI.report(user, params) do
1748 |> put_view(ReportView)
1749 |> try_render("report.json", %{activity: activity})
1753 |> put_status(:bad_request)
1754 |> json(%{error: err})
1758 def account_register(
1759 %{assigns: %{app: app}} = conn,
1760 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1768 "captcha_answer_data",
1772 |> Map.put("nickname", nickname)
1773 |> Map.put("fullname", params["fullname"] || nickname)
1774 |> Map.put("bio", params["bio"] || "")
1775 |> Map.put("confirm", params["password"])
1777 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1778 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1780 token_type: "Bearer",
1781 access_token: token.token,
1783 created_at: Token.Utils.format_created_at(token)
1788 |> put_status(:bad_request)
1793 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1794 render_error(conn, :bad_request, "Missing parameters")
1797 def account_register(conn, _) do
1798 render_error(conn, :forbidden, "Invalid credentials")
1801 def conversations(%{assigns: %{user: user}} = conn, params) do
1802 participations = Participation.for_user_with_last_activity_id(user, params)
1805 Enum.map(participations, fn participation ->
1806 ConversationView.render("participation.json", %{participation: participation, user: user})
1810 |> add_link_headers(:conversations, participations)
1811 |> json(conversations)
1814 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1815 with %Participation{} = participation <-
1816 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1817 {:ok, participation} <- Participation.mark_as_read(participation) do
1818 participation_view =
1819 ConversationView.render("participation.json", %{participation: participation, user: user})
1822 |> json(participation_view)
1826 def password_reset(conn, params) do
1827 nickname_or_email = params["email"] || params["nickname"]
1829 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1831 |> put_status(:no_content)
1834 {:error, "unknown user"} ->
1835 send_resp(conn, :not_found, "")
1838 send_resp(conn, :bad_request, "")
1842 def try_render(conn, target, params)
1843 when is_binary(target) do
1844 case render(conn, target, params) do
1845 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1850 def try_render(conn, _, _) do
1851 render_error(conn, :not_implemented, "Can't display this activity")
1854 defp present?(nil), do: false
1855 defp present?(false), do: false
1856 defp present?(_), do: true