mastodon_api_controller.ex 27 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
eal's avatar
eal committed
5
  alias Pleroma.Web.MastodonAPI.{StatusView, AccountView, MastodonView, ListView}
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

207
208
209
  def user_statuses(%{assigns: %{user: reading_user}} = conn, params) do
    with %User{} = user <- Repo.get(User, params["id"]) do
      # Since Pleroma has no "pinned" posts feature, we'll just set an empty list here
eal's avatar
eal committed
210
211
212
213
      activities =
        if params["pinned"] == "true" do
          []
        else
214
          ActivityPub.fetch_user_activities(user, reading_user, params)
eal's avatar
eal committed
215
        end
lain's avatar
lain committed
216

217
218
219
      conn
      |> add_link_headers(:user_statuses, activities, params["id"])
      |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
lain's avatar
lain committed
220
221
222
    end
  end

lain's avatar
lain committed
223
  def get_status(%{assigns: %{user: user}} = conn, %{"id" => id}) do
lain's avatar
lain committed
224
225
    with %Activity{} = activity <- Repo.get(Activity, id),
         true <- ActivityPub.visible_for_user?(activity, user) do
lain's avatar
lain committed
226
      render(conn, StatusView, "status.json", %{activity: activity, for: user})
lain's avatar
lain committed
227
228
229
    end
  end

lain's avatar
lain committed
230
231
  def get_context(%{assigns: %{user: user}} = conn, %{"id" => id}) do
    with %Activity{} = activity <- Repo.get(Activity, id),
lain's avatar
lain committed
232
233
234
235
236
237
238
239
240
241
         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
242
      result = %{
lain's avatar
lain committed
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
        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
259
260
261
262
263
264
      }

      json(conn, result)
    end
  end

Thog's avatar
Thog committed
265
  def post_status(%{assigns: %{user: user}} = conn, %{"status" => _} = params) do
lain's avatar
lain committed
266
267
268
269
    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
270

lain's avatar
lain committed
271
272
273
274
275
276
277
    idempotency_key =
      case get_req_header(conn, "idempotency-key") do
        [key] -> key
        _ -> Ecto.UUID.generate()
      end

    {:ok, activity} =
Thog's avatar
Thog committed
278
      Cachex.fetch!(:idempotency_cache, idempotency_key, fn _ -> CommonAPI.post(user, params) end)
lain's avatar
lain committed
279

lain's avatar
lain committed
280
    render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
lain's avatar
lain committed
281
  end
lain's avatar
lain committed
282
283
284
285
286
287
288
289
290
291
292

  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
293
294

  def reblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
lain's avatar
lain committed
295
    with {:ok, announce, _activity} = CommonAPI.repeat(ap_id_or_id, user) do
lain's avatar
lain committed
296
      render(conn, StatusView, "status.json", %{activity: announce, for: user, as: :activity})
lain's avatar
lain committed
297
298
    end
  end
lain's avatar
lain committed
299

normandy's avatar
normandy committed
300
  def unreblog_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
normandy's avatar
normandy committed
301
    with {:ok, _, _, %{data: %{"id" => id}}} = CommonAPI.unrepeat(ap_id_or_id, user),
302
303
         %Activity{} = activity <- Activity.get_create_activity_by_object_ap_id(id) do
      render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
normandy's avatar
normandy committed
304
305
306
    end
  end

lain's avatar
lain committed
307
  def fav_status(%{assigns: %{user: user}} = conn, %{"id" => ap_id_or_id}) do
lain's avatar
lain committed
308
309
    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
310
      render(conn, StatusView, "status.json", %{activity: activity, for: user, as: :activity})
lain's avatar
lain committed
311
312
313
314
    end
  end

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

321
322
  def notifications(%{assigns: %{user: user}} = conn, params) do
    notifications = Notification.for_user(user, params)
lain's avatar
lain committed
323
324
325
326
327
328

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

lain's avatar
lain committed
330
331
332
    conn
    |> add_link_headers(:notifications, notifications)
    |> json(result)
333
334
  end

335
336
337
338
339
340
341
  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
342
        |> send_resp(403, Jason.encode!(%{"error" => reason}))
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
    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
358
        |> send_resp(403, Jason.encode!(%{"error" => reason}))
359
360
361
    end
  end

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

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

lain's avatar
lain committed
375
      render(conn, StatusView, "attachment.json", %{attachment: data})
lain's avatar
lain committed
376
377
378
    end
  end

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

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

lain's avatar
lain committed
406
407
408
    activities =
      ActivityPub.fetch_public_activities(params)
      |> Enum.reverse()
Roger Braun's avatar
Roger Braun committed
409
410

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

415
416
417
418
  # 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
419
      render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
420
421
422
423
424
425
    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
426
      render(conn, AccountView, "accounts.json", %{users: followers, as: :user})
427
428
429
    end
  end

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

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

456
457
  def unfollow(%{assigns: %{user: follower}} = conn, %{"id" => id}) do
    with %User{} = followed <- Repo.get(User, id),
458
459
         {:ok, _activity} <- ActivityPub.unfollow(follower, followed),
         {:ok, follower, _} <- User.unfollow(follower, followed) do
lain's avatar
lain committed
460
      render(conn, AccountView, "relationship.json", %{user: follower, target: followed})
461
462
463
    end
  end

lain's avatar
lain committed
464
465
  def block(%{assigns: %{user: blocker}} = conn, %{"id" => id}) do
    with %User{} = blocked <- Repo.get(User, id),
466
467
         {:ok, blocker} <- User.block(blocker, blocked),
         {:ok, _activity} <- ActivityPub.block(blocker, blocked) do
lain's avatar
lain committed
468
      render(conn, AccountView, "relationship.json", %{user: blocker, target: blocked})
lain's avatar
lain committed
469
    else
Thog's avatar
Thog committed
470
      {:error, message} ->
lain's avatar
lain committed
471
472
        conn
        |> put_resp_content_type("application/json")
lain's avatar
lain committed
473
        |> send_resp(403, Jason.encode!(%{"error" => message}))
lain's avatar
lain committed
474
475
476
477
478
    end
  end

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

lain's avatar
lain committed
490
491
492
  # TODO: Use proper query
  def blocks(%{assigns: %{user: user}} = conn, _) do
    with blocked_users <- user.info["blocks"] || [],
lain's avatar
lain committed
493
         accounts <- Enum.map(blocked_users, fn ap_id -> User.get_cached_by_ap_id(ap_id) end) do
lain's avatar
lain committed
494
495
496
497
498
      res = AccountView.render("accounts.json", users: accounts, for: user, as: :user)
      json(conn, res)
    end
  end

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

lain's avatar
lain committed
502
503
504
505
    fetched =
      if Regex.match?(~r/https?:/, query) do
        with {:ok, activities} <- OStatus.fetch_activity_from_url(query) do
          activities
506
507
508
509
          |> Enum.filter(fn
            %{data: %{"type" => "Create"}} -> true
            _ -> false
          end)
lain's avatar
lain committed
510
511
512
513
514
515
516
517
518
        else
          _e -> []
        end
      end || []

    q =
      from(
        a in Activity,
        where: fragment("?->>'type' = 'Create'", a.data),
lain's avatar
lain committed
519
        where: "https://www.w3.org/ns/activitystreams#Public" in a.recipients,
lain's avatar
lain committed
520
521
522
523
524
525
        where:
          fragment(
            "to_tsvector('english', ?->'object'->>'content') @@ plainto_tsquery('english', ?)",
            a.data,
            ^query
          ),
lain's avatar
lain committed
526
        limit: 20,
lain's avatar
lain committed
527
        order_by: [desc: :id]
lain's avatar
lain committed
528
      )
529
530

    statuses = Repo.all(q) ++ fetched
lain's avatar
lain committed
531
532
533
534
535
536

    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
537
538
539

    res = %{
      "accounts" => AccountView.render("accounts.json", users: accounts, for: user, as: :user),
lain's avatar
lain committed
540
541
      "statuses" =>
        StatusView.render("index.json", activities: statuses, for: user, as: :activity),
542
      "hashtags" => tags
lain's avatar
lain committed
543
544
545
546
547
    }

    json(conn, res)
  end

lain's avatar
lain committed
548
549
  def account_search(%{assigns: %{user: user}} = conn, %{"q" => query} = params) do
    accounts = User.search(query, params["resolve"] == "true")
550
551
552
553
554
555

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

    json(conn, res)
  end

Thog's avatar
Thog committed
556
  def favourites(%{assigns: %{user: user}} = conn, _) do
lain's avatar
lain committed
557
558
559
560
561
    params =
      %{}
      |> Map.put("type", "Create")
      |> Map.put("favorited_by", user.ap_id)
      |> Map.put("blocking_user", user)
562

lain's avatar
lain committed
563
564
565
    activities =
      ActivityPub.fetch_public_activities(params)
      |> Enum.reverse()
566
567
568
569
570

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

eal's avatar
eal committed
571
572
573
574
575
576
577
  def get_lists(%{assigns: %{user: user}} = conn, opts) do
    lists = Pleroma.List.for_user(user, opts)
    res = ListView.render("lists.json", lists: lists)
    json(conn, res)
  end

  def get_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
eal's avatar
eal committed
578
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user) do
eal's avatar
eal committed
579
580
581
582
583
584
585
586
      res = ListView.render("list.json", list: list)
      json(conn, res)
    else
      _e -> json(conn, "error")
    end
  end

  def delete_list(%{assigns: %{user: user}} = conn, %{"id" => id}) do
eal's avatar
eal committed
587
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
         {:ok, _list} <- Pleroma.List.delete(list) do
      json(conn, %{})
    else
      _e ->
        json(conn, "error")
    end
  end

  def create_list(%{assigns: %{user: user}} = conn, %{"title" => title}) do
    with {:ok, %Pleroma.List{} = list} <- Pleroma.List.create(title, user) do
      res = ListView.render("list.json", list: list)
      json(conn, res)
    end
  end

  def add_to_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
    accounts
    |> Enum.each(fn account_id ->
eal's avatar
eal committed
606
      with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
607
           %User{} = followed <- Repo.get(User, account_id) do
eal's avatar
eal committed
608
        Pleroma.List.follow(list, followed)
eal's avatar
eal committed
609
610
611
612
613
614
615
616
617
      end
    end)

    json(conn, %{})
  end

  def remove_from_list(%{assigns: %{user: user}} = conn, %{"id" => id, "account_ids" => accounts}) do
    accounts
    |> Enum.each(fn account_id ->
eal's avatar
eal committed
618
      with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
619
620
621
622
623
624
625
626
627
           %User{} = followed <- Repo.get(Pleroma.User, account_id) do
        Pleroma.List.unfollow(list, followed)
      end
    end)

    json(conn, %{})
  end

  def list_accounts(%{assigns: %{user: user}} = conn, %{"id" => id}) do
eal's avatar
eal committed
628
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
629
630
631
632
633
634
         {:ok, users} = Pleroma.List.get_following(list) do
      render(conn, AccountView, "accounts.json", %{users: users, as: :user})
    end
  end

  def rename_list(%{assigns: %{user: user}} = conn, %{"id" => id, "title" => title}) do
eal's avatar
eal committed
635
    with %Pleroma.List{} = list <- Pleroma.List.get(id, user),
eal's avatar
eal committed
636
637
638
639
640
641
642
643
644
645
         {:ok, list} <- Pleroma.List.rename(list, title) do
      res = ListView.render("list.json", list: list)
      json(conn, res)
    else
      _e ->
        json(conn, "error")
    end
  end

  def list_timeline(%{assigns: %{user: user}} = conn, %{"list_id" => id} = params) do
eal's avatar
eal committed
646
    with %Pleroma.List{title: title, following: following} <- Pleroma.List.get(id, user) do
eal's avatar
eal committed
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
      params =
        params
        |> Map.put("type", "Create")
        |> Map.put("blocking_user", user)

      # adding title is a hack to not make empty lists function like a public timeline
      activities =
        ActivityPub.fetch_activities([title | following], params)
        |> Enum.reverse()

      conn
      |> render(StatusView, "index.json", %{activities: activities, for: user, as: :activity})
    else
      _e ->
        conn
        |> put_status(403)
        |> json(%{error: "Error."})
    end
  end

lain's avatar
lain committed
667
  def index(%{assigns: %{user: user}} = conn, _params) do
lain's avatar
lain committed
668
669
670
    token =
      conn
      |> get_session(:oauth_token)
lain's avatar
lain committed
671
672

    if user && token do
673
      mastodon_emoji = mastodonized_emoji()
lain's avatar
lain committed
674
      accounts = Map.put(%{}, user.id, AccountView.render("account.json", %{user: user}))
lain's avatar
lain committed
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690

      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
691
          },
lain's avatar
lain committed
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
          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
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
          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
743
744
745
746
747
748
749
750
              },
          push_subscription: nil,
          accounts: accounts,
          custom_emojis: mastodon_emoji,
          char_limit: Keyword.get(@instance, :limit)
        }
        |> Jason.encode!()

lain's avatar
lain committed
751
752
753
754
755
756
757
758
759
      conn
      |> put_layout(false)
      |> render(MastodonView, "index.html", %{initial_state: initial_state})
    else
      conn
      |> redirect(to: "/web/login")
    end
  end

760
761
762
763
764
765
  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
766
767
    else
      e ->
768
769
770
771
772
        conn
        |> json(%{error: inspect(e)})
    end
  end

Thog's avatar
Thog committed
773
  def login(conn, _) do
lain's avatar
lain committed
774
    conn
775
    |> render(MastodonView, "login.html", %{error: false})
lain's avatar
lain committed
776
777
778
779
780
781
782
  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
783
784
785
786
787
788
789
        cs =
          App.register_changeset(%App{}, %{
            client_name: "Mastodon-Local",
            redirect_uris: ".",
            scopes: "read,write,follow"
          })

lain's avatar
lain committed
790
791
792
793
        Repo.insert(cs)
    end
  end

lain's avatar
lain committed
794
  def login_post(conn, %{"authorization" => %{"name" => name, "password" => password}}) do
795
    with %User{} = user <- User.get_by_nickname_or_email(name),
lain's avatar
lain committed
796
797
798
799
800
801
         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
802
      |> redirect(to: "/web/getting-started")
803
804
805
806
    else
      _e ->
        conn
        |> render(MastodonView, "login.html", %{error: "Wrong username or password"})
lain's avatar
lain committed
807
808
809
    end
  end

lain's avatar
lain committed
810
811
812
813
814
815
  def logout(conn, _) do
    conn
    |> clear_session
    |> redirect(to: "/")
  end

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

819
    with %User{} = target <- Repo.get(User, id) do
lain's avatar
lain committed
820
      render(conn, AccountView, "relationship.json", %{user: user, target: target})
821
822
823
    end
  end

824
825
826
827
  def empty_array(conn, _) do
    Logger.debug("Unimplemented, returning an empty array")
    json(conn, [])
  end
828

829
830
831
832
833
  def empty_object(conn, _) do
    Logger.debug("Unimplemented, returning an empty object")
    json(conn, %{})
  end

834
  def render_notification(user, %{id: id, activity: activity, inserted_at: created_at} = _params) do
835
    actor = User.get_cached_by_ap_id(activity.data["actor"])
lain's avatar
lain committed
836
837
838
839
840

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

841
842
    case activity.data["type"] do
      "Create" ->
lain's avatar
lain committed
843
844
845
846
847
848
849
850
        %{
          id: id,
          type: "mention",
          created_at: created_at,
          account: AccountView.render("account.json", %{user: actor}),
          status: StatusView.render("status.json", %{activity: activity, for: user})
        }

851
852
      "Like" ->
        liked_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
lain's avatar
lain committed
853
854
855
856
857
858
859
860
861

        %{
          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})
        }

862
863
      "Announce" ->
        announced_activity = Activity.get_create_activity_by_object_ap_id(activity.data["object"])
lain's avatar
lain committed
864
865
866
867
868
869
870
871
872

        %{
          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})
        }

873
      "Follow" ->
lain's avatar
lain committed
874
875
876
877
878
879
880
881
882
        %{
          id: id,
          type: "follow",
          created_at: created_at,
          account: AccountView.render("account.json", %{user: actor})
        }

      _ ->
        nil
883
884
    end
  end
885
end