1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
5 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
6 use Pleroma.Web, :controller
8 import Pleroma.Web.ControllerHelper,
9 only: [json_response: 3, add_link_headers: 5, add_link_headers: 4, add_link_headers: 3]
12 alias Pleroma.Activity
13 alias Pleroma.Bookmark
15 alias Pleroma.Conversation.Participation
17 alias Pleroma.Formatter
19 alias Pleroma.Notification
21 alias Pleroma.Pagination
22 alias Pleroma.Plugs.RateLimiter
24 alias Pleroma.ScheduledActivity
28 alias Pleroma.Web.ActivityPub.ActivityPub
29 alias Pleroma.Web.ActivityPub.Visibility
30 alias Pleroma.Web.CommonAPI
31 alias Pleroma.Web.MastodonAPI.AccountView
32 alias Pleroma.Web.MastodonAPI.AppView
33 alias Pleroma.Web.MastodonAPI.ConversationView
34 alias Pleroma.Web.MastodonAPI.FilterView
35 alias Pleroma.Web.MastodonAPI.ListView
36 alias Pleroma.Web.MastodonAPI.MastodonAPI
37 alias Pleroma.Web.MastodonAPI.MastodonView
38 alias Pleroma.Web.MastodonAPI.NotificationView
39 alias Pleroma.Web.MastodonAPI.ReportView
40 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
41 alias Pleroma.Web.MastodonAPI.StatusView
42 alias Pleroma.Web.MediaProxy
43 alias Pleroma.Web.OAuth.App
44 alias Pleroma.Web.OAuth.Authorization
45 alias Pleroma.Web.OAuth.Scopes
46 alias Pleroma.Web.OAuth.Token
47 alias Pleroma.Web.TwitterAPI.TwitterAPI
49 alias Pleroma.Web.ControllerHelper
53 require Pleroma.Constants
55 @rate_limited_relations_actions ~w(follow unfollow)a
57 @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
58 post_status delete_status)a
62 {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
63 when action in ~w(reblog_status unreblog_status)a
68 {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
69 when action in ~w(fav_status unfav_status)a
74 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
77 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
78 plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
79 plug(RateLimiter, :app_account_creation when action == :account_register)
80 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
81 plug(RateLimiter, :password_reset when action == :password_reset)
82 plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
84 @local_mastodon_name "Mastodon-Local"
86 action_fallback(:errors)
88 def create_app(conn, params) do
89 scopes = Scopes.fetch_scopes(params, ["read"])
93 |> Map.drop(["scope", "scopes"])
94 |> Map.put("scopes", scopes)
96 with cs <- App.register_changeset(%App{}, app_attrs),
97 false <- cs.changes[:client_name] == @local_mastodon_name,
98 {:ok, app} <- Repo.insert(cs) do
101 |> render("show.json", %{app: app})
110 value_function \\ fn x -> {:ok, x} end
112 if Map.has_key?(params, params_field) do
113 case value_function.(params[params_field]) do
114 {:ok, new_value} -> Map.put(map, map_field, new_value)
122 def update_credentials(%{assigns: %{user: user}} = conn, params) do
127 |> add_if_present(params, "display_name", :name)
128 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
129 |> add_if_present(params, "avatar", :avatar, fn value ->
130 with %Plug.Upload{} <- value,
131 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
138 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
142 |> Map.get(:emoji, [])
143 |> Enum.concat(Formatter.get_emoji_map(emojis_text))
147 if Map.has_key?(params, "fields_attributes") do
148 Map.update!(params, "fields_attributes", fn fields ->
149 if Enum.all?(fields, &is_tuple/1) do
150 Enum.map(fields, fn {_, v} -> v end)
154 |> Enum.filter(fn %{"name" => n} -> n != "" end)
168 :skip_thread_containment
170 |> Enum.reduce(%{}, fn key, acc ->
171 add_if_present(acc, params, to_string(key), key, fn value ->
172 {:ok, ControllerHelper.truthy_param?(value)}
175 |> add_if_present(params, "default_scope", :default_scope)
176 |> add_if_present(params, "fields_attributes", :fields, fn fields ->
177 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
181 |> add_if_present(params, "fields_attributes", :raw_fields)
182 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
183 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
185 |> add_if_present(params, "header", :banner, fn value ->
186 with %Plug.Upload{} <- value,
187 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
193 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
194 with %Plug.Upload{} <- value,
195 {:ok, object} <- ActivityPub.upload(value, type: :background) do
201 |> Map.put(:emoji, user_info_emojis)
203 info_cng = User.Info.profile_update(user.info, info_params)
205 with changeset <- User.update_changeset(user, user_params),
206 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
207 {:ok, user} <- User.update_and_set_cache(changeset) do
208 if original_user != user do
209 CommonAPI.update(user)
214 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
217 _e -> render_error(conn, :forbidden, "Invalid request")
221 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
222 change = Changeset.change(user, %{avatar: nil})
223 {:ok, user} = User.update_and_set_cache(change)
224 CommonAPI.update(user)
226 json(conn, %{url: nil})
229 def update_avatar(%{assigns: %{user: user}} = conn, params) do
230 {:ok, object} = ActivityPub.upload(params, type: :avatar)
231 change = Changeset.change(user, %{avatar: object.data})
232 {:ok, user} = User.update_and_set_cache(change)
233 CommonAPI.update(user)
234 %{"url" => [%{"href" => href} | _]} = object.data
236 json(conn, %{url: href})
239 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
240 with new_info <- %{"banner" => %{}},
241 info_cng <- User.Info.profile_update(user.info, new_info),
242 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
243 {:ok, user} <- User.update_and_set_cache(changeset) do
244 CommonAPI.update(user)
246 json(conn, %{url: nil})
250 def update_banner(%{assigns: %{user: user}} = conn, params) do
251 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
252 new_info <- %{"banner" => object.data},
253 info_cng <- User.Info.profile_update(user.info, new_info),
254 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
255 {:ok, user} <- User.update_and_set_cache(changeset) do
256 CommonAPI.update(user)
257 %{"url" => [%{"href" => href} | _]} = object.data
259 json(conn, %{url: href})
263 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
264 with new_info <- %{"background" => %{}},
265 info_cng <- User.Info.profile_update(user.info, new_info),
266 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
267 {:ok, _user} <- User.update_and_set_cache(changeset) do
268 json(conn, %{url: nil})
272 def update_background(%{assigns: %{user: user}} = conn, params) do
273 with {:ok, object} <- ActivityPub.upload(params, type: :background),
274 new_info <- %{"background" => object.data},
275 info_cng <- User.Info.profile_update(user.info, new_info),
276 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng),
277 {:ok, _user} <- User.update_and_set_cache(changeset) do
278 %{"url" => [%{"href" => href} | _]} = object.data
280 json(conn, %{url: href})
284 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
285 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
288 AccountView.render("account.json", %{
291 with_pleroma_settings: true,
292 with_chat_token: chat_token
298 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
299 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
302 |> render("short.json", %{app: app})
306 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
307 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id),
308 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
309 account = AccountView.render("account.json", %{user: user, for: for_user})
312 _e -> render_error(conn, :not_found, "Can't find user")
316 @mastodon_api_level "2.7.2"
318 def masto_instance(conn, _params) do
319 instance = Config.get(:instance)
323 title: Keyword.get(instance, :name),
324 description: Keyword.get(instance, :description),
325 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
326 email: Keyword.get(instance, :email),
328 streaming_api: Pleroma.Web.Endpoint.websocket_url()
330 stats: Stats.get_stats(),
331 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
333 registrations: Pleroma.Config.get([:instance, :registrations_open]),
334 # Extra (not present in Mastodon):
335 max_toot_chars: Keyword.get(instance, :limit),
336 poll_limits: Keyword.get(instance, :poll_limits)
342 def peers(conn, _params) do
343 json(conn, Stats.get_peers())
346 defp mastodonized_emoji do
347 Pleroma.Emoji.get_all()
348 |> Enum.map(fn {shortcode, relative_url, tags} ->
349 url = to_string(URI.merge(Web.base_url(), relative_url))
352 "shortcode" => shortcode,
354 "visible_in_picker" => true,
357 # Assuming that a comma is authorized in the category name
358 "category" => (tags -- ["Custom"]) |> Enum.join(",")
363 def custom_emojis(conn, _params) do
364 mastodon_emoji = mastodonized_emoji()
365 json(conn, mastodon_emoji)
368 def home_timeline(%{assigns: %{user: user}} = conn, params) do
371 |> Map.put("type", ["Create", "Announce"])
372 |> Map.put("blocking_user", user)
373 |> Map.put("muting_user", user)
374 |> Map.put("user", user)
377 [user.ap_id | user.following]
378 |> ActivityPub.fetch_activities(params)
382 |> add_link_headers(:home_timeline, activities)
383 |> put_view(StatusView)
384 |> render("index.json", %{activities: activities, for: user, as: :activity})
387 def public_timeline(%{assigns: %{user: user}} = conn, params) do
388 local_only = params["local"] in [true, "True", "true", "1"]
392 |> Map.put("type", ["Create", "Announce"])
393 |> Map.put("local_only", local_only)
394 |> Map.put("blocking_user", user)
395 |> Map.put("muting_user", user)
396 |> Map.put("user", user)
397 |> ActivityPub.fetch_public_activities()
401 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
402 |> put_view(StatusView)
403 |> render("index.json", %{activities: activities, for: user, as: :activity})
406 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
407 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"]) do
410 |> Map.put("tag", params["tagged"])
412 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
415 |> add_link_headers(:user_statuses, activities, params["id"])
416 |> put_view(StatusView)
417 |> render("index.json", %{
418 activities: activities,
425 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
428 |> Map.put("type", "Create")
429 |> Map.put("blocking_user", user)
430 |> Map.put("user", user)
431 |> Map.put(:visibility, "direct")
435 |> ActivityPub.fetch_activities_query(params)
436 |> Pagination.fetch_paginated(params)
439 |> add_link_headers(:dm_timeline, activities)
440 |> put_view(StatusView)
441 |> render("index.json", %{activities: activities, for: user, as: :activity})
444 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
445 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
446 true <- Visibility.visible_for_user?(activity, user) do
448 |> put_view(StatusView)
449 |> try_render("status.json", %{activity: activity, for: user})
453 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
454 with %Activity{} = activity <- Activity.get_by_id(id),
456 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
457 "blocking_user" => user,
459 "exclude_id" => activity.id
461 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
467 activities: grouped_activities[true] || [],
471 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
476 activities: grouped_activities[false] || [],
480 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
487 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
488 with %Object{} = object <- Object.get_by_id(id),
489 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
490 true <- Visibility.visible_for_user?(activity, user) do
492 |> put_view(StatusView)
493 |> try_render("poll.json", %{object: object, for: user})
495 error when is_nil(error) or error == false ->
496 render_error(conn, :not_found, "Record not found")
500 defp get_cached_vote_or_vote(user, object, choices) do
501 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
504 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
505 case CommonAPI.vote(user, object, choices) do
506 {:error, _message} = res -> {:ignore, res}
507 res -> {:commit, res}
514 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
515 with %Object{} = object <- Object.get_by_id(id),
516 true <- object.data["type"] == "Question",
517 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
518 true <- Visibility.visible_for_user?(activity, user),
519 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
521 |> put_view(StatusView)
522 |> try_render("poll.json", %{object: object, for: user})
525 render_error(conn, :not_found, "Record not found")
528 render_error(conn, :not_found, "Record not found")
532 |> put_status(:unprocessable_entity)
533 |> json(%{error: message})
537 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
538 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
540 |> add_link_headers(:scheduled_statuses, scheduled_activities)
541 |> put_view(ScheduledActivityView)
542 |> render("index.json", %{scheduled_activities: scheduled_activities})
546 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
547 with %ScheduledActivity{} = scheduled_activity <-
548 ScheduledActivity.get(user, scheduled_activity_id) do
550 |> put_view(ScheduledActivityView)
551 |> render("show.json", %{scheduled_activity: scheduled_activity})
553 _ -> {:error, :not_found}
557 def update_scheduled_status(
558 %{assigns: %{user: user}} = conn,
559 %{"id" => scheduled_activity_id} = params
561 with %ScheduledActivity{} = scheduled_activity <-
562 ScheduledActivity.get(user, scheduled_activity_id),
563 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
565 |> put_view(ScheduledActivityView)
566 |> render("show.json", %{scheduled_activity: scheduled_activity})
568 nil -> {:error, :not_found}
573 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
574 with %ScheduledActivity{} = scheduled_activity <-
575 ScheduledActivity.get(user, scheduled_activity_id),
576 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
578 |> put_view(ScheduledActivityView)
579 |> render("show.json", %{scheduled_activity: scheduled_activity})
581 nil -> {:error, :not_found}
586 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
589 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
591 scheduled_at = params["scheduled_at"]
593 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
594 with {:ok, scheduled_activity} <-
595 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
597 |> put_view(ScheduledActivityView)
598 |> render("show.json", %{scheduled_activity: scheduled_activity})
601 params = Map.drop(params, ["scheduled_at"])
603 case CommonAPI.post(user, params) do
606 |> put_status(:unprocessable_entity)
607 |> json(%{error: message})
611 |> put_view(StatusView)
612 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
617 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
618 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
621 _e -> render_error(conn, :forbidden, "Can't delete this post")
625 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
626 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
627 %Activity{} = announce <- Activity.normalize(announce.data) do
629 |> put_view(StatusView)
630 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
634 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
635 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
636 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
638 |> put_view(StatusView)
639 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
643 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
644 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
645 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
647 |> put_view(StatusView)
648 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
652 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
653 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
654 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
656 |> put_view(StatusView)
657 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
661 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
662 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
664 |> put_view(StatusView)
665 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
669 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
670 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
672 |> put_view(StatusView)
673 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
677 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
678 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
679 %User{} = user <- User.get_cached_by_nickname(user.nickname),
680 true <- Visibility.visible_for_user?(activity, user),
681 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
683 |> put_view(StatusView)
684 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
688 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
689 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
690 %User{} = user <- User.get_cached_by_nickname(user.nickname),
691 true <- Visibility.visible_for_user?(activity, user),
692 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
694 |> put_view(StatusView)
695 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
699 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
700 activity = Activity.get_by_id(id)
702 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
704 |> put_view(StatusView)
705 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
709 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
710 activity = Activity.get_by_id(id)
712 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
714 |> put_view(StatusView)
715 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
719 def notifications(%{assigns: %{user: user}} = conn, params) do
720 notifications = MastodonAPI.get_notifications(user, params)
723 |> add_link_headers(:notifications, notifications)
724 |> put_view(NotificationView)
725 |> render("index.json", %{notifications: notifications, for: user})
728 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
729 with {:ok, notification} <- Notification.get(user, id) do
731 |> put_view(NotificationView)
732 |> render("show.json", %{notification: notification, for: user})
736 |> put_status(:forbidden)
737 |> json(%{"error" => reason})
741 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
742 Notification.clear(user)
746 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
747 with {:ok, _notif} <- Notification.dismiss(user, id) do
752 |> put_status(:forbidden)
753 |> json(%{"error" => reason})
757 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
758 Notification.destroy_multiple(user, ids)
762 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
764 q = from(u in User, where: u.id in ^id)
765 targets = Repo.all(q)
768 |> put_view(AccountView)
769 |> render("relationships.json", %{user: user, targets: targets})
772 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
773 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
775 def update_media(%{assigns: %{user: user}} = conn, data) do
776 with %Object{} = object <- Repo.get(Object, data["id"]),
777 true <- Object.authorize_mutation(object, user),
778 true <- is_binary(data["description"]),
779 description <- data["description"] do
780 new_data = %{object.data | "name" => description}
784 |> Object.change(%{data: new_data})
787 attachment_data = Map.put(new_data, "id", object.id)
790 |> put_view(StatusView)
791 |> render("attachment.json", %{attachment: attachment_data})
795 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
796 with {:ok, object} <-
799 actor: User.ap_id(user),
800 description: Map.get(data, "description")
802 attachment_data = Map.put(object.data, "id", object.id)
805 |> put_view(StatusView)
806 |> render("attachment.json", %{attachment: attachment_data})
810 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
811 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
812 %{} = attachment_data <- Map.put(object.data, "id", object.id),
813 %{type: type} = rendered <-
814 StatusView.render("attachment.json", %{attachment: attachment_data}) do
815 # Reject if not an image
816 if type == "image" do
818 # Save to the user's info
819 info_changeset = User.Info.mascot_update(user.info, rendered)
823 |> Ecto.Changeset.change()
824 |> Ecto.Changeset.put_embed(:info, info_changeset)
826 {:ok, _user} = User.update_and_set_cache(user_changeset)
831 render_error(conn, :unsupported_media_type, "mascots can only be images")
836 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
837 mascot = User.get_mascot(user)
843 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
844 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
845 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
846 q = from(u in User, where: u.ap_id in ^likes)
850 |> Enum.filter(&(not User.blocks?(user, &1)))
853 |> put_view(AccountView)
854 |> render("accounts.json", %{for: user, users: users, as: :user})
860 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
861 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
862 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
863 q = from(u in User, where: u.ap_id in ^announces)
867 |> Enum.filter(&(not User.blocks?(user, &1)))
870 |> put_view(AccountView)
871 |> render("accounts.json", %{for: user, users: users, as: :user})
877 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
878 local_only = params["local"] in [true, "True", "true", "1"]
881 [params["tag"], params["any"]]
885 |> Enum.map(&String.downcase(&1))
890 |> Enum.map(&String.downcase(&1))
895 |> Enum.map(&String.downcase(&1))
899 |> Map.put("type", "Create")
900 |> Map.put("local_only", local_only)
901 |> Map.put("blocking_user", user)
902 |> Map.put("muting_user", user)
903 |> Map.put("user", user)
904 |> Map.put("tag", tags)
905 |> Map.put("tag_all", tag_all)
906 |> Map.put("tag_reject", tag_reject)
907 |> ActivityPub.fetch_public_activities()
911 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
912 |> put_view(StatusView)
913 |> render("index.json", %{activities: activities, for: user, as: :activity})
916 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
917 with %User{} = user <- User.get_cached_by_id(id),
918 followers <- MastodonAPI.get_followers(user, params) do
921 for_user && user.id == for_user.id -> followers
922 user.info.hide_followers -> []
927 |> add_link_headers(:followers, followers, user)
928 |> put_view(AccountView)
929 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
933 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
934 with %User{} = user <- User.get_cached_by_id(id),
935 followers <- MastodonAPI.get_friends(user, params) do
938 for_user && user.id == for_user.id -> followers
939 user.info.hide_follows -> []
944 |> add_link_headers(:following, followers, user)
945 |> put_view(AccountView)
946 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
950 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
951 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
953 |> put_view(AccountView)
954 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
958 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
959 with %User{} = follower <- User.get_cached_by_id(id),
960 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
962 |> put_view(AccountView)
963 |> render("relationship.json", %{user: followed, target: follower})
967 |> put_status(:forbidden)
968 |> json(%{error: message})
972 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
973 with %User{} = follower <- User.get_cached_by_id(id),
974 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
976 |> put_view(AccountView)
977 |> render("relationship.json", %{user: followed, target: follower})
981 |> put_status(:forbidden)
982 |> json(%{error: message})
986 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
987 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
988 {_, true} <- {:followed, follower.id != followed.id},
989 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
991 |> put_view(AccountView)
992 |> render("relationship.json", %{user: follower, target: followed})
999 |> put_status(:forbidden)
1000 |> json(%{error: message})
1004 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1005 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1006 {_, true} <- {:followed, follower.id != followed.id},
1007 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1009 |> put_view(AccountView)
1010 |> render("account.json", %{user: followed, for: follower})
1013 {:error, :not_found}
1015 {:error, message} ->
1017 |> put_status(:forbidden)
1018 |> json(%{error: message})
1022 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1023 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1024 {_, true} <- {:followed, follower.id != followed.id},
1025 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1027 |> put_view(AccountView)
1028 |> render("relationship.json", %{user: follower, target: followed})
1031 {:error, :not_found}
1038 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1040 if Map.has_key?(params, "notifications"),
1041 do: params["notifications"] in [true, "True", "true", "1"],
1044 with %User{} = muted <- User.get_cached_by_id(id),
1045 {:ok, muter} <- User.mute(muter, muted, notifications) do
1047 |> put_view(AccountView)
1048 |> render("relationship.json", %{user: muter, target: muted})
1050 {:error, message} ->
1052 |> put_status(:forbidden)
1053 |> json(%{error: message})
1057 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1058 with %User{} = muted <- User.get_cached_by_id(id),
1059 {:ok, muter} <- User.unmute(muter, muted) do
1061 |> put_view(AccountView)
1062 |> render("relationship.json", %{user: muter, target: muted})
1064 {:error, message} ->
1066 |> put_status(:forbidden)
1067 |> json(%{error: message})
1071 def mutes(%{assigns: %{user: user}} = conn, _) do
1072 with muted_accounts <- User.muted_users(user) do
1073 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1078 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1079 with %User{} = blocked <- User.get_cached_by_id(id),
1080 {:ok, blocker} <- User.block(blocker, blocked),
1081 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1083 |> put_view(AccountView)
1084 |> render("relationship.json", %{user: blocker, target: blocked})
1086 {:error, message} ->
1088 |> put_status(:forbidden)
1089 |> json(%{error: message})
1093 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1094 with %User{} = blocked <- User.get_cached_by_id(id),
1095 {:ok, blocker} <- User.unblock(blocker, blocked),
1096 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1098 |> put_view(AccountView)
1099 |> render("relationship.json", %{user: blocker, target: blocked})
1101 {:error, message} ->
1103 |> put_status(:forbidden)
1104 |> json(%{error: message})
1108 def blocks(%{assigns: %{user: user}} = conn, _) do
1109 with blocked_accounts <- User.blocked_users(user) do
1110 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1115 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1116 json(conn, info.domain_blocks || [])
1119 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1120 User.block_domain(blocker, domain)
1124 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1125 User.unblock_domain(blocker, domain)
1129 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1130 with %User{} = subscription_target <- User.get_cached_by_id(id),
1131 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1133 |> put_view(AccountView)
1134 |> render("relationship.json", %{user: user, target: subscription_target})
1136 {:error, message} ->
1138 |> put_status(:forbidden)
1139 |> json(%{error: message})
1143 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1144 with %User{} = subscription_target <- User.get_cached_by_id(id),
1145 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1147 |> put_view(AccountView)
1148 |> render("relationship.json", %{user: user, target: subscription_target})
1150 {:error, message} ->
1152 |> put_status(:forbidden)
1153 |> json(%{error: message})
1157 def favourites(%{assigns: %{user: user}} = conn, params) do
1160 |> Map.put("type", "Create")
1161 |> Map.put("favorited_by", user.ap_id)
1162 |> Map.put("blocking_user", user)
1165 ActivityPub.fetch_activities([], params)
1169 |> add_link_headers(:favourites, activities)
1170 |> put_view(StatusView)
1171 |> render("index.json", %{activities: activities, for: user, as: :activity})
1174 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1175 with %User{} = user <- User.get_by_id(id),
1176 false <- user.info.hide_favorites do
1179 |> Map.put("type", "Create")
1180 |> Map.put("favorited_by", user.ap_id)
1181 |> Map.put("blocking_user", for_user)
1185 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1187 [Pleroma.Constants.as_public()]
1192 |> ActivityPub.fetch_activities(params)
1196 |> add_link_headers(:favourites, activities)
1197 |> put_view(StatusView)
1198 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1200 nil -> {:error, :not_found}
1201 true -> render_error(conn, :forbidden, "Can't get favorites")
1205 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1206 user = User.get_cached_by_id(user.id)
1209 Bookmark.for_user_query(user.id)
1210 |> Pagination.fetch_paginated(params)
1214 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1217 |> add_link_headers(:bookmarks, bookmarks)
1218 |> put_view(StatusView)
1219 |> render("index.json", %{activities: activities, for: user, as: :activity})
1222 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1223 lists = Pleroma.List.for_user(user, opts)
1224 res = ListView.render("lists.json", lists: lists)
1228 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1229 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1230 res = ListView.render("list.json", list: list)
1233 _e -> render_error(conn, :not_found, "Record not found")
1237 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1238 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1239 res = ListView.render("lists.json", lists: lists)
1243 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1244 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1245 {:ok, _list} <- Pleroma.List.delete(list) do
1249 json(conn, dgettext("errors", "error"))
1253 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1254 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1255 res = ListView.render("list.json", list: list)
1260 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1262 |> Enum.each(fn account_id ->
1263 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1264 %User{} = followed <- User.get_cached_by_id(account_id) do
1265 Pleroma.List.follow(list, followed)
1272 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1274 |> Enum.each(fn account_id ->
1275 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1276 %User{} = followed <- User.get_cached_by_id(account_id) do
1277 Pleroma.List.unfollow(list, followed)
1284 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1285 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1286 {:ok, users} = Pleroma.List.get_following(list) do
1288 |> put_view(AccountView)
1289 |> render("accounts.json", %{for: user, users: users, as: :user})
1293 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1294 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1295 {:ok, list} <- Pleroma.List.rename(list, title) do
1296 res = ListView.render("list.json", list: list)
1300 json(conn, dgettext("errors", "error"))
1304 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1305 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1308 |> Map.put("type", "Create")
1309 |> Map.put("blocking_user", user)
1310 |> Map.put("user", user)
1311 |> Map.put("muting_user", user)
1313 # we must filter the following list for the user to avoid leaking statuses the user
1314 # does not actually have permission to see (for more info, peruse security issue #270).
1317 |> Enum.filter(fn x -> x in user.following end)
1318 |> ActivityPub.fetch_activities_bounded(following, params)
1322 |> put_view(StatusView)
1323 |> render("index.json", %{activities: activities, for: user, as: :activity})
1325 _e -> render_error(conn, :forbidden, "Error.")
1329 def index(%{assigns: %{user: user}} = conn, _params) do
1330 token = get_session(conn, :oauth_token)
1333 mastodon_emoji = mastodonized_emoji()
1335 limit = Config.get([:instance, :limit])
1338 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1343 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1344 access_token: token,
1346 domain: Pleroma.Web.Endpoint.host(),
1349 unfollow_modal: false,
1352 auto_play_gif: false,
1353 display_sensitive_media: false,
1354 reduce_motion: false,
1355 max_toot_chars: limit,
1356 mascot: User.get_mascot(user)["url"]
1358 poll_limits: Config.get([:instance, :poll_limits]),
1360 delete_others_notice: present?(user.info.is_moderator),
1361 admin: present?(user.info.is_admin)
1365 default_privacy: user.info.default_scope,
1366 default_sensitive: false,
1367 allow_content_types: Config.get([:instance, :allowed_post_formats])
1369 media_attachments: %{
1370 accept_content_types: [
1386 user.info.settings ||
1416 push_subscription: nil,
1418 custom_emojis: mastodon_emoji,
1424 |> put_layout(false)
1425 |> put_view(MastodonView)
1426 |> render("index.html", %{initial_state: initial_state})
1429 |> put_session(:return_to, conn.request_path)
1430 |> redirect(to: "/web/login")
1434 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1435 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1437 with changeset <- Ecto.Changeset.change(user),
1438 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1439 {:ok, _user} <- User.update_and_set_cache(changeset) do
1444 |> put_status(:internal_server_error)
1445 |> json(%{error: inspect(e)})
1449 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1450 redirect(conn, to: local_mastodon_root_path(conn))
1453 @doc "Local Mastodon FE login init action"
1454 def login(conn, %{"code" => auth_token}) do
1455 with {:ok, app} <- get_or_make_app(),
1456 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1457 {:ok, token} <- Token.exchange_token(app, auth) do
1459 |> put_session(:oauth_token, token.token)
1460 |> redirect(to: local_mastodon_root_path(conn))
1464 @doc "Local Mastodon FE callback action"
1465 def login(conn, _) do
1466 with {:ok, app} <- get_or_make_app() do
1471 response_type: "code",
1472 client_id: app.client_id,
1474 scope: Enum.join(app.scopes, " ")
1477 redirect(conn, to: path)
1481 defp local_mastodon_root_path(conn) do
1482 case get_session(conn, :return_to) do
1484 mastodon_api_path(conn, :index, ["getting-started"])
1487 delete_session(conn, :return_to)
1492 defp get_or_make_app do
1493 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1494 scopes = ["read", "write", "follow", "push"]
1496 with %App{} = app <- Repo.get_by(App, find_attrs) do
1498 if app.scopes == scopes do
1502 |> Ecto.Changeset.change(%{scopes: scopes})
1510 App.register_changeset(
1512 Map.put(find_attrs, :scopes, scopes)
1519 def logout(conn, _) do
1522 |> redirect(to: "/")
1525 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1526 Logger.debug("Unimplemented, returning unmodified relationship")
1528 with %User{} = target <- User.get_cached_by_id(id) do
1530 |> put_view(AccountView)
1531 |> render("relationship.json", %{user: user, target: target})
1535 def empty_array(conn, _) do
1536 Logger.debug("Unimplemented, returning an empty array")
1540 def empty_object(conn, _) do
1541 Logger.debug("Unimplemented, returning an empty object")
1545 def get_filters(%{assigns: %{user: user}} = conn, _) do
1546 filters = Filter.get_filters(user)
1547 res = FilterView.render("filters.json", filters: filters)
1552 %{assigns: %{user: user}} = conn,
1553 %{"phrase" => phrase, "context" => context} = params
1559 hide: Map.get(params, "irreversible", false),
1560 whole_word: Map.get(params, "boolean", true)
1564 {:ok, response} = Filter.create(query)
1565 res = FilterView.render("filter.json", filter: response)
1569 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1570 filter = Filter.get(filter_id, user)
1571 res = FilterView.render("filter.json", filter: filter)
1576 %{assigns: %{user: user}} = conn,
1577 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1581 filter_id: filter_id,
1584 hide: Map.get(params, "irreversible", nil),
1585 whole_word: Map.get(params, "boolean", true)
1589 {:ok, response} = Filter.update(query)
1590 res = FilterView.render("filter.json", filter: response)
1594 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1597 filter_id: filter_id
1600 {:ok, _} = Filter.delete(query)
1606 def errors(conn, {:error, %Changeset{} = changeset}) do
1609 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1610 |> Enum.map_join(", ", fn {_k, v} -> v end)
1613 |> put_status(:unprocessable_entity)
1614 |> json(%{error: error_message})
1617 def errors(conn, {:error, :not_found}) do
1618 render_error(conn, :not_found, "Record not found")
1621 def errors(conn, {:error, error_message}) do
1623 |> put_status(:bad_request)
1624 |> json(%{error: error_message})
1627 def errors(conn, _) do
1629 |> put_status(:internal_server_error)
1630 |> json(dgettext("errors", "Something went wrong"))
1633 def suggestions(%{assigns: %{user: user}} = conn, _) do
1634 suggestions = Config.get(:suggestions)
1636 if Keyword.get(suggestions, :enabled, false) do
1637 api = Keyword.get(suggestions, :third_party_engine, "")
1638 timeout = Keyword.get(suggestions, :timeout, 5000)
1639 limit = Keyword.get(suggestions, :limit, 23)
1641 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1643 user = user.nickname
1647 |> String.replace("{{host}}", host)
1648 |> String.replace("{{user}}", user)
1650 with {:ok, %{status: 200, body: body}} <-
1651 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1652 {:ok, data} <- Jason.decode(body) do
1655 |> Enum.slice(0, limit)
1658 |> Map.put("id", fetch_suggestion_id(x))
1659 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1660 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1666 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1673 defp fetch_suggestion_id(attrs) do
1674 case User.get_or_fetch(attrs["acct"]) do
1675 {:ok, %User{id: id}} -> id
1680 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1681 with %Activity{} = activity <- Activity.get_by_id(status_id),
1682 true <- Visibility.visible_for_user?(activity, user) do
1686 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1696 def reports(%{assigns: %{user: user}} = conn, params) do
1697 case CommonAPI.report(user, params) do
1700 |> put_view(ReportView)
1701 |> try_render("report.json", %{activity: activity})
1705 |> put_status(:bad_request)
1706 |> json(%{error: err})
1710 def account_register(
1711 %{assigns: %{app: app}} = conn,
1712 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1720 "captcha_answer_data",
1724 |> Map.put("nickname", nickname)
1725 |> Map.put("fullname", params["fullname"] || nickname)
1726 |> Map.put("bio", params["bio"] || "")
1727 |> Map.put("confirm", params["password"])
1729 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1730 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1732 token_type: "Bearer",
1733 access_token: token.token,
1735 created_at: Token.Utils.format_created_at(token)
1740 |> put_status(:bad_request)
1745 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1746 render_error(conn, :bad_request, "Missing parameters")
1749 def account_register(conn, _) do
1750 render_error(conn, :forbidden, "Invalid credentials")
1753 def conversations(%{assigns: %{user: user}} = conn, params) do
1754 participations = Participation.for_user_with_last_activity_id(user, params)
1757 Enum.map(participations, fn participation ->
1758 ConversationView.render("participation.json", %{participation: participation, for: user})
1762 |> add_link_headers(:conversations, participations)
1763 |> json(conversations)
1766 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1767 with %Participation{} = participation <-
1768 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1769 {:ok, participation} <- Participation.mark_as_read(participation) do
1770 participation_view =
1771 ConversationView.render("participation.json", %{participation: participation, for: user})
1774 |> json(participation_view)
1778 def password_reset(conn, params) do
1779 nickname_or_email = params["email"] || params["nickname"]
1781 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1783 |> put_status(:no_content)
1786 {:error, "unknown user"} ->
1787 send_resp(conn, :not_found, "")
1790 send_resp(conn, :bad_request, "")
1794 def account_confirmation_resend(conn, params) do
1795 nickname_or_email = params["email"] || params["nickname"]
1797 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1798 {:ok, _} <- User.try_send_confirmation_email(user) do
1800 |> json_response(:no_content, "")
1804 def try_render(conn, target, params)
1805 when is_binary(target) do
1806 case render(conn, target, params) do
1807 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1812 def try_render(conn, _, _) do
1813 render_error(conn, :not_implemented, "Can't display this activity")
1816 defp present?(nil), do: false
1817 defp present?(false), do: false
1818 defp present?(_), do: true