1b6beb6d29437d2354ceccdf734f6c5f54f4d195
[akkoma] / test / web / mastodon_api / status_view_test.exs
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2018 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.MastodonAPI.StatusViewTest do
6 use Pleroma.DataCase
7
8 alias Pleroma.Activity
9 alias Pleroma.Bookmark
10 alias Pleroma.Object
11 alias Pleroma.Repo
12 alias Pleroma.User
13 alias Pleroma.Web.CommonAPI
14 alias Pleroma.Web.CommonAPI.Utils
15 alias Pleroma.Web.MastodonAPI.AccountView
16 alias Pleroma.Web.MastodonAPI.StatusView
17 alias Pleroma.Web.OStatus
18 import Pleroma.Factory
19 import Tesla.Mock
20
21 setup do
22 mock(fn env -> apply(HttpRequestMock, :request, [env]) end)
23 :ok
24 end
25
26 test "returns the direct conversation id when given the `with_conversation_id` option" do
27 user = insert(:user)
28
29 {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
30
31 status =
32 StatusView.render("status.json",
33 activity: activity,
34 with_direct_conversation_id: true,
35 for: user
36 )
37
38 assert status[:pleroma][:direct_conversation_id]
39 end
40
41 test "returns a temporary ap_id based user for activities missing db users" do
42 user = insert(:user)
43
44 {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
45
46 Repo.delete(user)
47 Cachex.clear(:user_cache)
48
49 %{account: ms_user} = StatusView.render("status.json", activity: activity)
50
51 assert ms_user.acct == "erroruser@example.com"
52 end
53
54 test "tries to get a user by nickname if fetching by ap_id doesn't work" do
55 user = insert(:user)
56
57 {:ok, activity} = CommonAPI.post(user, %{"status" => "Hey @shp!", "visibility" => "direct"})
58
59 {:ok, user} =
60 user
61 |> Ecto.Changeset.change(%{ap_id: "#{user.ap_id}/extension/#{user.nickname}"})
62 |> Repo.update()
63
64 Cachex.clear(:user_cache)
65
66 result = StatusView.render("status.json", activity: activity)
67
68 assert result[:account][:id] == to_string(user.id)
69 end
70
71 test "a note with null content" do
72 note = insert(:note_activity)
73 note_object = Object.normalize(note)
74
75 data =
76 note_object.data
77 |> Map.put("content", nil)
78
79 Object.change(note_object, %{data: data})
80 |> Object.update_and_set_cache()
81
82 User.get_cached_by_ap_id(note.data["actor"])
83
84 status = StatusView.render("status.json", %{activity: note})
85
86 assert status.content == ""
87 end
88
89 test "a note activity" do
90 note = insert(:note_activity)
91 object_data = Object.normalize(note).data
92 user = User.get_cached_by_ap_id(note.data["actor"])
93
94 convo_id = Utils.context_to_conversation_id(object_data["context"])
95
96 status = StatusView.render("status.json", %{activity: note})
97
98 created_at =
99 (object_data["published"] || "")
100 |> String.replace(~r/\.\d+Z/, ".000Z")
101
102 expected = %{
103 id: to_string(note.id),
104 uri: object_data["id"],
105 url: Pleroma.Web.Router.Helpers.o_status_url(Pleroma.Web.Endpoint, :notice, note),
106 account: AccountView.render("account.json", %{user: user}),
107 in_reply_to_id: nil,
108 in_reply_to_account_id: nil,
109 card: nil,
110 reblog: nil,
111 content: HtmlSanitizeEx.basic_html(object_data["content"]),
112 created_at: created_at,
113 reblogs_count: 0,
114 replies_count: 0,
115 favourites_count: 0,
116 reblogged: false,
117 bookmarked: false,
118 favourited: false,
119 muted: false,
120 pinned: false,
121 sensitive: false,
122 poll: nil,
123 spoiler_text: HtmlSanitizeEx.basic_html(object_data["summary"]),
124 visibility: "public",
125 media_attachments: [],
126 mentions: [],
127 tags: [
128 %{
129 name: "#{object_data["tag"]}",
130 url: "/tag/#{object_data["tag"]}"
131 }
132 ],
133 application: %{
134 name: "Web",
135 website: nil
136 },
137 language: nil,
138 emojis: [
139 %{
140 shortcode: "2hu",
141 url: "corndog.png",
142 static_url: "corndog.png",
143 visible_in_picker: false
144 }
145 ],
146 pleroma: %{
147 local: true,
148 conversation_id: convo_id,
149 in_reply_to_account_acct: nil,
150 content: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["content"])},
151 spoiler_text: %{"text/plain" => HtmlSanitizeEx.strip_tags(object_data["summary"])},
152 expires_at: nil,
153 direct_conversation_id: nil
154 }
155 }
156
157 assert status == expected
158 end
159
160 test "tells if the message is muted for some reason" do
161 user = insert(:user)
162 other_user = insert(:user)
163
164 {:ok, user} = User.mute(user, other_user)
165
166 {:ok, activity} = CommonAPI.post(other_user, %{"status" => "test"})
167 status = StatusView.render("status.json", %{activity: activity})
168
169 assert status.muted == false
170
171 status = StatusView.render("status.json", %{activity: activity, for: user})
172
173 assert status.muted == true
174 end
175
176 test "tells if the status is bookmarked" do
177 user = insert(:user)
178
179 {:ok, activity} = CommonAPI.post(user, %{"status" => "Cute girls doing cute things"})
180 status = StatusView.render("status.json", %{activity: activity})
181
182 assert status.bookmarked == false
183
184 status = StatusView.render("status.json", %{activity: activity, for: user})
185
186 assert status.bookmarked == false
187
188 {:ok, _bookmark} = Bookmark.create(user.id, activity.id)
189
190 activity = Activity.get_by_id_with_object(activity.id)
191
192 status = StatusView.render("status.json", %{activity: activity, for: user})
193
194 assert status.bookmarked == true
195 end
196
197 test "a reply" do
198 note = insert(:note_activity)
199 user = insert(:user)
200
201 {:ok, activity} =
202 CommonAPI.post(user, %{"status" => "he", "in_reply_to_status_id" => note.id})
203
204 status = StatusView.render("status.json", %{activity: activity})
205
206 assert status.in_reply_to_id == to_string(note.id)
207
208 [status] = StatusView.render("index.json", %{activities: [activity], as: :activity})
209
210 assert status.in_reply_to_id == to_string(note.id)
211 end
212
213 test "contains mentions" do
214 incoming = File.read!("test/fixtures/incoming_reply_mastodon.xml")
215 # a user with this ap id might be in the cache.
216 recipient = "https://pleroma.soykaf.com/users/lain"
217 user = insert(:user, %{ap_id: recipient})
218
219 {:ok, [activity]} = OStatus.handle_incoming(incoming)
220
221 status = StatusView.render("status.json", %{activity: activity})
222
223 assert status.mentions ==
224 Enum.map([user], fn u -> AccountView.render("mention.json", %{user: u}) end)
225 end
226
227 test "create mentions from the 'to' field" do
228 %User{ap_id: recipient_ap_id} = insert(:user)
229 cc = insert_pair(:user) |> Enum.map(& &1.ap_id)
230
231 object =
232 insert(:note, %{
233 data: %{
234 "to" => [recipient_ap_id],
235 "cc" => cc
236 }
237 })
238
239 activity =
240 insert(:note_activity, %{
241 note: object,
242 recipients: [recipient_ap_id | cc]
243 })
244
245 assert length(activity.recipients) == 3
246
247 %{mentions: [mention] = mentions} = StatusView.render("status.json", %{activity: activity})
248
249 assert length(mentions) == 1
250 assert mention.url == recipient_ap_id
251 end
252
253 test "create mentions from the 'tag' field" do
254 recipient = insert(:user)
255 cc = insert_pair(:user) |> Enum.map(& &1.ap_id)
256
257 object =
258 insert(:note, %{
259 data: %{
260 "cc" => cc,
261 "tag" => [
262 %{
263 "href" => recipient.ap_id,
264 "name" => recipient.nickname,
265 "type" => "Mention"
266 },
267 %{
268 "href" => "https://example.com/search?tag=test",
269 "name" => "#test",
270 "type" => "Hashtag"
271 }
272 ]
273 }
274 })
275
276 activity =
277 insert(:note_activity, %{
278 note: object,
279 recipients: [recipient.ap_id | cc]
280 })
281
282 assert length(activity.recipients) == 3
283
284 %{mentions: [mention] = mentions} = StatusView.render("status.json", %{activity: activity})
285
286 assert length(mentions) == 1
287 assert mention.url == recipient.ap_id
288 end
289
290 test "attachments" do
291 object = %{
292 "type" => "Image",
293 "url" => [
294 %{
295 "mediaType" => "image/png",
296 "href" => "someurl"
297 }
298 ],
299 "uuid" => 6
300 }
301
302 expected = %{
303 id: "1638338801",
304 type: "image",
305 url: "someurl",
306 remote_url: "someurl",
307 preview_url: "someurl",
308 text_url: "someurl",
309 description: nil,
310 pleroma: %{mime_type: "image/png"}
311 }
312
313 assert expected == StatusView.render("attachment.json", %{attachment: object})
314
315 # If theres a "id", use that instead of the generated one
316 object = Map.put(object, "id", 2)
317 assert %{id: "2"} = StatusView.render("attachment.json", %{attachment: object})
318 end
319
320 test "put the url advertised in the Activity in to the url attribute" do
321 id = "https://wedistribute.org/wp-json/pterotype/v1/object/85810"
322 [activity] = Activity.search(nil, id)
323
324 status = StatusView.render("status.json", %{activity: activity})
325
326 assert status.uri == id
327 assert status.url == "https://wedistribute.org/2019/07/mastodon-drops-ostatus/"
328 end
329
330 test "a reblog" do
331 user = insert(:user)
332 activity = insert(:note_activity)
333
334 {:ok, reblog, _} = CommonAPI.repeat(activity.id, user)
335
336 represented = StatusView.render("status.json", %{for: user, activity: reblog})
337
338 assert represented[:id] == to_string(reblog.id)
339 assert represented[:reblog][:id] == to_string(activity.id)
340 assert represented[:emojis] == []
341 end
342
343 test "a peertube video" do
344 user = insert(:user)
345
346 {:ok, object} =
347 Pleroma.Object.Fetcher.fetch_object_from_id(
348 "https://peertube.moe/videos/watch/df5f464b-be8d-46fb-ad81-2d4c2d1630e3"
349 )
350
351 %Activity{} = activity = Activity.get_create_by_object_ap_id(object.data["id"])
352
353 represented = StatusView.render("status.json", %{for: user, activity: activity})
354
355 assert represented[:id] == to_string(activity.id)
356 assert length(represented[:media_attachments]) == 1
357 end
358
359 describe "build_tags/1" do
360 test "it returns a a dictionary tags" do
361 object_tags = [
362 "fediverse",
363 "mastodon",
364 "nextcloud",
365 %{
366 "href" => "https://kawen.space/users/lain",
367 "name" => "@lain@kawen.space",
368 "type" => "Mention"
369 }
370 ]
371
372 assert StatusView.build_tags(object_tags) == [
373 %{name: "fediverse", url: "/tag/fediverse"},
374 %{name: "mastodon", url: "/tag/mastodon"},
375 %{name: "nextcloud", url: "/tag/nextcloud"}
376 ]
377 end
378 end
379
380 describe "rich media cards" do
381 test "a rich media card without a site name renders correctly" do
382 page_url = "http://example.com"
383
384 card = %{
385 url: page_url,
386 image: page_url <> "/example.jpg",
387 title: "Example website"
388 }
389
390 %{provider_name: "example.com"} =
391 StatusView.render("card.json", %{page_url: page_url, rich_media: card})
392 end
393
394 test "a rich media card without a site name or image renders correctly" do
395 page_url = "http://example.com"
396
397 card = %{
398 url: page_url,
399 title: "Example website"
400 }
401
402 %{provider_name: "example.com"} =
403 StatusView.render("card.json", %{page_url: page_url, rich_media: card})
404 end
405
406 test "a rich media card without an image renders correctly" do
407 page_url = "http://example.com"
408
409 card = %{
410 url: page_url,
411 site_name: "Example site name",
412 title: "Example website"
413 }
414
415 %{provider_name: "Example site name"} =
416 StatusView.render("card.json", %{page_url: page_url, rich_media: card})
417 end
418
419 test "a rich media card with all relevant data renders correctly" do
420 page_url = "http://example.com"
421
422 card = %{
423 url: page_url,
424 site_name: "Example site name",
425 title: "Example website",
426 image: page_url <> "/example.jpg",
427 description: "Example description"
428 }
429
430 %{provider_name: "Example site name"} =
431 StatusView.render("card.json", %{page_url: page_url, rich_media: card})
432 end
433 end
434
435 describe "poll view" do
436 test "renders a poll" do
437 user = insert(:user)
438
439 {:ok, activity} =
440 CommonAPI.post(user, %{
441 "status" => "Is Tenshi eating a corndog cute?",
442 "poll" => %{
443 "options" => ["absolutely!", "sure", "yes", "why are you even asking?"],
444 "expires_in" => 20
445 }
446 })
447
448 object = Object.normalize(activity)
449
450 expected = %{
451 emojis: [],
452 expired: false,
453 id: to_string(object.id),
454 multiple: false,
455 options: [
456 %{title: "absolutely!", votes_count: 0},
457 %{title: "sure", votes_count: 0},
458 %{title: "yes", votes_count: 0},
459 %{title: "why are you even asking?", votes_count: 0}
460 ],
461 voted: false,
462 votes_count: 0
463 }
464
465 result = StatusView.render("poll.json", %{object: object})
466 expires_at = result.expires_at
467 result = Map.delete(result, :expires_at)
468
469 assert result == expected
470
471 expires_at = NaiveDateTime.from_iso8601!(expires_at)
472 assert NaiveDateTime.diff(expires_at, NaiveDateTime.utc_now()) in 15..20
473 end
474
475 test "detects if it is multiple choice" do
476 user = insert(:user)
477
478 {:ok, activity} =
479 CommonAPI.post(user, %{
480 "status" => "Which Mastodon developer is your favourite?",
481 "poll" => %{
482 "options" => ["Gargron", "Eugen"],
483 "expires_in" => 20,
484 "multiple" => true
485 }
486 })
487
488 object = Object.normalize(activity)
489
490 assert %{multiple: true} = StatusView.render("poll.json", %{object: object})
491 end
492
493 test "detects emoji" do
494 user = insert(:user)
495
496 {:ok, activity} =
497 CommonAPI.post(user, %{
498 "status" => "What's with the smug face?",
499 "poll" => %{
500 "options" => [":blank: sip", ":blank::blank: sip", ":blank::blank::blank: sip"],
501 "expires_in" => 20
502 }
503 })
504
505 object = Object.normalize(activity)
506
507 assert %{emojis: [%{shortcode: "blank"}]} =
508 StatusView.render("poll.json", %{object: object})
509 end
510
511 test "detects vote status" do
512 user = insert(:user)
513 other_user = insert(:user)
514
515 {:ok, activity} =
516 CommonAPI.post(user, %{
517 "status" => "Which input devices do you use?",
518 "poll" => %{
519 "options" => ["mouse", "trackball", "trackpoint"],
520 "multiple" => true,
521 "expires_in" => 20
522 }
523 })
524
525 object = Object.normalize(activity)
526
527 {:ok, _, object} = CommonAPI.vote(other_user, object, [1, 2])
528
529 result = StatusView.render("poll.json", %{object: object, for: other_user})
530
531 assert result[:voted] == true
532 assert Enum.at(result[:options], 1)[:votes_count] == 1
533 assert Enum.at(result[:options], 2)[:votes_count] == 1
534 end
535 end
536
537 test "embeds a relationship in the account" do
538 user = insert(:user)
539 other_user = insert(:user)
540
541 {:ok, activity} =
542 CommonAPI.post(user, %{
543 "status" => "drink more water"
544 })
545
546 result = StatusView.render("status.json", %{activity: activity, for: other_user})
547
548 assert result[:account][:pleroma][:relationship] ==
549 AccountView.render("relationship.json", %{user: other_user, target: user})
550 end
551
552 test "embeds a relationship in the account in reposts" do
553 user = insert(:user)
554 other_user = insert(:user)
555
556 {:ok, activity} =
557 CommonAPI.post(user, %{
558 "status" => "˙˙ɐʎns"
559 })
560
561 {:ok, activity, _object} = CommonAPI.repeat(activity.id, other_user)
562
563 result = StatusView.render("status.json", %{activity: activity, for: user})
564
565 assert result[:account][:pleroma][:relationship] ==
566 AccountView.render("relationship.json", %{user: user, target: other_user})
567
568 assert result[:reblog][:account][:pleroma][:relationship] ==
569 AccountView.render("relationship.json", %{user: user, target: user})
570 end
571
572 test "visibility/list" do
573 user = insert(:user)
574
575 {:ok, list} = Pleroma.List.create("foo", user)
576
577 {:ok, activity} =
578 CommonAPI.post(user, %{"status" => "foobar", "visibility" => "list:#{list.id}"})
579
580 status = StatusView.render("status.json", activity: activity)
581
582 assert status.visibility == "list"
583 end
584 end