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