mastodon_api_controller.ex 23.9 KB
Newer Older
1
2
defmodule Pleroma.Web.MastodonAPI.MastodonAPIController do
  use Pleroma.Web, :controller
3
  alias Pleroma.{Repo, Activity, User, Notification, Stats}
lain's avatar
lain committed
4
  alias Pleroma.Web
lain's avatar
lain committed
5
  alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView}
lain's avatar
lain committed
6
  alias Pleroma.Web.ActivityPub.ActivityPub
7
  alias Pleroma.Web.{CommonAPI, OStatus}
lain's avatar
lain committed
8
9
  alias Pleroma.Web.OAuth.{Authorization, Token, App}
  alias Comeonin.Pbkdf2
Roger Braun's avatar
Roger Braun committed
10
  import Ecto.Query
Thog's avatar
Thog committed
11
  require Logger
12
13

  def create_app(conn, params) do
lain's avatar
lain committed
14
15
    with cs <- App.register_changeset(%App{}, params) |> IO.inspect(),
         {:ok, app} <- Repo.insert(cs) |> IO.inspect() do
16
17
18
19
20
21
22
23
24
25
      res = %{
        id: app.id,
        client_id: app.client_id,
        client_secret: app.client_secret
      }

      json(conn, res)
    end
  end

26
  def update_credentials(%{assigns: %{user: user}} = conn, params) do
lain's avatar
lain committed
27
    original_user = user
28

lain's avatar
lain committed
29
30
31
32
33
34
    params =
      if bio = params["note"] do
        Map.put(params, "bio", bio)
      else
        params
      end
35

lain's avatar
lain committed
36
37
38
    params =
      if name = params["display_name"] do
        Map.put(params, "name", name)
39
      else
lain's avatar
lain committed
40
        params
41
42
      end

lain's avatar
lain committed
43
44
45
46
47
48
49
50
51
52
53
    user =
      if avatar = params["avatar"] do
        with %Plug.Upload{} <- avatar,
             {:ok, object} <- ActivityPub.upload(avatar),
             change = Ecto.Changeset.change(user, %{avatar: object.data}),
             {:ok, user} = User.update_and_set_cache(change) do
          user
        else
          _e -> user
        end
      else
54
        user
lain's avatar
lain committed
55
56
57
58
59
60
61
62
63
64
65
66
67
      end

    user =
      if banner = params["header"] do
        with %Plug.Upload{} <- banner,
             {:ok, object} <- ActivityPub.upload(banner),
             new_info <- Map.put(user.info, "banner", object.data),
             change <- User.info_changeset(user, %{info: new_info}),
             {:ok, user} <- User.update_and_set_cache(change) do
          user
        else
          _e -> user
        end
68
      else
lain's avatar
lain committed
69
        user
70
71
72
      end

    with changeset <- User.update_changeset(user, params),
lain's avatar
lain committed
73
74
75
76
         {:ok, user} <- User.update_and_set_cache(changeset) do
      if original_user != user do
        CommonAPI.update(user)
      end
lain's avatar
lain committed
77
78

      json(conn, AccountView.render("account.json", %{user: user}))
79
80
81
82
83
84
85
86
    else
      _e ->
        conn
        |> put_status(403)
        |> json(%{error: "Invalid request"})
    end
  end

Thog's avatar
Thog committed
87
  def verify_credentials(%{assigns: %{user: user}} = conn, _) do
lain's avatar
lain committed
88
89
90
91
    account = AccountView.render("account.json", %{user: user})
    json(conn, account)
  end

Roger Braun's avatar
Roger Braun committed
92
93
94
95
96
  def user(conn, %{"id" => id}) do
    with %User{} = user <- Repo.get(User, id) do
      account = AccountView.render("account.json", %{user: user})
      json(conn, account)
    else
lain's avatar
lain committed
97
98
99
100
      _e ->
        conn
        |> put_status(404)
        |> json(%{error: "Can't find user"})
Roger Braun's avatar
Roger Braun committed
101
102
103
    end
  end

lain's avatar
lain committed
104
  @instance Application.get_env(:pleroma, :instance)
105
  @mastodon_api_level "2.3.3"
lain's avatar
lain committed
106

lain's avatar
lain committed
107
108
  def masto_instance(conn, _params) do
    response = %{
lain's avatar
lain committed
109
      uri: Web.base_url(),
lain's avatar
lain committed
110
      title: Keyword.get(@instance, :name),
lain's avatar
lain committed
111
      description: "A Pleroma instance, an alternative fediverse server",
112
      version: "#{@mastodon_api_level} (compatible; #{Keyword.get(@instance, :version)})",
lain's avatar
lain committed
113
114
      email: Keyword.get(@instance, :email),
      urls: %{
lain's avatar
lain committed
115
        streaming_api: String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws")
lain's avatar
lain committed
116
      },
lain's avatar
lain committed
117
118
      stats: Stats.get_stats(),
      thumbnail: Web.base_url() <> "/instance/thumbnail.jpeg",
lain's avatar
lain committed
119
      max_toot_chars: Keyword.get(@instance, :limit)
120
121
    }

lain's avatar
lain committed
122
    json(conn, response)
123
  end
lain's avatar
lain committed
124

125
  def peers(conn, _params) do
lain's avatar
lain committed
126
    json(conn, Stats.get_peers())
127
128
  end

129
130
  defp mastodonized_emoji do
    Pleroma.Formatter.get_custom_emoji()
131
    |> Enum.map(fn {shortcode, relative_url} ->
lain's avatar
lain committed
132
133
      url = to_string(URI.merge(Web.base_url(), relative_url))

134
135
136
137
138
139
      %{
        "shortcode" => shortcode,
        "static_url" => url,
        "url" => url
      }
    end)
140
141
142
143
  end

  def custom_emojis(conn, _params) do
    mastodon_emoji = mastodonized_emoji()
lain's avatar
lain committed
144
    json(conn, mastodon_emoji)
145
146
  end

147
  defp add_link_headers(conn, method, activities, param \\ false) do
148
149
    last = List.last(activities)
    first = List.first(activities)
lain's avatar
lain committed
150

151
152
153
    if last do
      min = last.id
      max = first.id
lain's avatar
lain committed
154
155
156
157
158
159
160
161
162
163
164
165
166
167

      {next_url, prev_url} =
        if param do
          {
            mastodon_api_url(Pleroma.Web.Endpoint, method, param, max_id: min),
            mastodon_api_url(Pleroma.Web.Endpoint, method, param, since_id: max)
          }
        else
          {
            mastodon_api_url(Pleroma.Web.Endpoint, method, max_id: min),
            mastodon_api_url(Pleroma.Web.Endpoint, method, since_id: max)
          }
        end

168
169
170
171
172
173
174
      conn
      |> put_resp_header("link", "<#{next_url}>; rel=\"next\", <#{prev_url}>; rel=\"prev\"")
    else
      conn
    end
  end

lain's avatar
lain committed
175
  def home_timeline(%{assigns: %{user: user}} = conn, params) do
lain's avatar
lain committed
176
177
178
179
180
    params =
      params
      |> Map.put("type", ["Create", "Announce"])
      |> Map.put("blocking_user", user)
      |> Map.put("user", user)
lain's avatar
lain committed
181

lain's avatar
lain committed
182
183
184
    activities =
      ActivityPub.fetch_activities([user.ap_id | user.following], params)
      |> Enum.reverse()
185
186

    conn
lain's avatar
lain committed
187
    |> add_link_headers(:home_timeline, activities)
lain's avatar
lain committed
188
    |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
lain's avatar
lain committed
189
190
191
  end

  def public_timeline(%{assigns: %{user: user}} = conn, params) do
lain's avatar
lain committed
192
193
194
195
196
    params =
      params
      |> Map.put("type", ["Create", "Announce"])
      |> Map.put("local_only", params["local"] in [true, "True", "true", "1"])
      |> Map.put("blocking_user", user)
lain's avatar
lain committed
197

lain's avatar
lain committed
198
199
200
    activities =
      ActivityPub.fetch_public_activities(params)
      |> Enum.reverse()
lain's avatar
lain committed
201

lain's avatar
lain committed
202
203
204
    conn
    |> add_link_headers(:public_timeline, activities)
    |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
lain's avatar
lain committed
205
206
  end

lain's avatar
lain committed
207
208
  def user_statuses(%{assigns: %{user: user}} = conn, params) do
    with %User{ap_id: ap_id} <- Repo.get(User, params["id"]) do
lain's avatar
lain committed
209
210
211
212
213
      params =
        params
        |> Map.put("type", ["Create", "Announce"])
        |> Map.put("actor_id", ap_id)
        |> Map.put("whole_db", true)
lain's avatar
lain committed
214

Gian Sass's avatar
Gian Sass committed
215
216
217
218
219
      if params["pinned"] == "true" do
        # Since Pleroma has no "pinned" posts feature, we'll just set an empty list here
        activities = []
      else
        activities =
lain's avatar
lain committed
220
221
          ActivityPub.fetch_public_activities(params)
          |> Enum.reverse()
Gian Sass's avatar
Gian Sass committed
222
      end
lain's avatar
lain committed
223

224
225
226
      conn
      |> add_link_headers(:user_statuses, activities, params["id"])
      |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
lain's avatar
lain committed
227
228
229
    end
  end

lain's avatar
lain committed
230
  def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
lain's avatar
lain committed
231
232
    with %Activity{} = activity <- Repo.get(Activity, id),
         true <- ActivityPub.visible_for_user?(activity, user) do
lain's avatar
lain committed
233
      render(conn, StatusView, "status.json", %{activity: activity, for: user})
lain's avatar
lain committed
234
235
236
    end
  end

lain's avatar
lain committed
237
238
  def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    with %Activity{} = activity <- Repo.get(Activity, id),
lain's avatar
lain committed
239
240
241
242
243
244
245
246
247
248
         activities <-
           ActivityPub.fetch_activities_for_context(activity.data["context"], %{
             "blocking_user" => user,
             "user" => user
           }),
         activities <-
           activities |> Enum.filter(fn %{id: aid} -> to_string(aid) != to_string(id) end),
         activities <-
           activities |> Enum.filter(fn %{data: %{"type" => type}} -> type == "Create" end),
         grouped_activities <- Enum.group_by(activities, fn %{id: id} -> id < activity.id end) do
lain's avatar
lain committed
249
      result = %{
lain's avatar
lain committed
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
        ancestors:
          StatusView.render(
            "index.json",
            for: user,
            activities: grouped_activities[true] || [],
            as: :activity
          )
          |> Enum.reverse(),
        descendants:
          StatusView.render(
            "index.json",
            for: user,
            activities: grouped_activities[false] || [],
            as: :activity
          )
          |> Enum.reverse()
lain's avatar
lain committed
266
267
268
269
270
271
      }

      json(conn, result)
    end
  end

Thog's avatar
Thog committed
272
  def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
lain's avatar
lain committed
273
274
275
276
    params =
      params
      |> Map.put("in_reply_to_status_id", params["in_reply_to_id"])
      |> Map.put("no_attachment_links", true)
lain's avatar
lain committed
277

lain's avatar
lain committed
278
279
280
281
282
283
284
285
    idempotency_key =
      case get_req_header(conn, "idempotency-key") do
        [key] -> key
        _ -> Ecto.UUID.generate()
      end

    {:ok, activity} =
      Cachex.get!(
286
287
        :idempotency_cache,
        idempotency_key,
lain's avatar
lain committed
288
289
290
        fallback: fn _ -> CommonAPI.post(user, params) end
      )

lain's avatar
lain committed
291
    render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
lain's avatar
lain committed
292
  end
lain's avatar
lain committed
293
294
295
296
297
298
299
300
301
302
303

  def delete_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    with {:ok, %Activity{}} <- CommonAPI.delete(id, user) do
      json(conn, %{})
    else
      _e ->
        conn
        |> put_status(403)
        |> json(%{error: "Can't delete this post"})
    end
  end
lain's avatar
lain committed
304
305

  def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
lain's avatar
lain committed
306
    with {:ok, announce, _activity} = CommonAPI.repeat(ap_id_or_id, user) do
lain's avatar
lain committed
307
      render(conn, StatusView, "status.json", %{activity: announce, for: user, as: :activity})
lain's avatar
lain committed
308
309
    end
  end
lain's avatar
lain committed
310
311

  def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
lain's avatar
lain committed
312
313
    with {:ok, _fav, %{data: %{"id" => id}}} = CommonAPI.favorite(ap_id_or_id, user),
         %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
lain's avatar
lain committed
314
      render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
lain's avatar
lain committed
315
316
317
318
319
    end
  end

  def unfav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
    with {:ok, %{data: %{"id" => id}}} = CommonAPI.unfavorite(ap_id_or_id, user),
lain's avatar
lain committed
320
         %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
lain's avatar
lain committed
321
      render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
lain's avatar
lain committed
322
323
    end
  end
324

325
326
  def notifications(%{assigns: %{user: user}} = conn, params) do
    notifications = Notification.for_user(user, params)
lain's avatar
lain committed
327
328
329
330
331
332

    result =
      Enum.map(notifications, fn x ->
        render_notification(user, x)
      end)
      |> Enum.filter(& &1)
333

lain's avatar
lain committed
334
335
336
    conn
    |> add_link_headers(:notifications, notifications)
    |> json(result)
337
338
  end

339
340
341
342
343
344
345
  def get_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
    with {:ok, notification} <- Notification.get(user, id) do
      json(conn, render_notification(user, notification))
    else
      {:error, reason} ->
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
346
        |> send_resp(403, Jason.encode!(%{"error" => reason}))
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
    end
  end

  def clear_notifications(%{assigns: %{user: user}} = conn, _params) do
    Notification.clear(user)
    json(conn, %{})
  end

  def dismiss_notification(%{assigns: %{user: user}} = conn, %{"id" => id} = _params) do
    with {:ok, _notif} <- Notification.dismiss(user, id) do
      json(conn, %{})
    else
      {:error, reason} ->
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
362
        |> send_resp(403, Jason.encode!(%{"error" => reason}))
363
364
365
    end
  end

Roger Braun's avatar
Roger Braun committed
366
367
  def relationships(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    id = List.wrap(id)
lain's avatar
lain committed
368
    q = from(u in User, where: u.id in ^id)
Roger Braun's avatar
Roger Braun committed
369
    targets = Repo.all(q)
lain's avatar
lain committed
370
    render(conn, AccountView, "relationships.json", %{user: user, targets: targets})
Roger Braun's avatar
Roger Braun committed
371
372
  end

Thog's avatar
Thog committed
373
  def upload(%{assigns: %{user: _}} = conn, %{"file" => file}) do
lain's avatar
lain committed
374
    with {:ok, object} <- ActivityPub.upload(file) do
lain's avatar
lain committed
375
376
377
      data =
        object.data
        |> Map.put("id", object.id)
lain's avatar
lain committed
378

lain's avatar
lain committed
379
      render(conn, StatusView, "attachment.json", %{attachment: data})
lain's avatar
lain committed
380
381
382
    end
  end

383
  def favourited_by(conn, %{"id" => id}) do
Thog's avatar
Thog committed
384
    with %Activity{data: %{"object" => %{"likes" => likes}}} <- Repo.get(Activity, id) do
lain's avatar
lain committed
385
      q = from(u in User, where: u.ap_id in ^likes)
386
      users = Repo.all(q)
lain's avatar
lain committed
387
      render(conn, AccountView, "accounts.json", %{users: users, as: :user})
388
389
390
391
392
393
394
    else
      _ -> json(conn, [])
    end
  end

  def reblogged_by(conn, %{"id" => id}) do
    with %Activity{data: %{"object" => %{"announcements" => announces}}} <- Repo.get(Activity, id) do
lain's avatar
lain committed
395
      q = from(u in User, where: u.ap_id in ^announces)
396
      users = Repo.all(q)
lain's avatar
lain committed
397
      render(conn, AccountView, "accounts.json", %{users: users, as: :user})
398
399
400
401
402
    else
      _ -> json(conn, [])
    end
  end

Roger Braun's avatar
Roger Braun committed
403
  def hashtag_timeline(%{assigns: %{user: user}} = conn, params) do
lain's avatar
lain committed
404
405
406
407
408
    params =
      params
      |> Map.put("type", "Create")
      |> Map.put("local_only", !!params["local"])
      |> Map.put("blocking_user", user)
Roger Braun's avatar
Roger Braun committed
409

lain's avatar
lain committed
410
411
412
    activities =
      ActivityPub.fetch_public_activities(params)
      |> Enum.reverse()
Roger Braun's avatar
Roger Braun committed
413
414

    conn
415
    |> add_link_headers(:hashtag_timeline, activities, params["tag"])
Roger Braun's avatar
Roger Braun committed
416
417
418
    |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
  end

419
420
421
422
  # TODO: Pagination
  def followers(conn, %{"id" => id}) do
    with %User{} = user <- Repo.get(User, id),
         {:ok, followers} <- User.get_followers(user) do
lain's avatar
lain committed
423
      render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
424
425
426
427
428
429
    end
  end

  def following(conn, %{"id" => id}) do
    with %User{} = user <- Repo.get(User, id),
         {:ok, followers} <- User.get_friends(user) do
lain's avatar
lain committed
430
      render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
431
432
433
    end
  end

eal's avatar
eal committed
434
435
  def follow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
    with %User{} = followed <- Repo.get(User, id),
eal's avatar
eal committed
436
         {:ok, follower} <- User.follow(follower, followed),
Thog's avatar
Thog committed
437
         {:ok, _activity} <- ActivityPub.follow(follower, followed) do
lain's avatar
lain committed
438
      render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
eal's avatar
eal committed
439
    else
Thog's avatar
Thog committed
440
      {:error, message} ->
eal's avatar
eal committed
441
442
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
443
        |> send_resp(403, Jason.encode!(%{"error" => message}))
444
445
446
    end
  end

eal's avatar
eal committed
447
  def follow(%{assigns: %{user: follower}} = conn, %{"uri" => uri}) do
eal's avatar
eal committed
448
    with %User{} = followed <- Repo.get_by(User, nickname: uri),
eal's avatar
eal committed
449
         {:ok, follower} <- User.follow(follower, followed),
Thog's avatar
Thog committed
450
         {:ok, _activity} <- ActivityPub.follow(follower, followed) do
lain's avatar
lain committed
451
      render(conn, AccountView, "account.json", %{user: followed})
eal's avatar
eal committed
452
    else
Thog's avatar
Thog committed
453
      {:error, message} ->
eal's avatar
eal committed
454
455
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
456
        |> send_resp(403, Jason.encode!(%{"error" => message}))
eal's avatar
eal committed
457
458
459
    end
  end

460
461
462
  # TODO: Clean up and unify
  def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
    with %User{} = followed <- Repo.get(User, id),
lain's avatar
lain committed
463
464
465
466
467
468
469
470
471
         {:ok, follower, follow_activity} <- User.unfollow(follower, followed),
         {:ok, _activity} <-
           ActivityPub.insert(%{
             "type" => "Undo",
             "actor" => follower.ap_id,
             # get latest Follow for these users
             "object" => follow_activity.data["id"]
           }) do
      render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
472
473
474
    end
  end

lain's avatar
lain committed
475
476
477
  def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
    with %User{} = blocked <- Repo.get(User, id),
         {:ok, blocker} <- User.block(blocker, blocked) do
lain's avatar
lain committed
478
      render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked})
lain's avatar
lain committed
479
    else
Thog's avatar
Thog committed
480
      {:error, message} ->
lain's avatar
lain committed
481
482
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
483
        |> send_resp(403, Jason.encode!(%{"error" => message}))
lain's avatar
lain committed
484
485
486
487
488
489
    end
  end

  def unblock(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
    with %User{} = blocked <- Repo.get(User, id),
         {:ok, blocker} <- User.unblock(blocker, blocked) do
lain's avatar
lain committed
490
      render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked})
lain's avatar
lain committed
491
    else
Thog's avatar
Thog committed
492
      {:error, message} ->
lain's avatar
lain committed
493
494
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
495
        |> send_resp(403, Jason.encode!(%{"error" => message}))
lain's avatar
lain committed
496
497
498
    end
  end

lain's avatar
lain committed
499
500
501
  # TODO: Use proper query
  def blocks(%{assigns: %{user: user}} = conn, _) do
    with blocked_users <- user.info["blocks"] || [],
lain's avatar
lain committed
502
         accounts <- Enum.map(blocked_users, fn ap_id -> User.get_cached_by_ap_id(ap_id) end) do
lain's avatar
lain committed
503
504
505
506
507
      res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
      json(conn, res)
    end
  end

508
  def search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
lain's avatar
lain committed
509
    accounts = User.search(query, params["resolve"] == "true")
lain's avatar
lain committed
510

lain's avatar
lain committed
511
512
513
514
    fetched =
      if Regex.match?(~r/https?:/, query) do
        with {:ok, activities} <- OStatus.fetch_activity_from_url(query) do
          activities
515
516
517
518
          |> Enum.filter(fn
            %{data: %{"type" => "Create"}} -> true
            _ -> false
          end)
lain's avatar
lain committed
519
520
521
522
523
524
525
526
527
        else
          _e -> []
        end
      end || []

    q =
      from(
        a in Activity,
        where: fragment("?->>'type' = 'Create'", a.data),
lain's avatar
lain committed
528
        where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
lain's avatar
lain committed
529
530
531
532
533
534
        where:
          fragment(
            "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
            a.data,
            ^query
          ),
lain's avatar
lain committed
535
        limit: 20,
lain's avatar
lain committed
536
        order_by: [desc: :id]
lain's avatar
lain committed
537
      )
538
539

    statuses = Repo.all(q) ++ fetched
lain's avatar
lain committed
540
541
542
543
544
545

    tags =
      String.split(query)
      |> Enum.uniq()
      |> Enum.filter(fn tag -> String.starts_with?(tag, "#") end)
      |> Enum.map(fn tag -> String.slice(tag, 1..-1) end)
lain's avatar
lain committed
546
547
548

    res = %{
      "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
lain's avatar
lain committed
549
550
      "statuses" =>
        StatusView.render("index.json", activities: statuses, for: user, as: :activity),
551
      "hashtags" => tags
lain's avatar
lain committed
552
553
554
555
556
    }

    json(conn, res)
  end

lain's avatar
lain committed
557
558
  def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
    accounts = User.search(query, params["resolve"] == "true")
559
560
561
562
563
564

    res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)

    json(conn, res)
  end

Thog's avatar
Thog committed
565
  def favourites(%{assigns: %{user: user}} = conn, _) do
lain's avatar
lain committed
566
567
568
569
570
    params =
      %{}
      |> Map.put("type", "Create")
      |> Map.put("favorited_by", user.ap_id)
      |> Map.put("blocking_user", user)
571

lain's avatar
lain committed
572
573
574
    activities =
      ActivityPub.fetch_public_activities(params)
      |> Enum.reverse()
575
576
577
578
579

    conn
    |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
  end

lain's avatar
lain committed
580
  def index(%{assigns: %{user: user}} = conn, _params) do
lain's avatar
lain committed
581
582
583
    token =
      conn
      |> get_session(:oauth_token)
lain's avatar
lain committed
584
585

    if user && token do
586
      mastodon_emoji = mastodonized_emoji()
lain's avatar
lain committed
587
      accounts = Map.put(%{}, user.id, AccountView.render("account.json", %{user: user}))
lain's avatar
lain committed
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603

      initial_state =
        %{
          meta: %{
            streaming_api_base_url:
              String.replace(Pleroma.Web.Endpoint.static_url(), "http", "ws"),
            access_token: token,
            locale: "en",
            domain: Pleroma.Web.Endpoint.host(),
            admin: "1",
            me: "#{user.id}",
            unfollow_modal: false,
            boost_modal: false,
            delete_modal: true,
            auto_play_gif: false,
            reduce_motion: false
lain's avatar
lain committed
604
          },
lain's avatar
lain committed
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
          compose: %{
            me: "#{user.id}",
            default_privacy: "public",
            default_sensitive: false
          },
          media_attachments: %{
            accept_content_types: [
              ".jpg",
              ".jpeg",
              ".png",
              ".gif",
              ".webm",
              ".mp4",
              ".m4v",
              "image\/jpeg",
              "image\/png",
              "image\/gif",
              "video\/webm",
              "video\/mp4"
            ]
          },
lain's avatar
lain committed
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
          settings:
            Map.get(user.info, "settings") ||
              %{
                onboarded: true,
                home: %{
                  shows: %{
                    reblog: true,
                    reply: true
                  }
                },
                notifications: %{
                  alerts: %{
                    follow: true,
                    favourite: true,
                    reblog: true,
                    mention: true
                  },
                  shows: %{
                    follow: true,
                    favourite: true,
                    reblog: true,
                    mention: true
                  },
                  sounds: %{
                    follow: true,
                    favourite: true,
                    reblog: true,
                    mention: true
                  }
                }
lain's avatar
lain committed
656
657
658
659
660
661
662
663
              },
          push_subscription: nil,
          accounts: accounts,
          custom_emojis: mastodon_emoji,
          char_limit: Keyword.get(@instance, :limit)
        }
        |> Jason.encode!()

lain's avatar
lain committed
664
665
666
667
668
669
670
671
672
      conn
      |> put_layout(false)
      |> render(MastodonView, "index.html", %{initial_state: initial_state})
    else
      conn
      |> redirect(to: "/web/login")
    end
  end

673
674
675
676
677
678
  def put_settings(%{assigns: %{user: user}} = conn, %{"data" => settings} = _params) do
    with new_info <- Map.put(user.info, "settings", settings),
         change <- User.info_changeset(user, %{info: new_info}),
         {:ok, _user} <- User.update_and_set_cache(change) do
      conn
      |> json(%{})
lain's avatar
lain committed
679
680
    else
      e ->
681
682
683
684
685
        conn
        |> json(%{error: inspect(e)})
    end
  end

Thog's avatar
Thog committed
686
  def login(conn, _) do
lain's avatar
lain committed
687
    conn
688
    |> render(MastodonView, "login.html", %{error: false})
lain's avatar
lain committed
689
690
691
692
693
694
695
  end

  defp get_or_make_app() do
    with %App{} = app <- Repo.get_by(App, client_name: "Mastodon-Local") do
      {:ok, app}
    else
      _e ->
lain's avatar
lain committed
696
697
698
699
700
701
702
        cs =
          App.register_changeset(%App{}, %{
            client_name: "Mastodon-Local",
            redirect_uris: ".",
            scopes: "read,write,follow"
          })

lain's avatar
lain committed
703
704
705
706
        Repo.insert(cs)
    end
  end

lain's avatar
lain committed
707
  def login_post(conn, %{"authorization" => %{"name" => name, "password" => password}}) do
708
    with %User{} = user <- User.get_by_nickname_or_email(name),
lain's avatar
lain committed
709
710
711
712
713
714
         true <- Pbkdf2.checkpw(password, user.password_hash),
         {:ok, app} <- get_or_make_app(),
         {:ok, auth} <- Authorization.create_authorization(app, user),
         {:ok, token} <- Token.exchange_token(app, auth) do
      conn
      |> put_session(:oauth_token, token.token)
eal's avatar
eal committed
715
      |> redirect(to: "/web/getting-started")
716
717
718
719
    else
      _e ->
        conn
        |> render(MastodonView, "login.html", %{error: "Wrong username or password"})
lain's avatar
lain committed
720
721
722
    end
  end

lain's avatar
lain committed
723
724
725
726
727
728
  def logout(conn, _) do
    conn
    |> clear_session
    |> redirect(to: "/")
  end

729
730
  def relationship_noop(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    Logger.debug("Unimplemented, returning unmodified relationship")
lain's avatar
lain committed
731

732
    with %User{} = target <- Repo.get(User, id) do
lain's avatar
lain committed
733
      render(conn, AccountView, "relationship.json", %{user: user, target: target})
734
735
736
    end
  end

737
738
739
740
  def empty_array(conn, _) do
    Logger.debug("Unimplemented, returning an empty array")
    json(conn, [])
  end
741

742
743
744
745
746
  def empty_object(conn, _) do
    Logger.debug("Unimplemented, returning an empty object")
    json(conn, %{})
  end

747
  def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
748
    actor = User.get_cached_by_ap_id(activity.data["actor"])
lain's avatar
lain committed
749
750
751
752
753

    created_at =
      NaiveDateTime.to_iso8601(created_at)
      |> String.replace(~r/(\.\d+)?$/, ".000Z", global: false)

754
755
    case activity.data["type"] do
      "Create" ->
lain's avatar
lain committed
756
757
758
759
760
761
762
763
        %{
          id: id,
          type: "mention",
          created_at: created_at,
          account: AccountView.render("account.json", %{user: actor}),
          status: StatusView.render("status.json", %{activity: activity, for: user})
        }

764
765
      "Like" ->
        liked_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
lain's avatar
lain committed
766
767
768
769
770
771
772
773
774

        %{
          id: id,
          type: "favourite",
          created_at: created_at,
          account: AccountView.render("account.json", %{user: actor}),
          status: StatusView.render("status.json", %{activity: liked_activity, for: user})
        }

775
776
      "Announce" ->
        announced_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
lain's avatar
lain committed
777
778
779
780
781
782
783
784
785

        %{
          id: id,
          type: "reblog",
          created_at: created_at,
          account: AccountView.render("account.json", %{user: actor}),
          status: StatusView.render("status.json", %{activity: announced_activity, for: user})
        }

786
      "Follow" ->
lain's avatar
lain committed
787
788
789
790
791
792
793
794
795
        %{
          id: id,
          type: "follow",
          created_at: created_at,
          account: AccountView.render("account.json", %{user: actor})
        }

      _ ->
        nil
796
797
    end
  end
798
end