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 |> put_view(AccountView)
890 |> render("accounts.json", %{for: user, users: users, as: :user})
896 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
897 with %Activity{data: %{"object" => object}} <- Activity.get_by_id(id),
898 %Object{data: %{"announcements" => announces}} <- Object.normalize(object) do
899 q = from(u in User, where: u.ap_id in ^announces)
903 |> put_view(AccountView)
904 |> render("accounts.json", %{for: user, users: users, as: :user})
910 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
911 local_only = params["local"] in [true, "True", "true", "1"]
914 [params["tag"], params["any"]]
918 |> Enum.map(&String.downcase(&1))
923 |> Enum.map(&String.downcase(&1))
928 |> Enum.map(&String.downcase(&1))
932 |> Map.put("type", "Create")
933 |> Map.put("local_only", local_only)
934 |> Map.put("blocking_user", user)
935 |> Map.put("muting_user", user)
936 |> Map.put("tag", tags)
937 |> Map.put("tag_all", tag_all)
938 |> Map.put("tag_reject", tag_reject)
939 |> ActivityPub.fetch_public_activities()
943 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
944 |> put_view(StatusView)
945 |> render("index.json", %{activities: activities, for: user, as: :activity})
948 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
949 with %User{} = user <- User.get_cached_by_id(id),
950 followers <- MastodonAPI.get_followers(user, params) do
953 for_user && user.id == for_user.id -> followers
954 user.info.hide_followers -> []
959 |> add_link_headers(:followers, followers, user)
960 |> put_view(AccountView)
961 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
965 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
966 with %User{} = user <- User.get_cached_by_id(id),
967 followers <- MastodonAPI.get_friends(user, params) do
970 for_user && user.id == for_user.id -> followers
971 user.info.hide_follows -> []
976 |> add_link_headers(:following, followers, user)
977 |> put_view(AccountView)
978 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
982 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
983 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
985 |> put_view(AccountView)
986 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
990 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
991 with %User{} = follower <- User.get_cached_by_id(id),
992 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
994 |> put_view(AccountView)
995 |> render("relationship.json", %{user: followed, target: follower})
999 |> put_status(:forbidden)
1000 |> json(%{error: message})
1004 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1005 with %User{} = follower <- User.get_cached_by_id(id),
1006 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
1008 |> put_view(AccountView)
1009 |> render("relationship.json", %{user: followed, target: follower})
1011 {:error, message} ->
1013 |> put_status(:forbidden)
1014 |> json(%{error: message})
1018 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1019 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1020 {_, true} <- {:followed, follower.id != followed.id},
1021 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
1023 |> put_view(AccountView)
1024 |> render("relationship.json", %{user: follower, target: followed})
1027 {:error, :not_found}
1029 {:error, message} ->
1031 |> put_status(:forbidden)
1032 |> json(%{error: message})
1036 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1037 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1038 {_, true} <- {:followed, follower.id != followed.id},
1039 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1041 |> put_view(AccountView)
1042 |> render("account.json", %{user: followed, for: follower})
1045 {:error, :not_found}
1047 {:error, message} ->
1049 |> put_status(:forbidden)
1050 |> json(%{error: message})
1054 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1055 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1056 {_, true} <- {:followed, follower.id != followed.id},
1057 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1059 |> put_view(AccountView)
1060 |> render("relationship.json", %{user: follower, target: followed})
1063 {:error, :not_found}
1070 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1072 if Map.has_key?(params, "notifications"),
1073 do: params["notifications"] in [true, "True", "true", "1"],
1076 with %User{} = muted <- User.get_cached_by_id(id),
1077 {:ok, muter} <- User.mute(muter, muted, notifications) do
1079 |> put_view(AccountView)
1080 |> render("relationship.json", %{user: muter, target: muted})
1082 {:error, message} ->
1084 |> put_status(:forbidden)
1085 |> json(%{error: message})
1089 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1090 with %User{} = muted <- User.get_cached_by_id(id),
1091 {:ok, muter} <- User.unmute(muter, muted) do
1093 |> put_view(AccountView)
1094 |> render("relationship.json", %{user: muter, target: muted})
1096 {:error, message} ->
1098 |> put_status(:forbidden)
1099 |> json(%{error: message})
1103 def mutes(%{assigns: %{user: user}} = conn, _) do
1104 with muted_accounts <- User.muted_users(user) do
1105 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1110 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1111 with %User{} = blocked <- User.get_cached_by_id(id),
1112 {:ok, blocker} <- User.block(blocker, blocked),
1113 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1115 |> put_view(AccountView)
1116 |> render("relationship.json", %{user: blocker, target: blocked})
1118 {:error, message} ->
1120 |> put_status(:forbidden)
1121 |> json(%{error: message})
1125 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1126 with %User{} = blocked <- User.get_cached_by_id(id),
1127 {:ok, blocker} <- User.unblock(blocker, blocked),
1128 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1130 |> put_view(AccountView)
1131 |> render("relationship.json", %{user: blocker, target: blocked})
1133 {:error, message} ->
1135 |> put_status(:forbidden)
1136 |> json(%{error: message})
1140 def blocks(%{assigns: %{user: user}} = conn, _) do
1141 with blocked_accounts <- User.blocked_users(user) do
1142 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1147 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1148 json(conn, info.domain_blocks || [])
1151 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1152 User.block_domain(blocker, domain)
1156 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1157 User.unblock_domain(blocker, domain)
1161 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1162 with %User{} = subscription_target <- User.get_cached_by_id(id),
1163 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1165 |> put_view(AccountView)
1166 |> render("relationship.json", %{user: user, target: subscription_target})
1168 {:error, message} ->
1170 |> put_status(:forbidden)
1171 |> json(%{error: message})
1175 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1176 with %User{} = subscription_target <- User.get_cached_by_id(id),
1177 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1179 |> put_view(AccountView)
1180 |> render("relationship.json", %{user: user, target: subscription_target})
1182 {:error, message} ->
1184 |> put_status(:forbidden)
1185 |> json(%{error: message})
1189 def favourites(%{assigns: %{user: user}} = conn, params) do
1192 |> Map.put("type", "Create")
1193 |> Map.put("favorited_by", user.ap_id)
1194 |> Map.put("blocking_user", user)
1197 ActivityPub.fetch_activities([], params)
1201 |> add_link_headers(:favourites, activities)
1202 |> put_view(StatusView)
1203 |> render("index.json", %{activities: activities, for: user, as: :activity})
1206 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1207 with %User{} = user <- User.get_by_id(id),
1208 false <- user.info.hide_favorites do
1211 |> Map.put("type", "Create")
1212 |> Map.put("favorited_by", user.ap_id)
1213 |> Map.put("blocking_user", for_user)
1217 ["https://www.w3.org/ns/activitystreams#Public"] ++
1218 [for_user.ap_id | for_user.following]
1220 ["https://www.w3.org/ns/activitystreams#Public"]
1225 |> ActivityPub.fetch_activities(params)
1229 |> add_link_headers(:favourites, activities)
1230 |> put_view(StatusView)
1231 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1233 nil -> {:error, :not_found}
1234 true -> render_error(conn, :forbidden, "Can't get favorites")
1238 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1239 user = User.get_cached_by_id(user.id)
1242 Bookmark.for_user_query(user.id)
1243 |> Pagination.fetch_paginated(params)
1247 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1250 |> add_link_headers(:bookmarks, bookmarks)
1251 |> put_view(StatusView)
1252 |> render("index.json", %{activities: activities, for: user, as: :activity})
1255 def get_lists(%{assigns: %{user: user}} = conn, opts) do
1256 lists = Pleroma.List.for_user(user, opts)
1257 res = ListView.render("lists.json", lists: lists)
1261 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1262 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
1263 res = ListView.render("list.json", list: list)
1266 _e -> render_error(conn, :not_found, "Record not found")
1270 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1271 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1272 res = ListView.render("lists.json", lists: lists)
1276 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1277 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1278 {:ok, _list} <- Pleroma.List.delete(list) do
1282 json(conn, dgettext("errors", "error"))
1286 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
1287 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
1288 res = ListView.render("list.json", list: list)
1293 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1295 |> Enum.each(fn account_id ->
1296 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1297 %User{} = followed <- User.get_cached_by_id(account_id) do
1298 Pleroma.List.follow(list, followed)
1305 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
1307 |> Enum.each(fn account_id ->
1308 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1309 %User{} = followed <- User.get_cached_by_id(account_id) do
1310 Pleroma.List.unfollow(list, followed)
1317 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1318 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1319 {:ok, users} = Pleroma.List.get_following(list) do
1321 |> put_view(AccountView)
1322 |> render("accounts.json", %{for: user, users: users, as: :user})
1326 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
1327 with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
1328 {:ok, list} <- Pleroma.List.rename(list, title) do
1329 res = ListView.render("list.json", list: list)
1333 json(conn, dgettext("errors", "error"))
1337 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1338 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1341 |> Map.put("type", "Create")
1342 |> Map.put("blocking_user", user)
1343 |> Map.put("muting_user", user)
1345 # we must filter the following list for the user to avoid leaking statuses the user
1346 # does not actually have permission to see (for more info, peruse security issue #270).
1349 |> Enum.filter(fn x -> x in user.following end)
1350 |> ActivityPub.fetch_activities_bounded(following, params)
1354 |> put_view(StatusView)
1355 |> render("index.json", %{activities: activities, for: user, as: :activity})
1357 _e -> render_error(conn, :forbidden, "Error.")
1361 def index(%{assigns: %{user: user}} = conn, _params) do
1362 token = get_session(conn, :oauth_token)
1365 mastodon_emoji = mastodonized_emoji()
1367 limit = Config.get([:instance, :limit])
1370 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1375 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1376 access_token: token,
1378 domain: Pleroma.Web.Endpoint.host(),
1381 unfollow_modal: false,
1384 auto_play_gif: false,
1385 display_sensitive_media: false,
1386 reduce_motion: false,
1387 max_toot_chars: limit,
1388 mascot: User.get_mascot(user)["url"]
1390 poll_limits: Config.get([:instance, :poll_limits]),
1392 delete_others_notice: present?(user.info.is_moderator),
1393 admin: present?(user.info.is_admin)
1397 default_privacy: user.info.default_scope,
1398 default_sensitive: false,
1399 allow_content_types: Config.get([:instance, :allowed_post_formats])
1401 media_attachments: %{
1402 accept_content_types: [
1418 user.info.settings ||
1448 push_subscription: nil,
1450 custom_emojis: mastodon_emoji,
1456 |> put_layout(false)
1457 |> put_view(MastodonView)
1458 |> render("index.html", %{initial_state: initial_state})
1461 |> put_session(:return_to, conn.request_path)
1462 |> redirect(to: "/web/login")
1466 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1467 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1469 with changeset <- Ecto.Changeset.change(user),
1470 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng),
1471 {:ok, _user} <- User.update_and_set_cache(changeset) do
1476 |> put_status(:internal_server_error)
1477 |> json(%{error: inspect(e)})
1481 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1482 redirect(conn, to: local_mastodon_root_path(conn))
1485 @doc "Local Mastodon FE login init action"
1486 def login(conn, %{"code" => auth_token}) do
1487 with {:ok, app} <- get_or_make_app(),
1488 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1489 {:ok, token} <- Token.exchange_token(app, auth) do
1491 |> put_session(:oauth_token, token.token)
1492 |> redirect(to: local_mastodon_root_path(conn))
1496 @doc "Local Mastodon FE callback action"
1497 def login(conn, _) do
1498 with {:ok, app} <- get_or_make_app() do
1503 response_type: "code",
1504 client_id: app.client_id,
1506 scope: Enum.join(app.scopes, " ")
1509 redirect(conn, to: path)
1513 defp local_mastodon_root_path(conn) do
1514 case get_session(conn, :return_to) do
1516 mastodon_api_path(conn, :index, ["getting-started"])
1519 delete_session(conn, :return_to)
1524 defp get_or_make_app do
1525 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1526 scopes = ["read", "write", "follow", "push"]
1528 with %App{} = app <- Repo.get_by(App, find_attrs) do
1530 if app.scopes == scopes do
1534 |> Ecto.Changeset.change(%{scopes: scopes})
1542 App.register_changeset(
1544 Map.put(find_attrs, :scopes, scopes)
1551 def logout(conn, _) do
1554 |> redirect(to: "/")
1557 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1558 Logger.debug("Unimplemented, returning unmodified relationship")
1560 with %User{} = target <- User.get_cached_by_id(id) do
1562 |> put_view(AccountView)
1563 |> render("relationship.json", %{user: user, target: target})
1567 def empty_array(conn, _) do
1568 Logger.debug("Unimplemented, returning an empty array")
1572 def empty_object(conn, _) do
1573 Logger.debug("Unimplemented, returning an empty object")
1577 def get_filters(%{assigns: %{user: user}} = conn, _) do
1578 filters = Filter.get_filters(user)
1579 res = FilterView.render("filters.json", filters: filters)
1584 %{assigns: %{user: user}} = conn,
1585 %{"phrase" => phrase, "context" => context} = params
1591 hide: Map.get(params, "irreversible", false),
1592 whole_word: Map.get(params, "boolean", true)
1596 {:ok, response} = Filter.create(query)
1597 res = FilterView.render("filter.json", filter: response)
1601 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1602 filter = Filter.get(filter_id, user)
1603 res = FilterView.render("filter.json", filter: filter)
1608 %{assigns: %{user: user}} = conn,
1609 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1613 filter_id: filter_id,
1616 hide: Map.get(params, "irreversible", nil),
1617 whole_word: Map.get(params, "boolean", true)
1621 {:ok, response} = Filter.update(query)
1622 res = FilterView.render("filter.json", filter: response)
1626 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1629 filter_id: filter_id
1632 {:ok, _} = Filter.delete(query)
1638 def errors(conn, {:error, %Changeset{} = changeset}) do
1641 |> Changeset.traverse_errors(fn {message, _opt} -> message end)
1642 |> Enum.map_join(", ", fn {_k, v} -> v end)
1645 |> put_status(:unprocessable_entity)
1646 |> json(%{error: error_message})
1649 def errors(conn, {:error, :not_found}) do
1650 render_error(conn, :not_found, "Record not found")
1653 def errors(conn, {:error, error_message}) do
1655 |> put_status(:bad_request)
1656 |> json(%{error: error_message})
1659 def errors(conn, _) do
1661 |> put_status(:internal_server_error)
1662 |> json(dgettext("errors", "Something went wrong"))
1665 def suggestions(%{assigns: %{user: user}} = conn, _) do
1666 suggestions = Config.get(:suggestions)
1668 if Keyword.get(suggestions, :enabled, false) do
1669 api = Keyword.get(suggestions, :third_party_engine, "")
1670 timeout = Keyword.get(suggestions, :timeout, 5000)
1671 limit = Keyword.get(suggestions, :limit, 23)
1673 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1675 user = user.nickname
1679 |> String.replace("{{host}}", host)
1680 |> String.replace("{{user}}", user)
1682 with {:ok, %{status: 200, body: body}} <-
1687 recv_timeout: timeout,
1691 {:ok, data} <- Jason.decode(body) do
1694 |> Enum.slice(0, limit)
1699 case User.get_or_fetch(x["acct"]) do
1700 {:ok, %User{id: id}} -> id
1706 Map.put(x, "avatar", MediaProxy.url(x["avatar"]))
1709 Map.put(x, "avatar_static", MediaProxy.url(x["avatar_static"]))
1715 e -> Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1722 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1723 with %Activity{} = activity <- Activity.get_by_id(status_id),
1724 true <- Visibility.visible_for_user?(activity, user) do
1728 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1738 def reports(%{assigns: %{user: user}} = conn, params) do
1739 case CommonAPI.report(user, params) do
1742 |> put_view(ReportView)
1743 |> try_render("report.json", %{activity: activity})
1747 |> put_status(:bad_request)
1748 |> json(%{error: err})
1752 def account_register(
1753 %{assigns: %{app: app}} = conn,
1754 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1762 "captcha_answer_data",
1766 |> Map.put("nickname", nickname)
1767 |> Map.put("fullname", params["fullname"] || nickname)
1768 |> Map.put("bio", params["bio"] || "")
1769 |> Map.put("confirm", params["password"])
1771 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1772 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1774 token_type: "Bearer",
1775 access_token: token.token,
1777 created_at: Token.Utils.format_created_at(token)
1782 |> put_status(:bad_request)
1787 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1788 render_error(conn, :bad_request, "Missing parameters")
1791 def account_register(conn, _) do
1792 render_error(conn, :forbidden, "Invalid credentials")
1795 def conversations(%{assigns: %{user: user}} = conn, params) do
1796 participations = Participation.for_user_with_last_activity_id(user, params)
1799 Enum.map(participations, fn participation ->
1800 ConversationView.render("participation.json", %{participation: participation, user: user})
1804 |> add_link_headers(:conversations, participations)
1805 |> json(conversations)
1808 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1809 with %Participation{} = participation <-
1810 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1811 {:ok, participation} <- Participation.mark_as_read(participation) do
1812 participation_view =
1813 ConversationView.render("participation.json", %{participation: participation, user: user})
1816 |> json(participation_view)
1820 def password_reset(conn, params) do
1821 nickname_or_email = params["email"] || params["nickname"]
1823 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1825 |> put_status(:no_content)
1828 {:error, "unknown user"} ->
1829 send_resp(conn, :not_found, "")
1832 send_resp(conn, :bad_request, "")
1836 def try_render(conn, target, params)
1837 when is_binary(target) do
1838 case render(conn, target, params) do
1839 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1844 def try_render(conn, _, _) do
1845 render_error(conn, :not_implemented, "Can't display this activity")
1848 defp present?(nil), do: false
1849 defp present?(false), do: false
1850 defp present?(_), do: true