mastodon_api_controller.ex (57215B)
1 # Pleroma: A lightweight social networking server 2 # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/> 3 # SPDX-License-Identifier: AGPL-3.0-only 4 5 defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do 6 use Pleroma.Web, :controller 7 8 import Pleroma.Web.ControllerHelper, 9 only: [json_response: 3, add_link_headers: 5, add_link_headers: 4, add_link_headers: 3] 10 11 alias Ecto.Changeset 12 alias Pleroma.Activity 13 alias Pleroma.Bookmark 14 alias Pleroma.Config 15 alias Pleroma.Conversation.Participation 16 alias Pleroma.Filter 17 alias Pleroma.Formatter 18 alias Pleroma.HTTP 19 alias Pleroma.Notification 20 alias Pleroma.Object 21 alias Pleroma.Pagination 22 alias Pleroma.Plugs.RateLimiter 23 alias Pleroma.Repo 24 alias Pleroma.ScheduledActivity 25 alias Pleroma.Stats 26 alias Pleroma.User 27 alias Pleroma.Web 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 48 49 alias Pleroma.Web.ControllerHelper 50 import Ecto.Query 51 52 require Logger 53 require Pleroma.Constants 54 55 @rate_limited_relations_actions ~w(follow unfollow)a 56 57 @rate_limited_status_actions ~w(reblog_status unreblog_status fav_status unfav_status 58 post_status delete_status)a 59 60 plug( 61 RateLimiter, 62 {:status_id_action, bucket_name: "status_id_action:reblog_unreblog", params: ["id"]} 63 when action in ~w(reblog_status unreblog_status)a 64 ) 65 66 plug( 67 RateLimiter, 68 {:status_id_action, bucket_name: "status_id_action:fav_unfav", params: ["id"]} 69 when action in ~w(fav_status unfav_status)a 70 ) 71 72 plug( 73 RateLimiter, 74 {:relations_id_action, params: ["id", "uri"]} when action in @rate_limited_relations_actions 75 ) 76 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) 83 84 @local_mastodon_name "Mastodon-Local" 85 86 action_fallback(:errors) 87 88 def create_app(conn, params) do 89 scopes = Scopes.fetch_scopes(params, ["read"]) 90 91 app_attrs = 92 params 93 |> Map.drop(["scope", "scopes"]) 94 |> Map.put("scopes", scopes) 95 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 99 conn 100 |> put_view(AppView) 101 |> render("show.json", %{app: app}) 102 end 103 end 104 105 defp add_if_present( 106 map, 107 params, 108 params_field, 109 map_field, 110 value_function \\ fn x -> {:ok, x} end 111 ) do 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) 115 :error -> map 116 end 117 else 118 map 119 end 120 end 121 122 def update_credentials(%{assigns: %{user: user}} = conn, params) do 123 original_user = user 124 125 user_params = 126 %{} 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 132 {:ok, object.data} 133 else 134 _ -> :error 135 end 136 end) 137 138 emojis_text = (user_params["display_name"] || "") <> (user_params["note"] || "") 139 140 user_info_emojis = 141 user.info 142 |> Map.get(:emoji, []) 143 |> Enum.concat(Formatter.get_emoji_map(emojis_text)) 144 |> Enum.dedup() 145 146 info_params = 147 [ 148 :no_rich_text, 149 :locked, 150 :hide_followers, 151 :hide_follows, 152 :hide_favorites, 153 :show_role, 154 :skip_thread_containment 155 ] 156 |> Enum.reduce(%{}, fn key, acc -> 157 add_if_present(acc, params, to_string(key), key, fn value -> 158 {:ok, ControllerHelper.truthy_param?(value)} 159 end) 160 end) 161 |> add_if_present(params, "default_scope", :default_scope) 162 |> add_if_present(params, "fields", :fields, fn fields -> 163 fields = Enum.map(fields, fn f -> Map.update!(f, "value", &AutoLinker.link(&1)) end) 164 165 {:ok, fields} 166 end) 167 |> add_if_present(params, "fields", :raw_fields) 168 |> add_if_present(params, "pleroma_settings_store", :pleroma_settings_store, fn value -> 169 {:ok, Map.merge(user.info.pleroma_settings_store, value)} 170 end) 171 |> add_if_present(params, "header", :banner, fn value -> 172 with %Plug.Upload{} <- value, 173 {:ok, object} <- ActivityPub.upload(value, type: :banner) do 174 {:ok, object.data} 175 else 176 _ -> :error 177 end 178 end) 179 |> add_if_present(params, "pleroma_background_image", :background, fn value -> 180 with %Plug.Upload{} <- value, 181 {:ok, object} <- ActivityPub.upload(value, type: :background) do 182 {:ok, object.data} 183 else 184 _ -> :error 185 end 186 end) 187 |> Map.put(:emoji, user_info_emojis) 188 189 info_cng = User.Info.profile_update(user.info, info_params) 190 191 with changeset <- User.update_changeset(user, user_params), 192 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng), 193 {:ok, user} <- User.update_and_set_cache(changeset) do 194 if original_user != user do 195 CommonAPI.update(user) 196 end 197 198 json( 199 conn, 200 AccountView.render("account.json", %{user: user, for: user, with_pleroma_settings: true}) 201 ) 202 else 203 _e -> render_error(conn, :forbidden, "Invalid request") 204 end 205 end 206 207 def update_avatar(%{assigns: %{user: user}} = conn, %{"img" => ""}) do 208 change = Changeset.change(user, %{avatar: nil}) 209 {:ok, user} = User.update_and_set_cache(change) 210 CommonAPI.update(user) 211 212 json(conn, %{url: nil}) 213 end 214 215 def update_avatar(%{assigns: %{user: user}} = conn, params) do 216 {:ok, object} = ActivityPub.upload(params, type: :avatar) 217 change = Changeset.change(user, %{avatar: object.data}) 218 {:ok, user} = User.update_and_set_cache(change) 219 CommonAPI.update(user) 220 %{"url" => [%{"href" => href} | _]} = object.data 221 222 json(conn, %{url: href}) 223 end 224 225 def update_banner(%{assigns: %{user: user}} = conn, %{"banner" => ""}) do 226 with new_info <- %{"banner" => %{}}, 227 info_cng <- User.Info.profile_update(user.info, new_info), 228 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), 229 {:ok, user} <- User.update_and_set_cache(changeset) do 230 CommonAPI.update(user) 231 232 json(conn, %{url: nil}) 233 end 234 end 235 236 def update_banner(%{assigns: %{user: user}} = conn, params) do 237 with {:ok, object} <- ActivityPub.upload(%{"img" => params["banner"]}, type: :banner), 238 new_info <- %{"banner" => object.data}, 239 info_cng <- User.Info.profile_update(user.info, new_info), 240 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), 241 {:ok, user} <- User.update_and_set_cache(changeset) do 242 CommonAPI.update(user) 243 %{"url" => [%{"href" => href} | _]} = object.data 244 245 json(conn, %{url: href}) 246 end 247 end 248 249 def update_background(%{assigns: %{user: user}} = conn, %{"img" => ""}) do 250 with new_info <- %{"background" => %{}}, 251 info_cng <- User.Info.profile_update(user.info, new_info), 252 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), 253 {:ok, _user} <- User.update_and_set_cache(changeset) do 254 json(conn, %{url: nil}) 255 end 256 end 257 258 def update_background(%{assigns: %{user: user}} = conn, params) do 259 with {:ok, object} <- ActivityPub.upload(params, type: :background), 260 new_info <- %{"background" => object.data}, 261 info_cng <- User.Info.profile_update(user.info, new_info), 262 changeset <- Ecto.Changeset.change(user) |> Ecto.Changeset.put_embed(:info, info_cng), 263 {:ok, _user} <- User.update_and_set_cache(changeset) do 264 %{"url" => [%{"href" => href} | _]} = object.data 265 266 json(conn, %{url: href}) 267 end 268 end 269 270 def verify_credentials(%{assigns: %{user: user}} = conn, _) do 271 chat_token = Phoenix.Token.sign(conn, "user socket", user.id) 272 273 account = 274 AccountView.render("account.json", %{ 275 user: user, 276 for: user, 277 with_pleroma_settings: true, 278 with_chat_token: chat_token 279 }) 280 281 json(conn, account) 282 end 283 284 def verify_app_credentials(%{assigns: %{user: _user, token: token}} = conn, _) do 285 with %Token{app: %App{} = app} <- Repo.preload(token, :app) do 286 conn 287 |> put_view(AppView) 288 |> render("short.json", %{app: app}) 289 end 290 end 291 292 def user(%{assigns: %{user: for_user}} = conn, %{"id" => nickname_or_id}) do 293 with %User{} = user <- User.get_cached_by_nickname_or_id(nickname_or_id), 294 true <- User.auth_active?(user) || user.id == for_user.id || User.superuser?(for_user) do 295 account = AccountView.render("account.json", %{user: user, for: for_user}) 296 json(conn, account) 297 else 298 _e -> render_error(conn, :not_found, "Can't find user") 299 end 300 end 301 302 @mastodon_api_level "2.7.2" 303 304 def masto_instance(conn, _params) do 305 instance = Config.get(:instance) 306 307 response = %{ 308 uri: Web.base_url(), 309 title: Keyword.get(instance, :name), 310 description: Keyword.get(instance, :description), 311 version: "#{@mastodon_api_level} (compatible; #{Pleroma.Application.named_version()})", 312 email: Keyword.get(instance, :email), 313 urls: %{ 314 streaming_api: Pleroma.Web.Endpoint.websocket_url() 315 }, 316 stats: Stats.get_stats(), 317 thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg", 318 languages: ["en"], 319 registrations: Pleroma.Config.get([:instance, :registrations_open]), 320 # Extra (not present in Mastodon): 321 max_toot_chars: Keyword.get(instance, :limit), 322 poll_limits: Keyword.get(instance, :poll_limits) 323 } 324 325 json(conn, response) 326 end 327 328 def peers(conn, _params) do 329 json(conn, Stats.get_peers()) 330 end 331 332 defp mastodonized_emoji do 333 Pleroma.Emoji.get_all() 334 |> Enum.map(fn {shortcode, relative_url, tags} -> 335 url = to_string(URI.merge(Web.base_url(), relative_url)) 336 337 %{ 338 "shortcode" => shortcode, 339 "static_url" => url, 340 "visible_in_picker" => true, 341 "url" => url, 342 "tags" => tags, 343 # Assuming that a comma is authorized in the category name 344 "category" => (tags -- ["Custom"]) |> Enum.join(",") 345 } 346 end) 347 end 348 349 def custom_emojis(conn, _params) do 350 mastodon_emoji = mastodonized_emoji() 351 json(conn, mastodon_emoji) 352 end 353 354 def home_timeline(%{assigns: %{user: user}} = conn, params) do 355 params = 356 params 357 |> Map.put("type", ["Create", "Announce"]) 358 |> Map.put("blocking_user", user) 359 |> Map.put("muting_user", user) 360 |> Map.put("user", user) 361 362 activities = 363 [user.ap_id | user.following] 364 |> ActivityPub.fetch_activities(params) 365 |> Enum.reverse() 366 367 conn 368 |> add_link_headers(:home_timeline, activities) 369 |> put_view(StatusView) 370 |> render("index.json", %{activities: activities, for: user, as: :activity}) 371 end 372 373 def public_timeline(%{assigns: %{user: user}} = conn, params) do 374 local_only = params["local"] in [true, "True", "true", "1"] 375 376 activities = 377 params 378 |> Map.put("type", ["Create", "Announce"]) 379 |> Map.put("local_only", local_only) 380 |> Map.put("blocking_user", user) 381 |> Map.put("muting_user", user) 382 |> Map.put("user", user) 383 |> ActivityPub.fetch_public_activities() 384 |> Enum.reverse() 385 386 conn 387 |> add_link_headers(:public_timeline, activities, false, %{"local" => local_only}) 388 |> put_view(StatusView) 389 |> render("index.json", %{activities: activities, for: user, as: :activity}) 390 end 391 392 def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do 393 with %User{} = user <- User.get_cached_by_nickname_or_id(params["id"]) do 394 params = 395 params 396 |> Map.put("tag", params["tagged"]) 397 398 activities = ActivityPub.fetch_user_activities(user, reading_user, params) 399 400 conn 401 |> add_link_headers(:user_statuses, activities, params["id"]) 402 |> put_view(StatusView) 403 |> render("index.json", %{ 404 activities: activities, 405 for: reading_user, 406 as: :activity 407 }) 408 end 409 end 410 411 def dm_timeline(%{assigns: %{user: user}} = conn, params) do 412 params = 413 params 414 |> Map.put("type", "Create") 415 |> Map.put("blocking_user", user) 416 |> Map.put("user", user) 417 |> Map.put(:visibility, "direct") 418 419 activities = 420 [user.ap_id] 421 |> ActivityPub.fetch_activities_query(params) 422 |> Pagination.fetch_paginated(params) 423 424 conn 425 |> add_link_headers(:dm_timeline, activities) 426 |> put_view(StatusView) 427 |> render("index.json", %{activities: activities, for: user, as: :activity}) 428 end 429 430 def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do 431 with %Activity{} = activity <- Activity.get_by_id_with_object(id), 432 true <- Visibility.visible_for_user?(activity, user) do 433 conn 434 |> put_view(StatusView) 435 |> try_render("status.json", %{activity: activity, for: user}) 436 end 437 end 438 439 def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do 440 with %Activity{} = activity <- Activity.get_by_id(id), 441 activities <- 442 ActivityPub.fetch_activities_for_context(activity.data["context"], %{ 443 "blocking_user" => user, 444 "user" => user, 445 "exclude_id" => activity.id 446 }), 447 grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do 448 result = %{ 449 ancestors: 450 StatusView.render( 451 "index.json", 452 for: user, 453 activities: grouped_activities[true] || [], 454 as: :activity 455 ) 456 |> Enum.reverse(), 457 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart 458 descendants: 459 StatusView.render( 460 "index.json", 461 for: user, 462 activities: grouped_activities[false] || [], 463 as: :activity 464 ) 465 |> Enum.reverse() 466 # credo:disable-for-previous-line Credo.Check.Refactor.PipeChainStart 467 } 468 469 json(conn, result) 470 end 471 end 472 473 def get_poll(%{assigns: %{user: user}} = conn, %{"id" => id}) do 474 with %Object{} = object <- Object.get_by_id(id), 475 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), 476 true <- Visibility.visible_for_user?(activity, user) do 477 conn 478 |> put_view(StatusView) 479 |> try_render("poll.json", %{object: object, for: user}) 480 else 481 error when is_nil(error) or error == false -> 482 render_error(conn, :not_found, "Record not found") 483 end 484 end 485 486 defp get_cached_vote_or_vote(user, object, choices) do 487 idempotency_key = "polls:#{user.id}:#{object.data["id"]}" 488 489 {_, res} = 490 Cachex.fetch(:idempotency_cache, idempotency_key, fn _ -> 491 case CommonAPI.vote(user, object, choices) do 492 {:error, _message} = res -> {:ignore, res} 493 res -> {:commit, res} 494 end 495 end) 496 497 res 498 end 499 500 def poll_vote(%{assigns: %{user: user}} = conn, %{"id" => id, "choices" => choices}) do 501 with %Object{} = object <- Object.get_by_id(id), 502 true <- object.data["type"] == "Question", 503 %Activity{} = activity <- Activity.get_create_by_object_ap_id(object.data["id"]), 504 true <- Visibility.visible_for_user?(activity, user), 505 {:ok, _activities, object} <- get_cached_vote_or_vote(user, object, choices) do 506 conn 507 |> put_view(StatusView) 508 |> try_render("poll.json", %{object: object, for: user}) 509 else 510 nil -> 511 render_error(conn, :not_found, "Record not found") 512 513 false -> 514 render_error(conn, :not_found, "Record not found") 515 516 {:error, message} -> 517 conn 518 |> put_status(:unprocessable_entity) 519 |> json(%{error: message}) 520 end 521 end 522 523 def scheduled_statuses(%{assigns: %{user: user}} = conn, params) do 524 with scheduled_activities <- MastodonAPI.get_scheduled_activities(user, params) do 525 conn 526 |> add_link_headers(:scheduled_statuses, scheduled_activities) 527 |> put_view(ScheduledActivityView) 528 |> render("index.json", %{scheduled_activities: scheduled_activities}) 529 end 530 end 531 532 def show_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do 533 with %ScheduledActivity{} = scheduled_activity <- 534 ScheduledActivity.get(user, scheduled_activity_id) do 535 conn 536 |> put_view(ScheduledActivityView) 537 |> render("show.json", %{scheduled_activity: scheduled_activity}) 538 else 539 _ -> {:error, :not_found} 540 end 541 end 542 543 def update_scheduled_status( 544 %{assigns: %{user: user}} = conn, 545 %{"id" => scheduled_activity_id} = params 546 ) do 547 with %ScheduledActivity{} = scheduled_activity <- 548 ScheduledActivity.get(user, scheduled_activity_id), 549 {:ok, scheduled_activity} <- ScheduledActivity.update(scheduled_activity, params) do 550 conn 551 |> put_view(ScheduledActivityView) 552 |> render("show.json", %{scheduled_activity: scheduled_activity}) 553 else 554 nil -> {:error, :not_found} 555 error -> error 556 end 557 end 558 559 def delete_scheduled_status(%{assigns: %{user: user}} = conn, %{"id" => scheduled_activity_id}) do 560 with %ScheduledActivity{} = scheduled_activity <- 561 ScheduledActivity.get(user, scheduled_activity_id), 562 {:ok, scheduled_activity} <- ScheduledActivity.delete(scheduled_activity) do 563 conn 564 |> put_view(ScheduledActivityView) 565 |> render("show.json", %{scheduled_activity: scheduled_activity}) 566 else 567 nil -> {:error, :not_found} 568 error -> error 569 end 570 end 571 572 def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do 573 params = 574 params 575 |> Map.put("in_reply_to_status_id", params["in_reply_to_id"]) 576 577 scheduled_at = params["scheduled_at"] 578 579 if scheduled_at && ScheduledActivity.far_enough?(scheduled_at) do 580 with {:ok, scheduled_activity} <- 581 ScheduledActivity.create(user, %{"params" => params, "scheduled_at" => scheduled_at}) do 582 conn 583 |> put_view(ScheduledActivityView) 584 |> render("show.json", %{scheduled_activity: scheduled_activity}) 585 end 586 else 587 params = Map.drop(params, ["scheduled_at"]) 588 589 case CommonAPI.post(user, params) do 590 {:error, message} -> 591 conn 592 |> put_status(:unprocessable_entity) 593 |> json(%{error: message}) 594 595 {:ok, activity} -> 596 conn 597 |> put_view(StatusView) 598 |> try_render("status.json", %{activity: activity, for: user, as: :activity}) 599 end 600 end 601 end 602 603 def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do 604 with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do 605 json(conn, %{}) 606 else 607 _e -> render_error(conn, :forbidden, "Can't delete this post") 608 end 609 end 610 611 def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do 612 with {:ok, announce, _activity} <- CommonAPI.repeat(ap_id_or_id, user), 613 %Activity{} = announce <- Activity.normalize(announce.data) do 614 conn 615 |> put_view(StatusView) 616 |> try_render("status.json", %{activity: announce, for: user, as: :activity}) 617 end 618 end 619 620 def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do 621 with {:ok, _unannounce, %{data: %{"id" => id}}} <- CommonAPI.unrepeat(ap_id_or_id, user), 622 %Activity{} = activity <- Activity.get_create_by_object_ap_id_with_object(id) do 623 conn 624 |> put_view(StatusView) 625 |> try_render("status.json", %{activity: activity, for: user, as: :activity}) 626 end 627 end 628 629 def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do 630 with {:ok, _fav, %{data: %{"id" => id}}} <- CommonAPI.favorite(ap_id_or_id, user), 631 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do 632 conn 633 |> put_view(StatusView) 634 |> try_render("status.json", %{activity: activity, for: user, as: :activity}) 635 end 636 end 637 638 def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do 639 with {:ok, _, _, %{data: %{"id" => id}}} <- CommonAPI.unfavorite(ap_id_or_id, user), 640 %Activity{} = activity <- Activity.get_create_by_object_ap_id(id) do 641 conn 642 |> put_view(StatusView) 643 |> try_render("status.json", %{activity: activity, for: user, as: :activity}) 644 end 645 end 646 647 def pin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do 648 with {:ok, activity} <- CommonAPI.pin(ap_id_or_id, user) do 649 conn 650 |> put_view(StatusView) 651 |> try_render("status.json", %{activity: activity, for: user, as: :activity}) 652 end 653 end 654 655 def unpin_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do 656 with {:ok, activity} <- CommonAPI.unpin(ap_id_or_id, user) do 657 conn 658 |> put_view(StatusView) 659 |> try_render("status.json", %{activity: activity, for: user, as: :activity}) 660 end 661 end 662 663 def bookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do 664 with %Activity{} = activity <- Activity.get_by_id_with_object(id), 665 %User{} = user <- User.get_cached_by_nickname(user.nickname), 666 true <- Visibility.visible_for_user?(activity, user), 667 {:ok, _bookmark} <- Bookmark.create(user.id, activity.id) do 668 conn 669 |> put_view(StatusView) 670 |> try_render("status.json", %{activity: activity, for: user, as: :activity}) 671 end 672 end 673 674 def unbookmark_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do 675 with %Activity{} = activity <- Activity.get_by_id_with_object(id), 676 %User{} = user <- User.get_cached_by_nickname(user.nickname), 677 true <- Visibility.visible_for_user?(activity, user), 678 {:ok, _bookmark} <- Bookmark.destroy(user.id, activity.id) do 679 conn 680 |> put_view(StatusView) 681 |> try_render("status.json", %{activity: activity, for: user, as: :activity}) 682 end 683 end 684 685 def mute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do 686 activity = Activity.get_by_id(id) 687 688 with {:ok, activity} <- CommonAPI.add_mute(user, activity) do 689 conn 690 |> put_view(StatusView) 691 |> try_render("status.json", %{activity: activity, for: user, as: :activity}) 692 end 693 end 694 695 def unmute_conversation(%{assigns: %{user: user}} = conn, %{"id" => id}) do 696 activity = Activity.get_by_id(id) 697 698 with {:ok, activity} <- CommonAPI.remove_mute(user, activity) do 699 conn 700 |> put_view(StatusView) 701 |> try_render("status.json", %{activity: activity, for: user, as: :activity}) 702 end 703 end 704 705 def notifications(%{assigns: %{user: user}} = conn, params) do 706 notifications = MastodonAPI.get_notifications(user, params) 707 708 conn 709 |> add_link_headers(:notifications, notifications) 710 |> put_view(NotificationView) 711 |> render("index.json", %{notifications: notifications, for: user}) 712 end 713 714 def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do 715 with {:ok, notification} <- Notification.get(user, id) do 716 conn 717 |> put_view(NotificationView) 718 |> render("show.json", %{notification: notification, for: user}) 719 else 720 {:error, reason} -> 721 conn 722 |> put_status(:forbidden) 723 |> json(%{"error" => reason}) 724 end 725 end 726 727 def clear_notifications(%{assigns: %{user: user}} = conn, _params) do 728 Notification.clear(user) 729 json(conn, %{}) 730 end 731 732 def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do 733 with {:ok, _notif} <- Notification.dismiss(user, id) do 734 json(conn, %{}) 735 else 736 {:error, reason} -> 737 conn 738 |> put_status(:forbidden) 739 |> json(%{"error" => reason}) 740 end 741 end 742 743 def destroy_multiple(%{assigns: %{user: user}} = conn, %{"ids" => ids} = _params) do 744 Notification.destroy_multiple(user, ids) 745 json(conn, %{}) 746 end 747 748 def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do 749 id = List.wrap(id) 750 q = from(u in User, where: u.id in ^id) 751 targets = Repo.all(q) 752 753 conn 754 |> put_view(AccountView) 755 |> render("relationships.json", %{user: user, targets: targets}) 756 end 757 758 # Instead of returning a 400 when no "id" params is present, Mastodon returns an empty array. 759 def relationships(%{assigns: %{user: _user}} = conn, _), do: json(conn, []) 760 761 def update_media(%{assigns: %{user: user}} = conn, data) do 762 with %Object{} = object <- Repo.get(Object, data["id"]), 763 true <- Object.authorize_mutation(object, user), 764 true <- is_binary(data["description"]), 765 description <- data["description"] do 766 new_data = %{object.data | "name" => description} 767 768 {:ok, _} = 769 object 770 |> Object.change(%{data: new_data}) 771 |> Repo.update() 772 773 attachment_data = Map.put(new_data, "id", object.id) 774 775 conn 776 |> put_view(StatusView) 777 |> render("attachment.json", %{attachment: attachment_data}) 778 end 779 end 780 781 def upload(%{assigns: %{user: user}} = conn, %{"file" => file} = data) do 782 with {:ok, object} <- 783 ActivityPub.upload( 784 file, 785 actor: User.ap_id(user), 786 description: Map.get(data, "description") 787 ) do 788 attachment_data = Map.put(object.data, "id", object.id) 789 790 conn 791 |> put_view(StatusView) 792 |> render("attachment.json", %{attachment: attachment_data}) 793 end 794 end 795 796 def set_mascot(%{assigns: %{user: user}} = conn, %{"file" => file}) do 797 with {:ok, object} <- ActivityPub.upload(file, actor: User.ap_id(user)), 798 %{} = attachment_data <- Map.put(object.data, "id", object.id), 799 %{type: type} = rendered <- 800 StatusView.render("attachment.json", %{attachment: attachment_data}) do 801 # Reject if not an image 802 if type == "image" do 803 # Sure! 804 # Save to the user's info 805 info_changeset = User.Info.mascot_update(user.info, rendered) 806 807 user_changeset = 808 user 809 |> Ecto.Changeset.change() 810 |> Ecto.Changeset.put_embed(:info, info_changeset) 811 812 {:ok, _user} = User.update_and_set_cache(user_changeset) 813 814 conn 815 |> json(rendered) 816 else 817 render_error(conn, :unsupported_media_type, "mascots can only be images") 818 end 819 end 820 end 821 822 def get_mascot(%{assigns: %{user: user}} = conn, _params) do 823 mascot = User.get_mascot(user) 824 825 conn 826 |> json(mascot) 827 end 828 829 def favourited_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do 830 with %Activity{} = activity <- Activity.get_by_id_with_object(id), 831 %Object{data: %{"likes" => likes}} <- Object.normalize(activity) do 832 q = from(u in User, where: u.ap_id in ^likes) 833 834 users = 835 Repo.all(q) 836 |> Enum.filter(&(not User.blocks?(user, &1))) 837 838 conn 839 |> put_view(AccountView) 840 |> render("accounts.json", %{for: user, users: users, as: :user}) 841 else 842 _ -> json(conn, []) 843 end 844 end 845 846 def reblogged_by(%{assigns: %{user: user}} = conn, %{"id" => id}) do 847 with %Activity{} = activity <- Activity.get_by_id_with_object(id), 848 %Object{data: %{"announcements" => announces}} <- Object.normalize(activity) do 849 q = from(u in User, where: u.ap_id in ^announces) 850 851 users = 852 Repo.all(q) 853 |> Enum.filter(&(not User.blocks?(user, &1))) 854 855 conn 856 |> put_view(AccountView) 857 |> render("accounts.json", %{for: user, users: users, as: :user}) 858 else 859 _ -> json(conn, []) 860 end 861 end 862 863 def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do 864 local_only = params["local"] in [true, "True", "true", "1"] 865 866 tags = 867 [params["tag"], params["any"]] 868 |> List.flatten() 869 |> Enum.uniq() 870 |> Enum.filter(& &1) 871 |> Enum.map(&String.downcase(&1)) 872 873 tag_all = 874 params["all"] || 875 [] 876 |> Enum.map(&String.downcase(&1)) 877 878 tag_reject = 879 params["none"] || 880 [] 881 |> Enum.map(&String.downcase(&1)) 882 883 activities = 884 params 885 |> Map.put("type", "Create") 886 |> Map.put("local_only", local_only) 887 |> Map.put("blocking_user", user) 888 |> Map.put("muting_user", user) 889 |> Map.put("user", user) 890 |> Map.put("tag", tags) 891 |> Map.put("tag_all", tag_all) 892 |> Map.put("tag_reject", tag_reject) 893 |> ActivityPub.fetch_public_activities() 894 |> Enum.reverse() 895 896 conn 897 |> add_link_headers(:hashtag_timeline, activities, params["tag"], %{"local" => local_only}) 898 |> put_view(StatusView) 899 |> render("index.json", %{activities: activities, for: user, as: :activity}) 900 end 901 902 def followers(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do 903 with %User{} = user <- User.get_cached_by_id(id), 904 followers <- MastodonAPI.get_followers(user, params) do 905 followers = 906 cond do 907 for_user && user.id == for_user.id -> followers 908 user.info.hide_followers -> [] 909 true -> followers 910 end 911 912 conn 913 |> add_link_headers(:followers, followers, user) 914 |> put_view(AccountView) 915 |> render("accounts.json", %{for: for_user, users: followers, as: :user}) 916 end 917 end 918 919 def following(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do 920 with %User{} = user <- User.get_cached_by_id(id), 921 followers <- MastodonAPI.get_friends(user, params) do 922 followers = 923 cond do 924 for_user && user.id == for_user.id -> followers 925 user.info.hide_follows -> [] 926 true -> followers 927 end 928 929 conn 930 |> add_link_headers(:following, followers, user) 931 |> put_view(AccountView) 932 |> render("accounts.json", %{for: for_user, users: followers, as: :user}) 933 end 934 end 935 936 def follow_requests(%{assigns: %{user: followed}} = conn, _params) do 937 with {:ok, follow_requests} <- User.get_follow_requests(followed) do 938 conn 939 |> put_view(AccountView) 940 |> render("accounts.json", %{for: followed, users: follow_requests, as: :user}) 941 end 942 end 943 944 def authorize_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do 945 with %User{} = follower <- User.get_cached_by_id(id), 946 {:ok, follower} <- CommonAPI.accept_follow_request(follower, followed) do 947 conn 948 |> put_view(AccountView) 949 |> render("relationship.json", %{user: followed, target: follower}) 950 else 951 {:error, message} -> 952 conn 953 |> put_status(:forbidden) 954 |> json(%{error: message}) 955 end 956 end 957 958 def reject_follow_request(%{assigns: %{user: followed}} = conn, %{"id" => id}) do 959 with %User{} = follower <- User.get_cached_by_id(id), 960 {:ok, follower} <- CommonAPI.reject_follow_request(follower, followed) do 961 conn 962 |> put_view(AccountView) 963 |> render("relationship.json", %{user: followed, target: follower}) 964 else 965 {:error, message} -> 966 conn 967 |> put_status(:forbidden) 968 |> json(%{error: message}) 969 end 970 end 971 972 def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do 973 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)}, 974 {_, true} <- {:followed, follower.id != followed.id}, 975 {:ok, follower} <- MastodonAPI.follow(follower, followed, conn.params) do 976 conn 977 |> put_view(AccountView) 978 |> render("relationship.json", %{user: follower, target: followed}) 979 else 980 {:followed, _} -> 981 {:error, :not_found} 982 983 {:error, message} -> 984 conn 985 |> put_status(:forbidden) 986 |> json(%{error: message}) 987 end 988 end 989 990 def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do 991 with {_, %User{} = followed} <- {:followed, User.get_cached_by_nickname(uri)}, 992 {_, true} <- {:followed, follower.id != followed.id}, 993 {:ok, follower, followed, _} <- CommonAPI.follow(follower, followed) do 994 conn 995 |> put_view(AccountView) 996 |> render("account.json", %{user: followed, for: follower}) 997 else 998 {:followed, _} -> 999 {:error, :not_found} 1000 1001 {:error, message} -> 1002 conn 1003 |> put_status(:forbidden) 1004 |> json(%{error: message}) 1005 end 1006 end 1007 1008 def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do 1009 with {_, %User{} = followed} <- {:followed, User.get_cached_by_id(id)}, 1010 {_, true} <- {:followed, follower.id != followed.id}, 1011 {:ok, follower} <- CommonAPI.unfollow(follower, followed) do 1012 conn 1013 |> put_view(AccountView) 1014 |> render("relationship.json", %{user: follower, target: followed}) 1015 else 1016 {:followed, _} -> 1017 {:error, :not_found} 1018 1019 error -> 1020 error 1021 end 1022 end 1023 1024 def mute(%{assigns: %{user: muter}} = conn, %{"id" => id} = params) do 1025 notifications = 1026 if Map.has_key?(params, "notifications"), 1027 do: params["notifications"] in [true, "True", "true", "1"], 1028 else: true 1029 1030 with %User{} = muted <- User.get_cached_by_id(id), 1031 {:ok, muter} <- User.mute(muter, muted, notifications) do 1032 conn 1033 |> put_view(AccountView) 1034 |> render("relationship.json", %{user: muter, target: muted}) 1035 else 1036 {:error, message} -> 1037 conn 1038 |> put_status(:forbidden) 1039 |> json(%{error: message}) 1040 end 1041 end 1042 1043 def unmute(%{assigns: %{user: muter}} = conn, %{"id" => id}) do 1044 with %User{} = muted <- User.get_cached_by_id(id), 1045 {:ok, muter} <- User.unmute(muter, muted) do 1046 conn 1047 |> put_view(AccountView) 1048 |> render("relationship.json", %{user: muter, target: muted}) 1049 else 1050 {:error, message} -> 1051 conn 1052 |> put_status(:forbidden) 1053 |> json(%{error: message}) 1054 end 1055 end 1056 1057 def mutes(%{assigns: %{user: user}} = conn, _) do 1058 with muted_accounts <- User.muted_users(user) do 1059 res = AccountView.render("accounts.json", users: muted_accounts, for: user, as: :user) 1060 json(conn, res) 1061 end 1062 end 1063 1064 def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do 1065 with %User{} = blocked <- User.get_cached_by_id(id), 1066 {:ok, blocker} <- User.block(blocker, blocked), 1067 {:ok, _activity} <- ActivityPub.block(blocker, blocked) do 1068 conn 1069 |> put_view(AccountView) 1070 |> render("relationship.json", %{user: blocker, target: blocked}) 1071 else 1072 {:error, message} -> 1073 conn 1074 |> put_status(:forbidden) 1075 |> json(%{error: message}) 1076 end 1077 end 1078 1079 def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do 1080 with %User{} = blocked <- User.get_cached_by_id(id), 1081 {:ok, blocker} <- User.unblock(blocker, blocked), 1082 {:ok, _activity} <- ActivityPub.unblock(blocker, blocked) do 1083 conn 1084 |> put_view(AccountView) 1085 |> render("relationship.json", %{user: blocker, target: blocked}) 1086 else 1087 {:error, message} -> 1088 conn 1089 |> put_status(:forbidden) 1090 |> json(%{error: message}) 1091 end 1092 end 1093 1094 def blocks(%{assigns: %{user: user}} = conn, _) do 1095 with blocked_accounts <- User.blocked_users(user) do 1096 res = AccountView.render("accounts.json", users: blocked_accounts, for: user, as: :user) 1097 json(conn, res) 1098 end 1099 end 1100 1101 def domain_blocks(%{assigns: %{user: %{info: info}}} = conn, _) do 1102 json(conn, info.domain_blocks || []) 1103 end 1104 1105 def block_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do 1106 User.block_domain(blocker, domain) 1107 json(conn, %{}) 1108 end 1109 1110 def unblock_domain(%{assigns: %{user: blocker}} = conn, %{"domain" => domain}) do 1111 User.unblock_domain(blocker, domain) 1112 json(conn, %{}) 1113 end 1114 1115 def subscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do 1116 with %User{} = subscription_target <- User.get_cached_by_id(id), 1117 {:ok, subscription_target} = User.subscribe(user, subscription_target) do 1118 conn 1119 |> put_view(AccountView) 1120 |> render("relationship.json", %{user: user, target: subscription_target}) 1121 else 1122 {:error, message} -> 1123 conn 1124 |> put_status(:forbidden) 1125 |> json(%{error: message}) 1126 end 1127 end 1128 1129 def unsubscribe(%{assigns: %{user: user}} = conn, %{"id" => id}) do 1130 with %User{} = subscription_target <- User.get_cached_by_id(id), 1131 {:ok, subscription_target} = User.unsubscribe(user, subscription_target) do 1132 conn 1133 |> put_view(AccountView) 1134 |> render("relationship.json", %{user: user, target: subscription_target}) 1135 else 1136 {:error, message} -> 1137 conn 1138 |> put_status(:forbidden) 1139 |> json(%{error: message}) 1140 end 1141 end 1142 1143 def favourites(%{assigns: %{user: user}} = conn, params) do 1144 params = 1145 params 1146 |> Map.put("type", "Create") 1147 |> Map.put("favorited_by", user.ap_id) 1148 |> Map.put("blocking_user", user) 1149 1150 activities = 1151 ActivityPub.fetch_activities([], params) 1152 |> Enum.reverse() 1153 1154 conn 1155 |> add_link_headers(:favourites, activities) 1156 |> put_view(StatusView) 1157 |> render("index.json", %{activities: activities, for: user, as: :activity}) 1158 end 1159 1160 def user_favourites(%{assigns: %{user: for_user}} = conn, %{"id" => id} = params) do 1161 with %User{} = user <- User.get_by_id(id), 1162 false <- user.info.hide_favorites do 1163 params = 1164 params 1165 |> Map.put("type", "Create") 1166 |> Map.put("favorited_by", user.ap_id) 1167 |> Map.put("blocking_user", for_user) 1168 1169 recipients = 1170 if for_user do 1171 [Pleroma.Constants.as_public()] ++ [for_user.ap_id | for_user.following] 1172 else 1173 [Pleroma.Constants.as_public()] 1174 end 1175 1176 activities = 1177 recipients 1178 |> ActivityPub.fetch_activities(params) 1179 |> Enum.reverse() 1180 1181 conn 1182 |> add_link_headers(:favourites, activities) 1183 |> put_view(StatusView) 1184 |> render("index.json", %{activities: activities, for: for_user, as: :activity}) 1185 else 1186 nil -> {:error, :not_found} 1187 true -> render_error(conn, :forbidden, "Can't get favorites") 1188 end 1189 end 1190 1191 def bookmarks(%{assigns: %{user: user}} = conn, params) do 1192 user = User.get_cached_by_id(user.id) 1193 1194 bookmarks = 1195 Bookmark.for_user_query(user.id) 1196 |> Pagination.fetch_paginated(params) 1197 1198 activities = 1199 bookmarks 1200 |> Enum.map(fn b -> Map.put(b.activity, :bookmark, Map.delete(b, :activity)) end) 1201 1202 conn 1203 |> add_link_headers(:bookmarks, bookmarks) 1204 |> put_view(StatusView) 1205 |> render("index.json", %{activities: activities, for: user, as: :activity}) 1206 end 1207 1208 def get_lists(%{assigns: %{user: user}} = conn, opts) do 1209 lists = Pleroma.List.for_user(user, opts) 1210 res = ListView.render("lists.json", lists: lists) 1211 json(conn, res) 1212 end 1213 1214 def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do 1215 with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do 1216 res = ListView.render("list.json", list: list) 1217 json(conn, res) 1218 else 1219 _e -> render_error(conn, :not_found, "Record not found") 1220 end 1221 end 1222 1223 def account_lists(%{assigns: %{user: user}} = conn, %{"id" => account_id}) do 1224 lists = Pleroma.List.get_lists_account_belongs(user, account_id) 1225 res = ListView.render("lists.json", lists: lists) 1226 json(conn, res) 1227 end 1228 1229 def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do 1230 with %Pleroma.List{} = list <- Pleroma.List.get(id, user), 1231 {:ok, _list} <- Pleroma.List.delete(list) do 1232 json(conn, %{}) 1233 else 1234 _e -> 1235 json(conn, dgettext("errors", "error")) 1236 end 1237 end 1238 1239 def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do 1240 with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do 1241 res = ListView.render("list.json", list: list) 1242 json(conn, res) 1243 end 1244 end 1245 1246 def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do 1247 accounts 1248 |> Enum.each(fn account_id -> 1249 with %Pleroma.List{} = list <- Pleroma.List.get(id, user), 1250 %User{} = followed <- User.get_cached_by_id(account_id) do 1251 Pleroma.List.follow(list, followed) 1252 end 1253 end) 1254 1255 json(conn, %{}) 1256 end 1257 1258 def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do 1259 accounts 1260 |> Enum.each(fn account_id -> 1261 with %Pleroma.List{} = list <- Pleroma.List.get(id, user), 1262 %User{} = followed <- User.get_cached_by_id(account_id) do 1263 Pleroma.List.unfollow(list, followed) 1264 end 1265 end) 1266 1267 json(conn, %{}) 1268 end 1269 1270 def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do 1271 with %Pleroma.List{} = list <- Pleroma.List.get(id, user), 1272 {:ok, users} = Pleroma.List.get_following(list) do 1273 conn 1274 |> put_view(AccountView) 1275 |> render("accounts.json", %{for: user, users: users, as: :user}) 1276 end 1277 end 1278 1279 def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do 1280 with %Pleroma.List{} = list <- Pleroma.List.get(id, user), 1281 {:ok, list} <- Pleroma.List.rename(list, title) do 1282 res = ListView.render("list.json", list: list) 1283 json(conn, res) 1284 else 1285 _e -> 1286 json(conn, dgettext("errors", "error")) 1287 end 1288 end 1289 1290 def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do 1291 with %Pleroma.List{title: _title, following: following} <- Pleroma.List.get(id, user) do 1292 params = 1293 params 1294 |> Map.put("type", "Create") 1295 |> Map.put("blocking_user", user) 1296 |> Map.put("user", user) 1297 |> Map.put("muting_user", user) 1298 1299 # we must filter the following list for the user to avoid leaking statuses the user 1300 # does not actually have permission to see (for more info, peruse security issue #270). 1301 activities = 1302 following 1303 |> Enum.filter(fn x -> x in user.following end) 1304 |> ActivityPub.fetch_activities_bounded(following, params) 1305 |> Enum.reverse() 1306 1307 conn 1308 |> put_view(StatusView) 1309 |> render("index.json", %{activities: activities, for: user, as: :activity}) 1310 else 1311 _e -> render_error(conn, :forbidden, "Error.") 1312 end 1313 end 1314 1315 def index(%{assigns: %{user: user}} = conn, _params) do 1316 token = get_session(conn, :oauth_token) 1317 1318 if user && token do 1319 mastodon_emoji = mastodonized_emoji() 1320 1321 limit = Config.get([:instance, :limit]) 1322 1323 accounts = 1324 Map.put(%{}, user.id, AccountView.render("account.json", %{user: user, for: user})) 1325 1326 initial_state = 1327 %{ 1328 meta: %{ 1329 streaming_api_base_url: Pleroma.Web.Endpoint.websocket_url(), 1330 access_token: token, 1331 locale: "en", 1332 domain: Pleroma.Web.Endpoint.host(), 1333 admin: "1", 1334 me: "#{user.id}", 1335 unfollow_modal: false, 1336 boost_modal: false, 1337 delete_modal: true, 1338 auto_play_gif: false, 1339 display_sensitive_media: false, 1340 reduce_motion: false, 1341 max_toot_chars: limit, 1342 mascot: User.get_mascot(user)["url"] 1343 }, 1344 poll_limits: Config.get([:instance, :poll_limits]), 1345 rights: %{ 1346 delete_others_notice: present?(user.info.is_moderator), 1347 admin: present?(user.info.is_admin) 1348 }, 1349 compose: %{ 1350 me: "#{user.id}", 1351 default_privacy: user.info.default_scope, 1352 default_sensitive: false, 1353 allow_content_types: Config.get([:instance, :allowed_post_formats]) 1354 }, 1355 media_attachments: %{ 1356 accept_content_types: [ 1357 ".jpg", 1358 ".jpeg", 1359 ".png", 1360 ".gif", 1361 ".webm", 1362 ".mp4", 1363 ".m4v", 1364 "image\/jpeg", 1365 "image\/png", 1366 "image\/gif", 1367 "video\/webm", 1368 "video\/mp4" 1369 ] 1370 }, 1371 settings: 1372 user.info.settings || 1373 %{ 1374 onboarded: true, 1375 home: %{ 1376 shows: %{ 1377 reblog: true, 1378 reply: true 1379 } 1380 }, 1381 notifications: %{ 1382 alerts: %{ 1383 follow: true, 1384 favourite: true, 1385 reblog: true, 1386 mention: true 1387 }, 1388 shows: %{ 1389 follow: true, 1390 favourite: true, 1391 reblog: true, 1392 mention: true 1393 }, 1394 sounds: %{ 1395 follow: true, 1396 favourite: true, 1397 reblog: true, 1398 mention: true 1399 } 1400 } 1401 }, 1402 push_subscription: nil, 1403 accounts: accounts, 1404 custom_emojis: mastodon_emoji, 1405 char_limit: limit 1406 } 1407 |> Jason.encode!() 1408 1409 conn 1410 |> put_layout(false) 1411 |> put_view(MastodonView) 1412 |> render("index.html", %{initial_state: initial_state}) 1413 else 1414 conn 1415 |> put_session(:return_to, conn.request_path) 1416 |> redirect(to: "/web/login") 1417 end 1418 end 1419 1420 def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do 1421 info_cng = User.Info.mastodon_settings_update(user.info, settings) 1422 1423 with changeset <- Ecto.Changeset.change(user), 1424 changeset <- Ecto.Changeset.put_embed(changeset, :info, info_cng), 1425 {:ok, _user} <- User.update_and_set_cache(changeset) do 1426 json(conn, %{}) 1427 else 1428 e -> 1429 conn 1430 |> put_status(:internal_server_error) 1431 |> json(%{error: inspect(e)}) 1432 end 1433 end 1434 1435 def login(%{assigns: %{user: %User{}}} = conn, _params) do 1436 redirect(conn, to: local_mastodon_root_path(conn)) 1437 end 1438 1439 @doc "Local Mastodon FE login init action" 1440 def login(conn, %{"code" => auth_token}) do 1441 with {:ok, app} <- get_or_make_app(), 1442 %Authorization{} = auth <- Repo.get_by(Authorization, token: auth_token, app_id: app.id), 1443 {:ok, token} <- Token.exchange_token(app, auth) do 1444 conn 1445 |> put_session(:oauth_token, token.token) 1446 |> redirect(to: local_mastodon_root_path(conn)) 1447 end 1448 end 1449 1450 @doc "Local Mastodon FE callback action" 1451 def login(conn, _) do 1452 with {:ok, app} <- get_or_make_app() do 1453 path = 1454 o_auth_path( 1455 conn, 1456 :authorize, 1457 response_type: "code", 1458 client_id: app.client_id, 1459 redirect_uri: ".", 1460 scope: Enum.join(app.scopes, " ") 1461 ) 1462 1463 redirect(conn, to: path) 1464 end 1465 end 1466 1467 defp local_mastodon_root_path(conn) do 1468 case get_session(conn, :return_to) do 1469 nil -> 1470 mastodon_api_path(conn, :index, ["getting-started"]) 1471 1472 return_to -> 1473 delete_session(conn, :return_to) 1474 return_to 1475 end 1476 end 1477 1478 defp get_or_make_app do 1479 find_attrs = %{client_name: @local_mastodon_name, redirect_uris: "."} 1480 scopes = ["read", "write", "follow", "push"] 1481 1482 with %App{} = app <- Repo.get_by(App, find_attrs) do 1483 {:ok, app} = 1484 if app.scopes == scopes do 1485 {:ok, app} 1486 else 1487 app 1488 |> Ecto.Changeset.change(%{scopes: scopes}) 1489 |> Repo.update() 1490 end 1491 1492 {:ok, app} 1493 else 1494 _e -> 1495 cs = 1496 App.register_changeset( 1497 %App{}, 1498 Map.put(find_attrs, :scopes, scopes) 1499 ) 1500 1501 Repo.insert(cs) 1502 end 1503 end 1504 1505 def logout(conn, _) do 1506 conn 1507 |> clear_session 1508 |> redirect(to: "/") 1509 end 1510 1511 def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do 1512 Logger.debug("Unimplemented, returning unmodified relationship") 1513 1514 with %User{} = target <- User.get_cached_by_id(id) do 1515 conn 1516 |> put_view(AccountView) 1517 |> render("relationship.json", %{user: user, target: target}) 1518 end 1519 end 1520 1521 def empty_array(conn, _) do 1522 Logger.debug("Unimplemented, returning an empty array") 1523 json(conn, []) 1524 end 1525 1526 def empty_object(conn, _) do 1527 Logger.debug("Unimplemented, returning an empty object") 1528 json(conn, %{}) 1529 end 1530 1531 def get_filters(%{assigns: %{user: user}} = conn, _) do 1532 filters = Filter.get_filters(user) 1533 res = FilterView.render("filters.json", filters: filters) 1534 json(conn, res) 1535 end 1536 1537 def create_filter( 1538 %{assigns: %{user: user}} = conn, 1539 %{"phrase" => phrase, "context" => context} = params 1540 ) do 1541 query = %Filter{ 1542 user_id: user.id, 1543 phrase: phrase, 1544 context: context, 1545 hide: Map.get(params, "irreversible", false), 1546 whole_word: Map.get(params, "boolean", true) 1547 # expires_at 1548 } 1549 1550 {:ok, response} = Filter.create(query) 1551 res = FilterView.render("filter.json", filter: response) 1552 json(conn, res) 1553 end 1554 1555 def get_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do 1556 filter = Filter.get(filter_id, user) 1557 res = FilterView.render("filter.json", filter: filter) 1558 json(conn, res) 1559 end 1560 1561 def update_filter( 1562 %{assigns: %{user: user}} = conn, 1563 %{"phrase" => phrase, "context" => context, "id" => filter_id} = params 1564 ) do 1565 query = %Filter{ 1566 user_id: user.id, 1567 filter_id: filter_id, 1568 phrase: phrase, 1569 context: context, 1570 hide: Map.get(params, "irreversible", nil), 1571 whole_word: Map.get(params, "boolean", true) 1572 # expires_at 1573 } 1574 1575 {:ok, response} = Filter.update(query) 1576 res = FilterView.render("filter.json", filter: response) 1577 json(conn, res) 1578 end 1579 1580 def delete_filter(%{assigns: %{user: user}} = conn, %{"id" => filter_id}) do 1581 query = %Filter{ 1582 user_id: user.id, 1583 filter_id: filter_id 1584 } 1585 1586 {:ok, _} = Filter.delete(query) 1587 json(conn, %{}) 1588 end 1589 1590 # fallback action 1591 # 1592 def errors(conn, {:error, %Changeset{} = changeset}) do 1593 error_message = 1594 changeset 1595 |> Changeset.traverse_errors(fn {message, _opt} -> message end) 1596 |> Enum.map_join(", ", fn {_k, v} -> v end) 1597 1598 conn 1599 |> put_status(:unprocessable_entity) 1600 |> json(%{error: error_message}) 1601 end 1602 1603 def errors(conn, {:error, :not_found}) do 1604 render_error(conn, :not_found, "Record not found") 1605 end 1606 1607 def errors(conn, {:error, error_message}) do 1608 conn 1609 |> put_status(:bad_request) 1610 |> json(%{error: error_message}) 1611 end 1612 1613 def errors(conn, _) do 1614 conn 1615 |> put_status(:internal_server_error) 1616 |> json(dgettext("errors", "Something went wrong")) 1617 end 1618 1619 def suggestions(%{assigns: %{user: user}} = conn, _) do 1620 suggestions = Config.get(:suggestions) 1621 1622 if Keyword.get(suggestions, :enabled, false) do 1623 api = Keyword.get(suggestions, :third_party_engine, "") 1624 timeout = Keyword.get(suggestions, :timeout, 5000) 1625 limit = Keyword.get(suggestions, :limit, 23) 1626 1627 host = Config.get([Pleroma.Web.Endpoint, :url, :host]) 1628 1629 user = user.nickname 1630 1631 url = 1632 api 1633 |> String.replace("{{host}}", host) 1634 |> String.replace("{{user}}", user) 1635 1636 with {:ok, %{status: 200, body: body}} <- 1637 HTTP.get(url, [], adapter: [recv_timeout: timeout, pool: :default]), 1638 {:ok, data} <- Jason.decode(body) do 1639 data = 1640 data 1641 |> Enum.slice(0, limit) 1642 |> Enum.map(fn x -> 1643 x 1644 |> Map.put("id", fetch_suggestion_id(x)) 1645 |> Map.put("avatar", MediaProxy.url(x["avatar"])) 1646 |> Map.put("avatar_static", MediaProxy.url(x["avatar_static"])) 1647 end) 1648 1649 json(conn, data) 1650 else 1651 e -> 1652 Logger.error("Could not retrieve suggestions at fetch #{url}, #{inspect(e)}") 1653 end 1654 else 1655 json(conn, []) 1656 end 1657 end 1658 1659 defp fetch_suggestion_id(attrs) do 1660 case User.get_or_fetch(attrs["acct"]) do 1661 {:ok, %User{id: id}} -> id 1662 _ -> 0 1663 end 1664 end 1665 1666 def status_card(%{assigns: %{user: user}} = conn, %{"id" => status_id}) do 1667 with %Activity{} = activity <- Activity.get_by_id(status_id), 1668 true <- Visibility.visible_for_user?(activity, user) do 1669 data = 1670 StatusView.render( 1671 "card.json", 1672 Pleroma.Web.RichMedia.Helpers.fetch_data_for_activity(activity) 1673 ) 1674 1675 json(conn, data) 1676 else 1677 _e -> 1678 %{} 1679 end 1680 end 1681 1682 def reports(%{assigns: %{user: user}} = conn, params) do 1683 case CommonAPI.report(user, params) do 1684 {:ok, activity} -> 1685 conn 1686 |> put_view(ReportView) 1687 |> try_render("report.json", %{activity: activity}) 1688 1689 {:error, err} -> 1690 conn 1691 |> put_status(:bad_request) 1692 |> json(%{error: err}) 1693 end 1694 end 1695 1696 def account_register( 1697 %{assigns: %{app: app}} = conn, 1698 %{"username" => nickname, "email" => _, "password" => _, "agreement" => true} = params 1699 ) do 1700 params = 1701 params 1702 |> Map.take([ 1703 "email", 1704 "captcha_solution", 1705 "captcha_token", 1706 "captcha_answer_data", 1707 "token", 1708 "password" 1709 ]) 1710 |> Map.put("nickname", nickname) 1711 |> Map.put("fullname", params["fullname"] || nickname) 1712 |> Map.put("bio", params["bio"] || "") 1713 |> Map.put("confirm", params["password"]) 1714 1715 with {:ok, user} <- TwitterAPI.register_user(params, need_confirmation: true), 1716 {:ok, token} <- Token.create_token(app, user, %{scopes: app.scopes}) do 1717 json(conn, %{ 1718 token_type: "Bearer", 1719 access_token: token.token, 1720 scope: app.scopes, 1721 created_at: Token.Utils.format_created_at(token) 1722 }) 1723 else 1724 {:error, errors} -> 1725 conn 1726 |> put_status(:bad_request) 1727 |> json(errors) 1728 end 1729 end 1730 1731 def account_register(%{assigns: %{app: _app}} = conn, _params) do 1732 render_error(conn, :bad_request, "Missing parameters") 1733 end 1734 1735 def account_register(conn, _) do 1736 render_error(conn, :forbidden, "Invalid credentials") 1737 end 1738 1739 def conversations(%{assigns: %{user: user}} = conn, params) do 1740 participations = Participation.for_user_with_last_activity_id(user, params) 1741 1742 conversations = 1743 Enum.map(participations, fn participation -> 1744 ConversationView.render("participation.json", %{participation: participation, for: user}) 1745 end) 1746 1747 conn 1748 |> add_link_headers(:conversations, participations) 1749 |> json(conversations) 1750 end 1751 1752 def conversation_read(%{assigns: %{user: user}} = conn, %{"id" => participation_id}) do 1753 with %Participation{} = participation <- 1754 Repo.get_by(Participation, id: participation_id, user_id: user.id), 1755 {:ok, participation} <- Participation.mark_as_read(participation) do 1756 participation_view = 1757 ConversationView.render("participation.json", %{participation: participation, for: user}) 1758 1759 conn 1760 |> json(participation_view) 1761 end 1762 end 1763 1764 def password_reset(conn, params) do 1765 nickname_or_email = params["email"] || params["nickname"] 1766 1767 with {:ok, _} <- TwitterAPI.password_reset(nickname_or_email) do 1768 conn 1769 |> put_status(:no_content) 1770 |> json("") 1771 else 1772 {:error, "unknown user"} -> 1773 send_resp(conn, :not_found, "") 1774 1775 {:error, _} -> 1776 send_resp(conn, :bad_request, "") 1777 end 1778 end 1779 1780 def account_confirmation_resend(conn, params) do 1781 nickname_or_email = params["email"] || params["nickname"] 1782 1783 with %User{} = user <- User.get_by_nickname_or_email(nickname_or_email), 1784 {:ok, _} <- User.try_send_confirmation_email(user) do 1785 conn 1786 |> json_response(:no_content, "") 1787 end 1788 end 1789 1790 def try_render(conn, target, params) 1791 when is_binary(target) do 1792 case render(conn, target, params) do 1793 nil -> render_error(conn, :not_implemented, "Can't display this activity") 1794 res -> res 1795 end 1796 end 1797 1798 def try_render(conn, _, _) do 1799 render_error(conn, :not_implemented, "Can't display this activity") 1800 end 1801 1802 defp present?(nil), do: false 1803 defp present?(false), do: false 1804 defp present?(_), do: true 1805 end