Merge branch 'develop' into feature/admin-api-status-count-per-instance
[akkoma] / lib / pleroma / web / api_spec / operations / account_operation.ex
1 # Pleroma: A lightweight social networking server
2 # Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
3 # SPDX-License-Identifier: AGPL-3.0-only
4
5 defmodule Pleroma.Web.ApiSpec.AccountOperation do
6 alias OpenApiSpex.Operation
7 alias OpenApiSpex.Reference
8 alias OpenApiSpex.Schema
9 alias Pleroma.Web.ApiSpec.Schemas.Account
10 alias Pleroma.Web.ApiSpec.Schemas.AccountRelationship
11 alias Pleroma.Web.ApiSpec.Schemas.ActorType
12 alias Pleroma.Web.ApiSpec.Schemas.ApiError
13 alias Pleroma.Web.ApiSpec.Schemas.BooleanLike
14 alias Pleroma.Web.ApiSpec.Schemas.List
15 alias Pleroma.Web.ApiSpec.Schemas.Status
16 alias Pleroma.Web.ApiSpec.Schemas.VisibilityScope
17
18 import Pleroma.Web.ApiSpec.Helpers
19
20 @spec open_api_operation(atom) :: Operation.t()
21 def open_api_operation(action) do
22 operation = String.to_existing_atom("#{action}_operation")
23 apply(__MODULE__, operation, [])
24 end
25
26 @spec create_operation() :: Operation.t()
27 def create_operation do
28 %Operation{
29 tags: ["accounts"],
30 summary: "Register an account",
31 description:
32 "Creates a user and account records. Returns an account access token for the app that initiated the request. The app should save this token for later, and should wait for the user to confirm their account by clicking a link in their email inbox.",
33 operationId: "AccountController.create",
34 requestBody: request_body("Parameters", create_request(), required: true),
35 responses: %{
36 200 => Operation.response("Account", "application/json", create_response()),
37 400 => Operation.response("Error", "application/json", ApiError),
38 403 => Operation.response("Error", "application/json", ApiError),
39 429 => Operation.response("Error", "application/json", ApiError)
40 }
41 }
42 end
43
44 def verify_credentials_operation do
45 %Operation{
46 tags: ["accounts"],
47 description: "Test to make sure that the user token works.",
48 summary: "Verify account credentials",
49 operationId: "AccountController.verify_credentials",
50 security: [%{"oAuth" => ["read:accounts"]}],
51 responses: %{
52 200 => Operation.response("Account", "application/json", Account)
53 }
54 }
55 end
56
57 def update_credentials_operation do
58 %Operation{
59 tags: ["accounts"],
60 summary: "Update account credentials",
61 description: "Update the user's display and preferences.",
62 operationId: "AccountController.update_credentials",
63 security: [%{"oAuth" => ["write:accounts"]}],
64 requestBody: request_body("Parameters", update_creadentials_request(), required: true),
65 responses: %{
66 200 => Operation.response("Account", "application/json", Account),
67 403 => Operation.response("Error", "application/json", ApiError)
68 }
69 }
70 end
71
72 def relationships_operation do
73 %Operation{
74 tags: ["accounts"],
75 summary: "Check relationships to other accounts",
76 operationId: "AccountController.relationships",
77 description: "Find out whether a given account is followed, blocked, muted, etc.",
78 security: [%{"oAuth" => ["read:follows"]}],
79 parameters: [
80 Operation.parameter(
81 :id,
82 :query,
83 %Schema{
84 oneOf: [%Schema{type: :array, items: %Schema{type: :string}}, %Schema{type: :string}]
85 },
86 "Account IDs",
87 example: "123"
88 )
89 ],
90 responses: %{
91 200 => Operation.response("Account", "application/json", array_of_relationships())
92 }
93 }
94 end
95
96 def show_operation do
97 %Operation{
98 tags: ["accounts"],
99 summary: "Account",
100 operationId: "AccountController.show",
101 description: "View information about a profile.",
102 parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
103 responses: %{
104 200 => Operation.response("Account", "application/json", Account),
105 404 => Operation.response("Error", "application/json", ApiError)
106 }
107 }
108 end
109
110 def statuses_operation do
111 %Operation{
112 tags: ["accounts"],
113 summary: "Statuses",
114 operationId: "AccountController.statuses",
115 description:
116 "Statuses posted to the given account. Public (for public statuses only), or user token + `read:statuses` (for private statuses the user is authorized to see)",
117 parameters:
118 [
119 %Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
120 Operation.parameter(:pinned, :query, BooleanLike, "Include only pinned statuses"),
121 Operation.parameter(:tagged, :query, :string, "With tag"),
122 Operation.parameter(
123 :only_media,
124 :query,
125 BooleanLike,
126 "Include only statuses with media attached"
127 ),
128 Operation.parameter(
129 :with_muted,
130 :query,
131 BooleanLike,
132 "Include statuses from muted acccounts."
133 ),
134 Operation.parameter(:exclude_reblogs, :query, BooleanLike, "Exclude reblogs"),
135 Operation.parameter(:exclude_replies, :query, BooleanLike, "Exclude replies"),
136 Operation.parameter(
137 :exclude_visibilities,
138 :query,
139 %Schema{type: :array, items: VisibilityScope},
140 "Exclude visibilities"
141 )
142 ] ++ pagination_params(),
143 responses: %{
144 200 => Operation.response("Statuses", "application/json", array_of_statuses()),
145 404 => Operation.response("Error", "application/json", ApiError)
146 }
147 }
148 end
149
150 def followers_operation do
151 %Operation{
152 tags: ["accounts"],
153 summary: "Followers",
154 operationId: "AccountController.followers",
155 security: [%{"oAuth" => ["read:accounts"]}],
156 description:
157 "Accounts which follow the given account, if network is not hidden by the account owner.",
158 parameters: [
159 %Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
160 with_relationships_param() | pagination_params()
161 ],
162 responses: %{
163 200 => Operation.response("Accounts", "application/json", array_of_accounts())
164 }
165 }
166 end
167
168 def following_operation do
169 %Operation{
170 tags: ["accounts"],
171 summary: "Following",
172 operationId: "AccountController.following",
173 security: [%{"oAuth" => ["read:accounts"]}],
174 description:
175 "Accounts which the given account is following, if network is not hidden by the account owner.",
176 parameters: [
177 %Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
178 with_relationships_param() | pagination_params()
179 ],
180 responses: %{200 => Operation.response("Accounts", "application/json", array_of_accounts())}
181 }
182 end
183
184 def lists_operation do
185 %Operation{
186 tags: ["accounts"],
187 summary: "Lists containing this account",
188 operationId: "AccountController.lists",
189 security: [%{"oAuth" => ["read:lists"]}],
190 description: "User lists that you have added this account to.",
191 parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
192 responses: %{200 => Operation.response("Lists", "application/json", array_of_lists())}
193 }
194 end
195
196 def follow_operation do
197 %Operation{
198 tags: ["accounts"],
199 summary: "Follow",
200 operationId: "AccountController.follow",
201 security: [%{"oAuth" => ["follow", "write:follows"]}],
202 description: "Follow the given account",
203 parameters: [
204 %Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
205 Operation.parameter(
206 :reblogs,
207 :query,
208 BooleanLike,
209 "Receive this account's reblogs in home timeline? Defaults to true."
210 )
211 ],
212 responses: %{
213 200 => Operation.response("Relationship", "application/json", AccountRelationship),
214 400 => Operation.response("Error", "application/json", ApiError),
215 404 => Operation.response("Error", "application/json", ApiError)
216 }
217 }
218 end
219
220 def unfollow_operation do
221 %Operation{
222 tags: ["accounts"],
223 summary: "Unfollow",
224 operationId: "AccountController.unfollow",
225 security: [%{"oAuth" => ["follow", "write:follows"]}],
226 description: "Unfollow the given account",
227 parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
228 responses: %{
229 200 => Operation.response("Relationship", "application/json", AccountRelationship),
230 400 => Operation.response("Error", "application/json", ApiError),
231 404 => Operation.response("Error", "application/json", ApiError)
232 }
233 }
234 end
235
236 def mute_operation do
237 %Operation{
238 tags: ["accounts"],
239 summary: "Mute",
240 operationId: "AccountController.mute",
241 security: [%{"oAuth" => ["follow", "write:mutes"]}],
242 requestBody: request_body("Parameters", mute_request()),
243 description:
244 "Mute the given account. Clients should filter statuses and notifications from this account, if received (e.g. due to a boost in the Home timeline).",
245 parameters: [
246 %Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
247 Operation.parameter(
248 :notifications,
249 :query,
250 %Schema{allOf: [BooleanLike], default: true},
251 "Mute notifications in addition to statuses? Defaults to `true`."
252 )
253 ],
254 responses: %{
255 200 => Operation.response("Relationship", "application/json", AccountRelationship)
256 }
257 }
258 end
259
260 def unmute_operation do
261 %Operation{
262 tags: ["accounts"],
263 summary: "Unmute",
264 operationId: "AccountController.unmute",
265 security: [%{"oAuth" => ["follow", "write:mutes"]}],
266 description: "Unmute the given account.",
267 parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
268 responses: %{
269 200 => Operation.response("Relationship", "application/json", AccountRelationship)
270 }
271 }
272 end
273
274 def block_operation do
275 %Operation{
276 tags: ["accounts"],
277 summary: "Block",
278 operationId: "AccountController.block",
279 security: [%{"oAuth" => ["follow", "write:blocks"]}],
280 description:
281 "Block the given account. Clients should filter statuses from this account if received (e.g. due to a boost in the Home timeline)",
282 parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
283 responses: %{
284 200 => Operation.response("Relationship", "application/json", AccountRelationship)
285 }
286 }
287 end
288
289 def unblock_operation do
290 %Operation{
291 tags: ["accounts"],
292 summary: "Unblock",
293 operationId: "AccountController.unblock",
294 security: [%{"oAuth" => ["follow", "write:blocks"]}],
295 description: "Unblock the given account.",
296 parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
297 responses: %{
298 200 => Operation.response("Relationship", "application/json", AccountRelationship)
299 }
300 }
301 end
302
303 def follow_by_uri_operation do
304 %Operation{
305 tags: ["accounts"],
306 summary: "Follow by URI",
307 operationId: "AccountController.follows",
308 security: [%{"oAuth" => ["follow", "write:follows"]}],
309 requestBody: request_body("Parameters", follow_by_uri_request(), required: true),
310 responses: %{
311 200 => Operation.response("Account", "application/json", AccountRelationship),
312 400 => Operation.response("Error", "application/json", ApiError),
313 404 => Operation.response("Error", "application/json", ApiError)
314 }
315 }
316 end
317
318 def mutes_operation do
319 %Operation{
320 tags: ["accounts"],
321 summary: "Muted accounts",
322 operationId: "AccountController.mutes",
323 description: "Accounts the user has muted.",
324 security: [%{"oAuth" => ["follow", "read:mutes"]}],
325 responses: %{
326 200 => Operation.response("Accounts", "application/json", array_of_accounts())
327 }
328 }
329 end
330
331 def blocks_operation do
332 %Operation{
333 tags: ["accounts"],
334 summary: "Blocked users",
335 operationId: "AccountController.blocks",
336 description: "View your blocks. See also accounts/:id/{block,unblock}",
337 security: [%{"oAuth" => ["read:blocks"]}],
338 responses: %{
339 200 => Operation.response("Accounts", "application/json", array_of_accounts())
340 }
341 }
342 end
343
344 def endorsements_operation do
345 %Operation{
346 tags: ["accounts"],
347 summary: "Endorsements",
348 operationId: "AccountController.endorsements",
349 description: "Not implemented",
350 security: [%{"oAuth" => ["read:accounts"]}],
351 responses: %{
352 200 => empty_array_response()
353 }
354 }
355 end
356
357 def identity_proofs_operation do
358 %Operation{
359 tags: ["accounts"],
360 summary: "Identity proofs",
361 operationId: "AccountController.identity_proofs",
362 description: "Not implemented",
363 responses: %{
364 200 => empty_array_response()
365 }
366 }
367 end
368
369 defp create_request do
370 %Schema{
371 title: "AccountCreateRequest",
372 description: "POST body for creating an account",
373 type: :object,
374 required: [:username, :password, :agreement],
375 properties: %{
376 reason: %Schema{
377 type: :string,
378 nullable: true,
379 description:
380 "Text that will be reviewed by moderators if registrations require manual approval"
381 },
382 username: %Schema{type: :string, description: "The desired username for the account"},
383 email: %Schema{
384 type: :string,
385 nullable: true,
386 description:
387 "The email address to be used for login. Required when `account_activation_required` is enabled.",
388 format: :email
389 },
390 password: %Schema{
391 type: :string,
392 description: "The password to be used for login",
393 format: :password
394 },
395 agreement: %Schema{
396 type: :boolean,
397 description:
398 "Whether the user agrees to the local rules, terms, and policies. These should be presented to the user in order to allow them to consent before setting this parameter to TRUE."
399 },
400 locale: %Schema{
401 type: :string,
402 nullable: true,
403 description: "The language of the confirmation email that will be sent"
404 },
405 # Pleroma-specific properties:
406 fullname: %Schema{type: :string, nullable: true, description: "Full name"},
407 bio: %Schema{type: :string, description: "Bio", nullable: true, default: ""},
408 captcha_solution: %Schema{
409 type: :string,
410 nullable: true,
411 description: "Provider-specific captcha solution"
412 },
413 captcha_token: %Schema{
414 type: :string,
415 nullable: true,
416 description: "Provider-specific captcha token"
417 },
418 captcha_answer_data: %Schema{
419 type: :string,
420 nullable: true,
421 description: "Provider-specific captcha data"
422 },
423 token: %Schema{
424 type: :string,
425 nullable: true,
426 description: "Invite token required when the registrations aren't public"
427 }
428 },
429 example: %{
430 "username" => "cofe",
431 "email" => "cofe@example.com",
432 "password" => "secret",
433 "agreement" => "true",
434 "bio" => "☕️"
435 }
436 }
437 end
438
439 defp create_response do
440 %Schema{
441 title: "AccountCreateResponse",
442 description: "Response schema for an account",
443 type: :object,
444 properties: %{
445 token_type: %Schema{type: :string},
446 access_token: %Schema{type: :string},
447 scope: %Schema{type: :array, items: %Schema{type: :string}},
448 created_at: %Schema{type: :integer, format: :"date-time"}
449 },
450 example: %{
451 "access_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzk",
452 "created_at" => 1_585_918_714,
453 "scope" => ["read", "write", "follow", "push"],
454 "token_type" => "Bearer"
455 }
456 }
457 end
458
459 defp update_creadentials_request do
460 %Schema{
461 title: "AccountUpdateCredentialsRequest",
462 description: "POST body for creating an account",
463 type: :object,
464 properties: %{
465 bot: %Schema{
466 type: :boolean,
467 nullable: true,
468 description: "Whether the account has a bot flag."
469 },
470 display_name: %Schema{
471 type: :string,
472 nullable: true,
473 description: "The display name to use for the profile."
474 },
475 note: %Schema{type: :string, description: "The account bio."},
476 avatar: %Schema{
477 type: :string,
478 nullable: true,
479 description: "Avatar image encoded using multipart/form-data",
480 format: :binary
481 },
482 header: %Schema{
483 type: :string,
484 nullable: true,
485 description: "Header image encoded using multipart/form-data",
486 format: :binary
487 },
488 locked: %Schema{
489 type: :boolean,
490 nullable: true,
491 description: "Whether manual approval of follow requests is required."
492 },
493 fields_attributes: %Schema{
494 nullable: true,
495 oneOf: [
496 %Schema{type: :array, items: attribute_field()},
497 %Schema{type: :object, additionalProperties: %Schema{type: attribute_field()}}
498 ]
499 },
500 # NOTE: `source` field is not supported
501 #
502 # source: %Schema{
503 # type: :object,
504 # properties: %{
505 # privacy: %Schema{type: :string},
506 # sensitive: %Schema{type: :boolean},
507 # language: %Schema{type: :string}
508 # }
509 # },
510
511 # Pleroma-specific fields
512 no_rich_text: %Schema{
513 type: :boolean,
514 nullable: true,
515 description: "html tags are stripped from all statuses requested from the API"
516 },
517 hide_followers: %Schema{
518 type: :boolean,
519 nullable: true,
520 description: "user's followers will be hidden"
521 },
522 hide_follows: %Schema{
523 type: :boolean,
524 nullable: true,
525 description: "user's follows will be hidden"
526 },
527 hide_followers_count: %Schema{
528 type: :boolean,
529 nullable: true,
530 description: "user's follower count will be hidden"
531 },
532 hide_follows_count: %Schema{
533 type: :boolean,
534 nullable: true,
535 description: "user's follow count will be hidden"
536 },
537 hide_favorites: %Schema{
538 type: :boolean,
539 nullable: true,
540 description: "user's favorites timeline will be hidden"
541 },
542 show_role: %Schema{
543 type: :boolean,
544 nullable: true,
545 description: "user's role (e.g admin, moderator) will be exposed to anyone in the
546 API"
547 },
548 default_scope: VisibilityScope,
549 pleroma_settings_store: %Schema{
550 type: :object,
551 nullable: true,
552 description: "Opaque user settings to be saved on the backend."
553 },
554 skip_thread_containment: %Schema{
555 type: :boolean,
556 nullable: true,
557 description: "Skip filtering out broken threads"
558 },
559 allow_following_move: %Schema{
560 type: :boolean,
561 nullable: true,
562 description: "Allows automatically follow moved following accounts"
563 },
564 pleroma_background_image: %Schema{
565 type: :string,
566 nullable: true,
567 description: "Sets the background image of the user.",
568 format: :binary
569 },
570 discoverable: %Schema{
571 type: :boolean,
572 nullable: true,
573 description:
574 "Discovery of this account in search results and other services is allowed."
575 },
576 actor_type: ActorType
577 },
578 example: %{
579 bot: false,
580 display_name: "cofe",
581 note: "foobar",
582 fields_attributes: [%{name: "foo", value: "bar"}],
583 no_rich_text: false,
584 hide_followers: true,
585 hide_follows: false,
586 hide_followers_count: false,
587 hide_follows_count: false,
588 hide_favorites: false,
589 show_role: false,
590 default_scope: "private",
591 pleroma_settings_store: %{"pleroma-fe" => %{"key" => "val"}},
592 skip_thread_containment: false,
593 allow_following_move: false,
594 discoverable: false,
595 actor_type: "Person"
596 }
597 }
598 end
599
600 def array_of_accounts do
601 %Schema{
602 title: "ArrayOfAccounts",
603 type: :array,
604 items: Account,
605 example: [Account.schema().example]
606 }
607 end
608
609 defp array_of_relationships do
610 %Schema{
611 title: "ArrayOfRelationships",
612 description: "Response schema for account relationships",
613 type: :array,
614 items: AccountRelationship,
615 example: [
616 %{
617 "id" => "1",
618 "following" => true,
619 "showing_reblogs" => true,
620 "followed_by" => true,
621 "blocking" => false,
622 "blocked_by" => true,
623 "muting" => false,
624 "muting_notifications" => false,
625 "requested" => false,
626 "domain_blocking" => false,
627 "subscribing" => false,
628 "endorsed" => true
629 },
630 %{
631 "id" => "2",
632 "following" => true,
633 "showing_reblogs" => true,
634 "followed_by" => true,
635 "blocking" => false,
636 "blocked_by" => true,
637 "muting" => true,
638 "muting_notifications" => false,
639 "requested" => true,
640 "domain_blocking" => false,
641 "subscribing" => false,
642 "endorsed" => false
643 },
644 %{
645 "id" => "3",
646 "following" => true,
647 "showing_reblogs" => true,
648 "followed_by" => true,
649 "blocking" => true,
650 "blocked_by" => false,
651 "muting" => true,
652 "muting_notifications" => false,
653 "requested" => false,
654 "domain_blocking" => true,
655 "subscribing" => true,
656 "endorsed" => false
657 }
658 ]
659 }
660 end
661
662 defp follow_by_uri_request do
663 %Schema{
664 title: "AccountFollowsRequest",
665 description: "POST body for muting an account",
666 type: :object,
667 properties: %{
668 uri: %Schema{type: :string, nullable: true, format: :uri}
669 },
670 required: [:uri]
671 }
672 end
673
674 defp mute_request do
675 %Schema{
676 title: "AccountMuteRequest",
677 description: "POST body for muting an account",
678 type: :object,
679 properties: %{
680 notifications: %Schema{
681 type: :boolean,
682 nullable: true,
683 description: "Mute notifications in addition to statuses? Defaults to true.",
684 default: true
685 }
686 },
687 example: %{
688 "notifications" => true
689 }
690 }
691 end
692
693 defp array_of_lists do
694 %Schema{
695 title: "ArrayOfLists",
696 description: "Response schema for lists",
697 type: :array,
698 items: List,
699 example: [
700 %{"id" => "123", "title" => "my list"},
701 %{"id" => "1337", "title" => "anotehr list"}
702 ]
703 }
704 end
705
706 defp array_of_statuses do
707 %Schema{
708 title: "ArrayOfStatuses",
709 type: :array,
710 items: Status
711 }
712 end
713
714 defp attribute_field do
715 %Schema{
716 title: "AccountAttributeField",
717 description: "Request schema for account custom fields",
718 type: :object,
719 properties: %{
720 name: %Schema{type: :string},
721 value: %Schema{type: :string}
722 },
723 required: [:name, :value],
724 example: %{
725 "name" => "Website",
726 "value" => "https://pleroma.com"
727 }
728 }
729 end
730 end