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.OAuthScopesPlug
23 alias Pleroma.Plugs.RateLimiter
25 alias Pleroma.ScheduledActivity
29 alias Pleroma.Web.ActivityPub.ActivityPub
30 alias Pleroma.Web.ActivityPub.Visibility
31 alias Pleroma.Web.CommonAPI
32 alias Pleroma.Web.MastodonAPI.AccountView
33 alias Pleroma.Web.MastodonAPI.AppView
34 alias Pleroma.Web.MastodonAPI.ConversationView
35 alias Pleroma.Web.MastodonAPI.FilterView
36 alias Pleroma.Web.MastodonAPI.ListView
37 alias Pleroma.Web.MastodonAPI.MastodonAPI
38 alias Pleroma.Web.MastodonAPI.MastodonView
39 alias Pleroma.Web.MastodonAPI.NotificationView
40 alias Pleroma.Web.MastodonAPI.ReportView
41 alias Pleroma.Web.MastodonAPI.ScheduledActivityView
42 alias Pleroma.Web.MastodonAPI.StatusView
43 alias Pleroma.Web.MediaProxy
44 alias Pleroma.Web.OAuth.App
45 alias Pleroma.Web.OAuth.Authorization
46 alias Pleroma.Web.OAuth.Scopes
47 alias Pleroma.Web.OAuth.Token
48 alias Pleroma.Web.TwitterAPI.TwitterAPI
50 alias Pleroma.Web.ControllerHelper
54 require Pleroma.Constants
56 plug(Pleroma.Plugs.EnsurePublicOrAuthenticatedPlug when action != :index)
58 @unauthenticated_access %{fallback: :proceed_unauthenticated, scopes: []}
62 %{scopes: ["read"], skip_instance_privacy_check: true} when action == :index
67 %{scopes: ["read"]} when action in [:suggestions, :verify_app_credentials]
72 %{scopes: ["write:accounts"]}
73 # Note: the following actions are not permission-secured in Mastodon:
85 %{scopes: ["write:accounts"]}
86 when action in [:pin_status, :unpin_status, :update_credentials]
91 %{scopes: ["read:statuses"]}
95 :show_scheduled_status,
103 %{@unauthenticated_access | scopes: ["read:statuses"]}
104 when action in [:user_statuses, :get_status, :get_context, :status_card, :get_poll]
109 %{scopes: ["write:statuses"]}
111 :update_scheduled_status,
112 :delete_scheduled_status,
121 plug(OAuthScopesPlug, %{scopes: ["write:conversations"]} when action == :conversation_read)
125 %{scopes: ["read:accounts"]}
126 when action in [:endorsements, :verify_credentials, :followers, :following, :get_mascot]
131 %{@unauthenticated_access | scopes: ["read:accounts"]}
132 when action in [:user, :favourited_by, :reblogged_by]
137 %{scopes: ["read:favourites"]} when action in [:favourites, :user_favourites]
142 %{scopes: ["write:favourites"]} when action in [:fav_status, :unfav_status]
145 plug(OAuthScopesPlug, %{scopes: ["read:filters"]} when action in [:get_filters, :get_filter])
149 %{scopes: ["write:filters"]} when action in [:create_filter, :update_filter, :delete_filter]
152 plug(OAuthScopesPlug, %{scopes: ["read:lists"]} when action in [:account_lists, :list_timeline])
154 plug(OAuthScopesPlug, %{scopes: ["write:media"]} when action in [:upload, :update_media])
158 %{scopes: ["read:notifications"]} when action in [:notifications, :get_notification]
163 %{scopes: ["write:notifications"]}
164 when action in [:clear_notifications, :dismiss_notification, :destroy_multiple_notifications]
169 %{scopes: ["write:reports"]}
170 when action in [:create_report, :report_update_state, :report_respond]
175 %{scopes: ["follow", "read:blocks"]} when action in [:blocks, :domain_blocks]
180 %{scopes: ["follow", "write:blocks"]}
181 when action in [:block, :unblock, :block_domain, :unblock_domain]
184 plug(OAuthScopesPlug, %{scopes: ["read:follows"]} when action == :relationships)
185 plug(OAuthScopesPlug, %{scopes: ["follow", "read:follows"]} when action == :follow_requests)
189 %{scopes: ["follow", "write:follows"]}
195 :authorize_follow_request,
196 :reject_follow_request
200 plug(OAuthScopesPlug, %{scopes: ["follow", "read:mutes"]} when action == :mutes)
201 plug(OAuthScopesPlug, %{scopes: ["follow", "write:mutes"]} when action in [:mute, :unmute])
205 %{scopes: ["write:mutes"]} when action in [:mute_conversation, :unmute_conversation]
208 # Note: scopes not present in Mastodon: read:bookmarks, write:bookmarks
209 plug(OAuthScopesPlug, %{scopes: ["read:bookmarks"]} when action == :bookmarks)
213 %{scopes: ["write:bookmarks"]} when action in [:bookmark_status, :unbookmark_status]
216 @rate_limited_relations_actions ~w(follow unfollow)a
218 @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status
219 post_status delete_status)a
223 {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]}
224 when action in ~w(reblog_status unreblog_status)a
229 {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]}
230 when action in ~w(fav_status unfav_status)a
235 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions
238 plug(RateLimiter, :relations_actions when action in @rate_limited_relations_actions)
239 plug(RateLimiter, :statuses_actions when action in @rate_limited_status_actions)
240 plug(RateLimiter, :app_account_creation when action == :account_register)
241 plug(RateLimiter, :search when action in [:search, :search2, :account_search])
242 plug(RateLimiter, :password_reset when action == :password_reset)
243 plug(RateLimiter, :account_confirmation_resend when action == :account_confirmation_resend)
245 @local_mastodon_name "Mastodon-Local"
247 action_fallback(Pleroma.Web.MastodonAPI.FallbackController)
249 def create_app(conn, params) do
250 scopes = Scopes.fetch_scopes(params, ["read"])
254 |> Map.drop(["scope", "scopes"])
255 |> Map.put("scopes", scopes)
257 with cs <- App.register_changeset(%App{}, app_attrs),
258 false <- cs.changes[:client_name] == @local_mastodon_name,
259 {:ok, app} <- Repo.insert(cs) do
262 |> render("show.json", %{app: app})
271 value_function \\ fn x -> {:ok, x} end
273 if Map.has_key?(params, params_field) do
274 case value_function.(params[params_field]) do
275 {:ok, new_value} -> Map.put(map, map_field, new_value)
283 def update_credentials(%{assigns: %{user: user}} = conn, params) do
288 |> add_if_present(params, "display_name", :name)
289 |> add_if_present(params, "note", :bio, fn value -> {:ok, User.parse_bio(value, user)} end)
290 |> add_if_present(params, "avatar", :avatar, fn value ->
291 with %Plug.Upload{} <- value,
292 {:ok, object} <- ActivityPub.upload(value, type: :avatar) do
299 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "")
303 |> Map.get(:emoji, [])
304 |> Enum.concat(Formatter.get_emoji_map(emojis_text))
315 :skip_thread_containment
317 |> Enum.reduce(%{}, fn key, acc ->
318 add_if_present(acc, params, to_string(key), key, fn value ->
319 {:ok, ControllerHelper.truthy_param?(value)}
322 |> add_if_present(params, "default_scope", :default_scope)
323 |> add_if_present(params, "fields", :fields, fn fields ->
324 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end)
328 |> add_if_present(params, "fields", :raw_fields)
329 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value ->
330 {:ok, Map.merge(user.info.pleroma_settings_store, value)}
332 |> add_if_present(params, "header", :banner, fn value ->
333 with %Plug.Upload{} <- value,
334 {:ok, object} <- ActivityPub.upload(value, type: :banner) do
340 |> add_if_present(params, "pleroma_background_image", :background, fn value ->
341 with %Plug.Upload{} <- value,
342 {:ok, object} <- ActivityPub.upload(value, type: :background) do
348 |> Map.put(:emoji, user_info_emojis)
350 info_cng = User.Info.profile_update(user.info, info_params)
352 with changeset <- User.update_changeset(user, user_params),
353 changeset <- Changeset.put_embed(changeset, :info, info_cng),
354 {:ok, user} <- User.update_and_set_cache(changeset) do
355 if original_user != user do
356 CommonAPI.update(user)
361 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true})
364 _e -> render_error(conn, :forbidden, "Invalid request")
368 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
369 change = Changeset.change(user, %{avatar: nil})
370 {:ok, user} = User.update_and_set_cache(change)
371 CommonAPI.update(user)
373 json(conn, %{url: nil})
376 def update_avatar(%{assigns: %{user: user}} = conn, params) do
377 {:ok, object} = ActivityPub.upload(params, type: :avatar)
378 change = Changeset.change(user, %{avatar: object.data})
379 {:ok, user} = User.update_and_set_cache(change)
380 CommonAPI.update(user)
381 %{"url" => [%{"href" => href} | _]} = object.data
383 json(conn, %{url: href})
386 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do
387 with new_info <- %{"banner" => %{}},
388 info_cng <- User.Info.profile_update(user.info, new_info),
389 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
390 {:ok, user} <- User.update_and_set_cache(changeset) do
391 CommonAPI.update(user)
393 json(conn, %{url: nil})
397 def update_banner(%{assigns: %{user: user}} = conn, params) do
398 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner),
399 new_info <- %{"banner" => object.data},
400 info_cng <- User.Info.profile_update(user.info, new_info),
401 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
402 {:ok, user} <- User.update_and_set_cache(changeset) do
403 CommonAPI.update(user)
404 %{"url" => [%{"href" => href} | _]} = object.data
406 json(conn, %{url: href})
410 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do
411 with new_info <- %{"background" => %{}},
412 info_cng <- User.Info.profile_update(user.info, new_info),
413 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
414 {:ok, _user} <- User.update_and_set_cache(changeset) do
415 json(conn, %{url: nil})
419 def update_background(%{assigns: %{user: user}} = conn, params) do
420 with {:ok, object} <- ActivityPub.upload(params, type: :background),
421 new_info <- %{"background" => object.data},
422 info_cng <- User.Info.profile_update(user.info, new_info),
423 changeset <- Changeset.change(user) |> Changeset.put_embed(:info, info_cng),
424 {:ok, _user} <- User.update_and_set_cache(changeset) do
425 %{"url" => [%{"href" => href} | _]} = object.data
427 json(conn, %{url: href})
431 def verify_credentials(%{assigns: %{user: user}} = conn, _) do
432 chat_token = Phoenix.Token.sign(conn, "user socket", user.id)
435 AccountView.render("account.json", %{
438 with_pleroma_settings: true,
439 with_chat_token: chat_token
445 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do
446 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do
449 |> render("short.json", %{app: app})
453 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do
454 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id, for: for_user),
455 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do
456 account = AccountView.render("account.json", %{user: user, for: for_user})
459 _e -> render_error(conn, :not_found, "Can't find user")
463 @mastodon_api_level "2.7.2"
465 def masto_instance(conn, _params) do
466 instance = Config.get(:instance)
470 title: Keyword.get(instance, :name),
471 description: Keyword.get(instance, :description),
472 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})",
473 email: Keyword.get(instance, :email),
475 streaming_api: Pleroma.Web.Endpoint.websocket_url()
477 stats: Stats.get_stats(),
478 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
480 registrations: Pleroma.Config.get([:instance, :registrations_open]),
481 # Extra (not present in Mastodon):
482 max_toot_chars: Keyword.get(instance, :limit),
483 poll_limits: Keyword.get(instance, :poll_limits)
489 def peers(conn, _params) do
490 json(conn, Stats.get_peers())
493 defp mastodonized_emoji do
494 Pleroma.Emoji.get_all()
495 |> Enum.map(fn {shortcode, relative_url, tags} ->
496 url = to_string(URI.merge(Web.base_url(), relative_url))
499 "shortcode" => shortcode,
501 "visible_in_picker" => true,
504 # Assuming that a comma is authorized in the category name
505 "category" => (tags -- ["Custom"]) |> Enum.join(",")
510 def custom_emojis(conn, _params) do
511 mastodon_emoji = mastodonized_emoji()
512 json(conn, mastodon_emoji)
515 def home_timeline(%{assigns: %{user: user}} = conn, params) do
518 |> Map.put("type", ["Create", "Announce"])
519 |> Map.put("blocking_user", user)
520 |> Map.put("muting_user", user)
521 |> Map.put("user", user)
524 [user.ap_id | user.following]
525 |> ActivityPub.fetch_activities(params)
529 |> add_link_headers(:home_timeline, activities)
530 |> put_view(StatusView)
531 |> render("index.json", %{activities: activities, for: user, as: :activity})
534 def public_timeline(%{assigns: %{user: user}} = conn, params) do
535 local_only = params["local"] in [true, "True", "true", "1"]
539 |> Map.put("type", ["Create", "Announce"])
540 |> Map.put("local_only", local_only)
541 |> Map.put("blocking_user", user)
542 |> Map.put("muting_user", user)
543 |> Map.put("user", user)
544 |> ActivityPub.fetch_public_activities()
548 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only})
549 |> put_view(StatusView)
550 |> render("index.json", %{activities: activities, for: user, as: :activity})
553 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
554 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"], for: reading_user) do
557 |> Map.put("tag", params["tagged"])
559 activities = ActivityPub.fetch_user_activities(user, reading_user, params)
562 |> add_link_headers(:user_statuses, activities, params["id"])
563 |> put_view(StatusView)
564 |> render("index.json", %{
565 activities: activities,
572 def dm_timeline(%{assigns: %{user: user}} = conn, params) do
575 |> Map.put("type", "Create")
576 |> Map.put("blocking_user", user)
577 |> Map.put("user", user)
578 |> Map.put(:visibility, "direct")
582 |> ActivityPub.fetch_activities_query(params)
583 |> Pagination.fetch_paginated(params)
586 |> add_link_headers(:dm_timeline, activities)
587 |> put_view(StatusView)
588 |> render("index.json", %{activities: activities, for: user, as: :activity})
591 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
592 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
593 true <- Visibility.visible_for_user?(activity, user) do
595 |> put_view(StatusView)
596 |> try_render("status.json", %{activity: activity, for: user})
600 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
601 with %Activity{} = activity <- Activity.get_by_id(id),
603 ActivityPub.fetch_activities_for_context(activity.data["context"], %{
604 "blocking_user" => user,
606 "exclude_id" => activity.id
608 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
614 activities: grouped_activities[true] || [],
618 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
623 activities: grouped_activities[false] || [],
627 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart
634 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do
635 with %Object{} = object <- Object.get_by_id(id),
636 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
637 true <- Visibility.visible_for_user?(activity, user) do
639 |> put_view(StatusView)
640 |> try_render("poll.json", %{object: object, for: user})
642 error when is_nil(error) or error == false ->
643 render_error(conn, :not_found, "Record not found")
647 defp get_cached_vote_or_vote(user, object, choices) do
648 idempotency_key = "polls:#{user.id}:#{object.data["id"]}"
651 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ ->
652 case CommonAPI.vote(user, object, choices) do
653 {:error, _message} = res -> {:ignore, res}
654 res -> {:commit, res}
661 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do
662 with %Object{} = object <- Object.get_by_id(id),
663 true <- object.data["type"] == "Question",
664 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]),
665 true <- Visibility.visible_for_user?(activity, user),
666 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do
668 |> put_view(StatusView)
669 |> try_render("poll.json", %{object: object, for: user})
672 render_error(conn, :not_found, "Record not found")
675 render_error(conn, :not_found, "Record not found")
679 |> put_status(:unprocessable_entity)
680 |> json(%{error: message})
684 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do
685 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do
687 |> add_link_headers(:scheduled_statuses, scheduled_activities)
688 |> put_view(ScheduledActivityView)
689 |> render("index.json", %{scheduled_activities: scheduled_activities})
693 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
694 with %ScheduledActivity{} = scheduled_activity <-
695 ScheduledActivity.get(user, scheduled_activity_id) do
697 |> put_view(ScheduledActivityView)
698 |> render("show.json", %{scheduled_activity: scheduled_activity})
700 _ -> {:error, :not_found}
704 def update_scheduled_status(
705 %{assigns: %{user: user}} = conn,
706 %{"id" => scheduled_activity_id} = params
708 with %ScheduledActivity{} = scheduled_activity <-
709 ScheduledActivity.get(user, scheduled_activity_id),
710 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do
712 |> put_view(ScheduledActivityView)
713 |> render("show.json", %{scheduled_activity: scheduled_activity})
715 nil -> {:error, :not_found}
720 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do
721 with %ScheduledActivity{} = scheduled_activity <-
722 ScheduledActivity.get(user, scheduled_activity_id),
723 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do
725 |> put_view(ScheduledActivityView)
726 |> render("show.json", %{scheduled_activity: scheduled_activity})
728 nil -> {:error, :not_found}
733 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
736 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
738 scheduled_at = params["scheduled_at"]
740 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do
741 with {:ok, scheduled_activity} <-
742 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do
744 |> put_view(ScheduledActivityView)
745 |> render("show.json", %{scheduled_activity: scheduled_activity})
748 params = Map.drop(params, ["scheduled_at"])
750 case CommonAPI.post(user, params) do
753 |> put_status(:unprocessable_entity)
754 |> json(%{error: message})
758 |> put_view(StatusView)
759 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
764 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
765 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
768 _e -> render_error(conn, :forbidden, "Can't delete this post")
772 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
773 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user),
774 %Activity{} = announce <- Activity.normalize(announce.data) do
776 |> put_view(StatusView)
777 |> try_render("status.json", %{activity: announce, for: user, as: :activity})
781 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
782 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user),
783 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do
785 |> put_view(StatusView)
786 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
790 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
791 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user),
792 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
794 |> put_view(StatusView)
795 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
799 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
800 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user),
801 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do
803 |> put_view(StatusView)
804 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
808 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
809 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do
811 |> put_view(StatusView)
812 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
816 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
817 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do
819 |> put_view(StatusView)
820 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
824 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
825 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
826 %User{} = user <- User.get_cached_by_nickname(user.nickname),
827 true <- Visibility.visible_for_user?(activity, user),
828 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do
830 |> put_view(StatusView)
831 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
835 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
836 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
837 %User{} = user <- User.get_cached_by_nickname(user.nickname),
838 true <- Visibility.visible_for_user?(activity, user),
839 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do
841 |> put_view(StatusView)
842 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
846 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
847 activity = Activity.get_by_id(id)
849 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do
851 |> put_view(StatusView)
852 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
856 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do
857 activity = Activity.get_by_id(id)
859 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do
861 |> put_view(StatusView)
862 |> try_render("status.json", %{activity: activity, for: user, as: :activity})
866 def notifications(%{assigns: %{user: user}} = conn, params) do
867 notifications = MastodonAPI.get_notifications(user, params)
870 |> add_link_headers(:notifications, notifications)
871 |> put_view(NotificationView)
872 |> render("index.json", %{notifications: notifications, for: user})
875 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
876 with {:ok, notification} <- Notification.get(user, id) do
878 |> put_view(NotificationView)
879 |> render("show.json", %{notification: notification, for: user})
883 |> put_status(:forbidden)
884 |> json(%{"error" => reason})
888 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
889 Notification.clear(user)
893 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
894 with {:ok, _notif} <- Notification.dismiss(user, id) do
899 |> put_status(:forbidden)
900 |> json(%{"error" => reason})
904 def destroy_multiple_notifications(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do
905 Notification.destroy_multiple(user, ids)
909 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
911 q = from(u in User, where: u.id in ^id)
912 targets = Repo.all(q)
915 |> put_view(AccountView)
916 |> render("relationships.json", %{user: user, targets: targets})
919 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array.
920 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, [])
922 def update_media(%{assigns: %{user: user}} = conn, data) do
923 with %Object{} = object <- Repo.get(Object, data["id"]),
924 true <- Object.authorize_mutation(object, user),
925 true <- is_binary(data["description"]),
926 description <- data["description"] do
927 new_data = %{object.data | "name" => description}
931 |> Object.change(%{data: new_data})
934 attachment_data = Map.put(new_data, "id", object.id)
937 |> put_view(StatusView)
938 |> render("attachment.json", %{attachment: attachment_data})
942 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do
943 with {:ok, object} <-
946 actor: User.ap_id(user),
947 description: Map.get(data, "description")
949 attachment_data = Map.put(object.data, "id", object.id)
952 |> put_view(StatusView)
953 |> render("attachment.json", %{attachment: attachment_data})
957 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do
958 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)),
959 %{} = attachment_data <- Map.put(object.data, "id", object.id),
960 %{type: type} = rendered <-
961 StatusView.render("attachment.json", %{attachment: attachment_data}) do
962 # Reject if not an image
963 if type == "image" do
965 # Save to the user's info
966 info_changeset = User.Info.mascot_update(user.info, rendered)
970 |> Changeset.change()
971 |> Changeset.put_embed(:info, info_changeset)
973 {:ok, _user} = User.update_and_set_cache(user_changeset)
978 render_error(conn, :unsupported_media_type, "mascots can only be images")
983 def get_mascot(%{assigns: %{user: user}} = conn, _params) do
984 mascot = User.get_mascot(user)
990 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
991 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
992 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do
993 q = from(u in User, where: u.ap_id in ^likes)
997 |> Enum.filter(&(not User.blocks?(user, &1)))
1000 |> put_view(AccountView)
1001 |> render("accounts.json", %{for: user, users: users, as: :user})
1007 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1008 with %Activity{} = activity <- Activity.get_by_id_with_object(id),
1009 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do
1010 q = from(u in User, where: u.ap_id in ^announces)
1014 |> Enum.filter(&(not User.blocks?(user, &1)))
1017 |> put_view(AccountView)
1018 |> render("accounts.json", %{for: user, users: users, as: :user})
1024 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
1025 local_only = params["local"] in [true, "True", "true", "1"]
1028 [params["tag"], params["any"]]
1031 |> Enum.filter(& &1)
1032 |> Enum.map(&String.downcase(&1))
1037 |> Enum.map(&String.downcase(&1))
1042 |> Enum.map(&String.downcase(&1))
1046 |> Map.put("type", "Create")
1047 |> Map.put("local_only", local_only)
1048 |> Map.put("blocking_user", user)
1049 |> Map.put("muting_user", user)
1050 |> Map.put("user", user)
1051 |> Map.put("tag", tags)
1052 |> Map.put("tag_all", tag_all)
1053 |> Map.put("tag_reject", tag_reject)
1054 |> ActivityPub.fetch_public_activities()
1058 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only})
1059 |> put_view(StatusView)
1060 |> render("index.json", %{activities: activities, for: user, as: :activity})
1063 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1064 with %User{} = user <- User.get_cached_by_id(id),
1065 followers <- MastodonAPI.get_followers(user, params) do
1068 for_user && user.id == for_user.id -> followers
1069 user.info.hide_followers -> []
1074 |> add_link_headers(:followers, followers, user)
1075 |> put_view(AccountView)
1076 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
1080 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1081 with %User{} = user <- User.get_cached_by_id(id),
1082 followers <- MastodonAPI.get_friends(user, params) do
1085 for_user && user.id == for_user.id -> followers
1086 user.info.hide_follows -> []
1091 |> add_link_headers(:following, followers, user)
1092 |> put_view(AccountView)
1093 |> render("accounts.json", %{for: for_user, users: followers, as: :user})
1097 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do
1098 with {:ok, follow_requests} <- User.get_follow_requests(followed) do
1100 |> put_view(AccountView)
1101 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user})
1105 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1106 with %User{} = follower <- User.get_cached_by_id(id),
1107 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do
1109 |> put_view(AccountView)
1110 |> render("relationship.json", %{user: followed, target: follower})
1112 {:error, message} ->
1114 |> put_status(:forbidden)
1115 |> json(%{error: message})
1119 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do
1120 with %User{} = follower <- User.get_cached_by_id(id),
1121 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do
1123 |> put_view(AccountView)
1124 |> render("relationship.json", %{user: followed, target: follower})
1126 {:error, message} ->
1128 |> put_status(:forbidden)
1129 |> json(%{error: message})
1133 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1134 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1135 {_, true} <- {:followed, follower.id != followed.id},
1136 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do
1138 |> put_view(AccountView)
1139 |> render("relationship.json", %{user: follower, target: followed})
1142 {:error, :not_found}
1144 {:error, message} ->
1146 |> put_status(:forbidden)
1147 |> json(%{error: message})
1151 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
1152 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)},
1153 {_, true} <- {:followed, follower.id != followed.id},
1154 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do
1156 |> put_view(AccountView)
1157 |> render("account.json", %{user: followed, for: follower})
1160 {:error, :not_found}
1162 {:error, message} ->
1164 |> put_status(:forbidden)
1165 |> json(%{error: message})
1169 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
1170 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)},
1171 {_, true} <- {:followed, follower.id != followed.id},
1172 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do
1174 |> put_view(AccountView)
1175 |> render("relationship.json", %{user: follower, target: followed})
1178 {:error, :not_found}
1185 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do
1187 if Map.has_key?(params, "notifications"),
1188 do: params["notifications"] in [true, "True", "true", "1"],
1191 with %User{} = muted <- User.get_cached_by_id(id),
1192 {:ok, muter} <- User.mute(muter, muted, notifications) do
1194 |> put_view(AccountView)
1195 |> render("relationship.json", %{user: muter, target: muted})
1197 {:error, message} ->
1199 |> put_status(:forbidden)
1200 |> json(%{error: message})
1204 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do
1205 with %User{} = muted <- User.get_cached_by_id(id),
1206 {:ok, muter} <- User.unmute(muter, muted) do
1208 |> put_view(AccountView)
1209 |> render("relationship.json", %{user: muter, target: muted})
1211 {:error, message} ->
1213 |> put_status(:forbidden)
1214 |> json(%{error: message})
1218 def mutes(%{assigns: %{user: user}} = conn, _) do
1219 with muted_accounts <- User.muted_users(user) do
1220 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user)
1225 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1226 with %User{} = blocked <- User.get_cached_by_id(id),
1227 {:ok, blocker} <- User.block(blocker, blocked),
1228 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
1230 |> put_view(AccountView)
1231 |> render("relationship.json", %{user: blocker, target: blocked})
1233 {:error, message} ->
1235 |> put_status(:forbidden)
1236 |> json(%{error: message})
1240 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
1241 with %User{} = blocked <- User.get_cached_by_id(id),
1242 {:ok, blocker} <- User.unblock(blocker, blocked),
1243 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do
1245 |> put_view(AccountView)
1246 |> render("relationship.json", %{user: blocker, target: blocked})
1248 {:error, message} ->
1250 |> put_status(:forbidden)
1251 |> json(%{error: message})
1255 def blocks(%{assigns: %{user: user}} = conn, _) do
1256 with blocked_accounts <- User.blocked_users(user) do
1257 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user)
1262 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do
1263 json(conn, info.domain_blocks || [])
1266 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1267 User.block_domain(blocker, domain)
1271 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do
1272 User.unblock_domain(blocker, domain)
1276 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1277 with %User{} = subscription_target <- User.get_cached_by_id(id),
1278 {:ok, subscription_target} = User.subscribe(user, subscription_target) do
1280 |> put_view(AccountView)
1281 |> render("relationship.json", %{user: user, target: subscription_target})
1283 {:error, message} ->
1285 |> put_status(:forbidden)
1286 |> json(%{error: message})
1290 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1291 with %User{} = subscription_target <- User.get_cached_by_id(id),
1292 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do
1294 |> put_view(AccountView)
1295 |> render("relationship.json", %{user: user, target: subscription_target})
1297 {:error, message} ->
1299 |> put_status(:forbidden)
1300 |> json(%{error: message})
1304 def favourites(%{assigns: %{user: user}} = conn, params) do
1307 |> Map.put("type", "Create")
1308 |> Map.put("favorited_by", user.ap_id)
1309 |> Map.put("blocking_user", user)
1312 ActivityPub.fetch_activities([], params)
1316 |> add_link_headers(:favourites, activities)
1317 |> put_view(StatusView)
1318 |> render("index.json", %{activities: activities, for: user, as: :activity})
1321 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do
1322 with %User{} = user <- User.get_by_id(id),
1323 false <- user.info.hide_favorites do
1326 |> Map.put("type", "Create")
1327 |> Map.put("favorited_by", user.ap_id)
1328 |> Map.put("blocking_user", for_user)
1332 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following]
1334 [Pleroma.Constants.as_public()]
1339 |> ActivityPub.fetch_activities(params)
1343 |> add_link_headers(:favourites, activities)
1344 |> put_view(StatusView)
1345 |> render("index.json", %{activities: activities, for: for_user, as: :activity})
1347 nil -> {:error, :not_found}
1348 true -> render_error(conn, :forbidden, "Can't get favorites")
1352 def bookmarks(%{assigns: %{user: user}} = conn, params) do
1353 user = User.get_cached_by_id(user.id)
1356 Bookmark.for_user_query(user.id)
1357 |> Pagination.fetch_paginated(params)
1361 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end)
1364 |> add_link_headers(:bookmarks, bookmarks)
1365 |> put_view(StatusView)
1366 |> render("index.json", %{activities: activities, for: user, as: :activity})
1369 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do
1370 lists = Pleroma.List.get_lists_account_belongs(user, account_id)
1371 res = ListView.render("lists.json", lists: lists)
1375 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
1376 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do
1379 |> Map.put("type", "Create")
1380 |> Map.put("blocking_user", user)
1381 |> Map.put("user", user)
1382 |> Map.put("muting_user", user)
1384 # we must filter the following list for the user to avoid leaking statuses the user
1385 # does not actually have permission to see (for more info, peruse security issue #270).
1388 |> Enum.filter(fn x -> x in user.following end)
1389 |> ActivityPub.fetch_activities_bounded(following, params)
1393 |> put_view(StatusView)
1394 |> render("index.json", %{activities: activities, for: user, as: :activity})
1396 _e -> render_error(conn, :forbidden, "Error.")
1400 def index(%{assigns: %{user: user}} = conn, _params) do
1401 token = get_session(conn, :oauth_token)
1404 mastodon_emoji = mastodonized_emoji()
1406 limit = Config.get([:instance, :limit])
1409 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user}))
1414 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(),
1415 access_token: token,
1417 domain: Pleroma.Web.Endpoint.host(),
1420 unfollow_modal: false,
1423 auto_play_gif: false,
1424 display_sensitive_media: false,
1425 reduce_motion: false,
1426 max_toot_chars: limit,
1427 mascot: User.get_mascot(user)["url"]
1429 poll_limits: Config.get([:instance, :poll_limits]),
1431 delete_others_notice: present?(user.info.is_moderator),
1432 admin: present?(user.info.is_admin)
1436 default_privacy: user.info.default_scope,
1437 default_sensitive: false,
1438 allow_content_types: Config.get([:instance, :allowed_post_formats])
1440 media_attachments: %{
1441 accept_content_types: [
1457 user.info.settings ||
1487 push_subscription: nil,
1489 custom_emojis: mastodon_emoji,
1495 |> put_layout(false)
1496 |> put_view(MastodonView)
1497 |> render("index.html", %{initial_state: initial_state})
1500 |> put_session(:return_to, conn.request_path)
1501 |> redirect(to: "/web/login")
1505 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
1506 info_cng = User.Info.mastodon_settings_update(user.info, settings)
1508 with changeset <- Changeset.change(user),
1509 changeset <- Changeset.put_embed(changeset, :info, info_cng),
1510 {:ok, _user} <- User.update_and_set_cache(changeset) do
1515 |> put_status(:internal_server_error)
1516 |> json(%{error: inspect(e)})
1520 def login(%{assigns: %{user: %User{}}} = conn, _params) do
1521 redirect(conn, to: local_mastodon_root_path(conn))
1524 @doc "Local Mastodon FE login init action"
1525 def login(conn, %{"code" => auth_token}) do
1526 with {:ok, app} <- get_or_make_app(),
1527 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id),
1528 {:ok, token} <- Token.exchange_token(app, auth) do
1530 |> put_session(:oauth_token, token.token)
1531 |> redirect(to: local_mastodon_root_path(conn))
1535 @doc "Local Mastodon FE callback action"
1536 def login(conn, _) do
1537 with {:ok, app} <- get_or_make_app() do
1542 response_type: "code",
1543 client_id: app.client_id,
1545 scope: Enum.join(app.scopes, " ")
1548 redirect(conn, to: path)
1552 defp local_mastodon_root_path(conn) do
1553 case get_session(conn, :return_to) do
1555 mastodon_api_path(conn, :index, ["getting-started"])
1558 delete_session(conn, :return_to)
1563 defp get_or_make_app do
1564 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."}
1565 scopes = ["read", "write", "follow", "push"]
1567 with %App{} = app <- Repo.get_by(App, find_attrs) do
1569 if app.scopes == scopes do
1573 |> Changeset.change(%{scopes: scopes})
1581 App.register_changeset(
1583 Map.put(find_attrs, :scopes, scopes)
1590 def logout(conn, _) do
1593 |> redirect(to: "/")
1596 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
1597 Logger.debug("Unimplemented, returning unmodified relationship")
1599 with %User{} = target <- User.get_cached_by_id(id) do
1601 |> put_view(AccountView)
1602 |> render("relationship.json", %{user: user, target: target})
1606 def empty_array(conn, _) do
1607 Logger.debug("Unimplemented, returning an empty array")
1611 def empty_object(conn, _) do
1612 Logger.debug("Unimplemented, returning an empty object")
1616 def endorsements(conn, params), do: empty_array(conn, params)
1618 def get_filters(%{assigns: %{user: user}} = conn, _) do
1619 filters = Filter.get_filters(user)
1620 res = FilterView.render("filters.json", filters: filters)
1625 %{assigns: %{user: user}} = conn,
1626 %{"phrase" => phrase, "context" => context} = params
1632 hide: Map.get(params, "irreversible", false),
1633 whole_word: Map.get(params, "boolean", true)
1637 {:ok, response} = Filter.create(query)
1638 res = FilterView.render("filter.json", filter: response)
1642 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1643 filter = Filter.get(filter_id, user)
1644 res = FilterView.render("filter.json", filter: filter)
1649 %{assigns: %{user: user}} = conn,
1650 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params
1654 filter_id: filter_id,
1657 hide: Map.get(params, "irreversible", nil),
1658 whole_word: Map.get(params, "boolean", true)
1662 {:ok, response} = Filter.update(query)
1663 res = FilterView.render("filter.json", filter: response)
1667 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do
1670 filter_id: filter_id
1673 {:ok, _} = Filter.delete(query)
1677 def suggestions(%{assigns: %{user: user}} = conn, _) do
1678 suggestions = Config.get(:suggestions)
1680 if Keyword.get(suggestions, :enabled, false) do
1681 api = Keyword.get(suggestions, :third_party_engine, "")
1682 timeout = Keyword.get(suggestions, :timeout, 5000)
1683 limit = Keyword.get(suggestions, :limit, 23)
1685 host = Config.get([Pleroma.Web.Endpoint, :url, :host])
1687 user = user.nickname
1691 |> String.replace("{{host}}", host)
1692 |> String.replace("{{user}}", user)
1694 with {:ok, %{status: 200, body: body}} <-
1695 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]),
1696 {:ok, data} <- Jason.decode(body) do
1699 |> Enum.slice(0, limit)
1702 |> Map.put("id", fetch_suggestion_id(x))
1703 |> Map.put("avatar", MediaProxy.url(x["avatar"]))
1704 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"]))
1710 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}")
1717 defp fetch_suggestion_id(attrs) do
1718 case User.get_or_fetch(attrs["acct"]) do
1719 {:ok, %User{id: id}} -> id
1724 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do
1725 with %Activity{} = activity <- Activity.get_by_id(status_id),
1726 true <- Visibility.visible_for_user?(activity, user) do
1730 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity)
1740 def create_report(%{assigns: %{user: user}} = conn, params) do
1741 case CommonAPI.report(user, params) do
1744 |> put_view(ReportView)
1745 |> try_render("report.json", %{activity: activity})
1749 |> put_status(:bad_request)
1750 |> json(%{error: err})
1754 def account_register(
1755 %{assigns: %{app: app}} = conn,
1756 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params
1764 "captcha_answer_data",
1768 |> Map.put("nickname", nickname)
1769 |> Map.put("fullname", params["fullname"] || nickname)
1770 |> Map.put("bio", params["bio"] || "")
1771 |> Map.put("confirm", params["password"])
1773 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true),
1774 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do
1776 token_type: "Bearer",
1777 access_token: token.token,
1779 created_at: Token.Utils.format_created_at(token)
1784 |> put_status(:bad_request)
1789 def account_register(%{assigns: %{app: _app}} = conn, _params) do
1790 render_error(conn, :bad_request, "Missing parameters")
1793 def account_register(conn, _) do
1794 render_error(conn, :forbidden, "Invalid credentials")
1797 def conversations(%{assigns: %{user: user}} = conn, params) do
1798 participations = Participation.for_user_with_last_activity_id(user, params)
1801 Enum.map(participations, fn participation ->
1802 ConversationView.render("participation.json", %{participation: participation, for: user})
1806 |> add_link_headers(:conversations, participations)
1807 |> json(conversations)
1810 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do
1811 with %Participation{} = participation <-
1812 Repo.get_by(Participation, id: participation_id, user_id: user.id),
1813 {:ok, participation} <- Participation.mark_as_read(participation) do
1814 participation_view =
1815 ConversationView.render("participation.json", %{participation: participation, for: user})
1818 |> json(participation_view)
1822 def password_reset(conn, params) do
1823 nickname_or_email = params["email"] || params["nickname"]
1825 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do
1827 |> put_status(:no_content)
1830 {:error, "unknown user"} ->
1831 send_resp(conn, :not_found, "")
1834 send_resp(conn, :bad_request, "")
1838 def account_confirmation_resend(conn, params) do
1839 nickname_or_email = params["email"] || params["nickname"]
1841 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email),
1842 {:ok, _} <- User.try_send_confirmation_email(user) do
1844 |> json_response(:no_content, "")
1848 def try_render(conn, target, params)
1849 when is_binary(target) do
1850 case render(conn, target, params) do
1851 nil -> render_error(conn, :not_implemented, "Can't display this activity")
1856 def try_render(conn, _, _) do
1857 render_error(conn, :not_implemented, "Can't display this activity")
1860 defp present?(nil), do: false
1861 defp present?(false), do: false
1862 defp present?(_), do: true