Merge branch 'issue/2069' into 'develop'
[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_credentials_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 401 => Operation.response("Error", "application/json", ApiError),
106 404 => Operation.response("Error", "application/json", ApiError)
107 }
108 }
109 end
110
111 def statuses_operation do
112 %Operation{
113 tags: ["accounts"],
114 summary: "Statuses",
115 operationId: "AccountController.statuses",
116 description:
117 "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)",
118 parameters:
119 [
120 %Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
121 Operation.parameter(:pinned, :query, BooleanLike, "Include only pinned statuses"),
122 Operation.parameter(:tagged, :query, :string, "With tag"),
123 Operation.parameter(
124 :only_media,
125 :query,
126 BooleanLike,
127 "Include only statuses with media attached"
128 ),
129 Operation.parameter(
130 :with_muted,
131 :query,
132 BooleanLike,
133 "Include statuses from muted acccounts."
134 ),
135 Operation.parameter(:exclude_reblogs, :query, BooleanLike, "Exclude reblogs"),
136 Operation.parameter(:exclude_replies, :query, BooleanLike, "Exclude replies"),
137 Operation.parameter(
138 :exclude_visibilities,
139 :query,
140 %Schema{type: :array, items: VisibilityScope},
141 "Exclude visibilities"
142 )
143 ] ++ pagination_params(),
144 responses: %{
145 200 => Operation.response("Statuses", "application/json", array_of_statuses()),
146 401 => Operation.response("Error", "application/json", ApiError),
147 404 => Operation.response("Error", "application/json", ApiError)
148 }
149 }
150 end
151
152 def followers_operation do
153 %Operation{
154 tags: ["accounts"],
155 summary: "Followers",
156 operationId: "AccountController.followers",
157 security: [%{"oAuth" => ["read:accounts"]}],
158 description:
159 "Accounts which follow the given account, if network is not hidden by the account owner.",
160 parameters: [
161 %Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
162 Operation.parameter(:id, :query, :string, "ID of the resource owner"),
163 with_relationships_param() | pagination_params()
164 ],
165 responses: %{
166 200 => Operation.response("Accounts", "application/json", array_of_accounts())
167 }
168 }
169 end
170
171 def following_operation do
172 %Operation{
173 tags: ["accounts"],
174 summary: "Following",
175 operationId: "AccountController.following",
176 security: [%{"oAuth" => ["read:accounts"]}],
177 description:
178 "Accounts which the given account is following, if network is not hidden by the account owner.",
179 parameters: [
180 %Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
181 Operation.parameter(:id, :query, :string, "ID of the resource owner"),
182 with_relationships_param() | pagination_params()
183 ],
184 responses: %{200 => Operation.response("Accounts", "application/json", array_of_accounts())}
185 }
186 end
187
188 def lists_operation do
189 %Operation{
190 tags: ["accounts"],
191 summary: "Lists containing this account",
192 operationId: "AccountController.lists",
193 security: [%{"oAuth" => ["read:lists"]}],
194 description: "User lists that you have added this account to.",
195 parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
196 responses: %{200 => Operation.response("Lists", "application/json", array_of_lists())}
197 }
198 end
199
200 def follow_operation do
201 %Operation{
202 tags: ["accounts"],
203 summary: "Follow",
204 operationId: "AccountController.follow",
205 security: [%{"oAuth" => ["follow", "write:follows"]}],
206 description: "Follow the given account",
207 parameters: [
208 %Reference{"$ref": "#/components/parameters/accountIdOrNickname"}
209 ],
210 requestBody:
211 request_body(
212 "Parameters",
213 %Schema{
214 type: :object,
215 properties: %{
216 reblogs: %Schema{
217 type: :boolean,
218 description: "Receive this account's reblogs in home timeline? Defaults to true.",
219 default: true
220 }
221 }
222 },
223 required: false
224 ),
225 responses: %{
226 200 => Operation.response("Relationship", "application/json", AccountRelationship),
227 400 => Operation.response("Error", "application/json", ApiError),
228 404 => Operation.response("Error", "application/json", ApiError)
229 }
230 }
231 end
232
233 def unfollow_operation do
234 %Operation{
235 tags: ["accounts"],
236 summary: "Unfollow",
237 operationId: "AccountController.unfollow",
238 security: [%{"oAuth" => ["follow", "write:follows"]}],
239 description: "Unfollow the given account",
240 parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
241 responses: %{
242 200 => Operation.response("Relationship", "application/json", AccountRelationship),
243 400 => Operation.response("Error", "application/json", ApiError),
244 404 => Operation.response("Error", "application/json", ApiError)
245 }
246 }
247 end
248
249 def mute_operation do
250 %Operation{
251 tags: ["accounts"],
252 summary: "Mute",
253 operationId: "AccountController.mute",
254 security: [%{"oAuth" => ["follow", "write:mutes"]}],
255 requestBody: request_body("Parameters", mute_request()),
256 description:
257 "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).",
258 parameters: [
259 %Reference{"$ref": "#/components/parameters/accountIdOrNickname"},
260 Operation.parameter(
261 :notifications,
262 :query,
263 %Schema{allOf: [BooleanLike], default: true},
264 "Mute notifications in addition to statuses? Defaults to `true`."
265 )
266 ],
267 responses: %{
268 200 => Operation.response("Relationship", "application/json", AccountRelationship)
269 }
270 }
271 end
272
273 def unmute_operation do
274 %Operation{
275 tags: ["accounts"],
276 summary: "Unmute",
277 operationId: "AccountController.unmute",
278 security: [%{"oAuth" => ["follow", "write:mutes"]}],
279 description: "Unmute the given account.",
280 parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
281 responses: %{
282 200 => Operation.response("Relationship", "application/json", AccountRelationship)
283 }
284 }
285 end
286
287 def block_operation do
288 %Operation{
289 tags: ["accounts"],
290 summary: "Block",
291 operationId: "AccountController.block",
292 security: [%{"oAuth" => ["follow", "write:blocks"]}],
293 description:
294 "Block the given account. Clients should filter statuses from this account if received (e.g. due to a boost in the Home timeline)",
295 parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
296 responses: %{
297 200 => Operation.response("Relationship", "application/json", AccountRelationship)
298 }
299 }
300 end
301
302 def unblock_operation do
303 %Operation{
304 tags: ["accounts"],
305 summary: "Unblock",
306 operationId: "AccountController.unblock",
307 security: [%{"oAuth" => ["follow", "write:blocks"]}],
308 description: "Unblock the given account.",
309 parameters: [%Reference{"$ref": "#/components/parameters/accountIdOrNickname"}],
310 responses: %{
311 200 => Operation.response("Relationship", "application/json", AccountRelationship)
312 }
313 }
314 end
315
316 def follow_by_uri_operation do
317 %Operation{
318 tags: ["accounts"],
319 summary: "Follow by URI",
320 operationId: "AccountController.follows",
321 security: [%{"oAuth" => ["follow", "write:follows"]}],
322 requestBody: request_body("Parameters", follow_by_uri_request(), required: true),
323 responses: %{
324 200 => Operation.response("Account", "application/json", AccountRelationship),
325 400 => Operation.response("Error", "application/json", ApiError),
326 404 => Operation.response("Error", "application/json", ApiError)
327 }
328 }
329 end
330
331 def mutes_operation do
332 %Operation{
333 tags: ["accounts"],
334 summary: "Muted accounts",
335 operationId: "AccountController.mutes",
336 description: "Accounts the user has muted.",
337 security: [%{"oAuth" => ["follow", "read:mutes"]}],
338 parameters: pagination_params(),
339 responses: %{
340 200 => Operation.response("Accounts", "application/json", array_of_accounts())
341 }
342 }
343 end
344
345 def blocks_operation do
346 %Operation{
347 tags: ["accounts"],
348 summary: "Blocked users",
349 operationId: "AccountController.blocks",
350 description: "View your blocks. See also accounts/:id/{block,unblock}",
351 security: [%{"oAuth" => ["read:blocks"]}],
352 parameters: pagination_params(),
353 responses: %{
354 200 => Operation.response("Accounts", "application/json", array_of_accounts())
355 }
356 }
357 end
358
359 def endorsements_operation do
360 %Operation{
361 tags: ["accounts"],
362 summary: "Endorsements",
363 operationId: "AccountController.endorsements",
364 description: "Not implemented",
365 security: [%{"oAuth" => ["read:accounts"]}],
366 responses: %{
367 200 => empty_array_response()
368 }
369 }
370 end
371
372 def identity_proofs_operation do
373 %Operation{
374 tags: ["accounts"],
375 summary: "Identity proofs",
376 operationId: "AccountController.identity_proofs",
377 # Validators complains about unused path params otherwise
378 parameters: [
379 %Reference{"$ref": "#/components/parameters/accountIdOrNickname"}
380 ],
381 description: "Not implemented",
382 responses: %{
383 200 => empty_array_response()
384 }
385 }
386 end
387
388 defp create_request do
389 %Schema{
390 title: "AccountCreateRequest",
391 description: "POST body for creating an account",
392 type: :object,
393 required: [:username, :password, :agreement],
394 properties: %{
395 reason: %Schema{
396 type: :string,
397 nullable: true,
398 description:
399 "Text that will be reviewed by moderators if registrations require manual approval"
400 },
401 username: %Schema{type: :string, description: "The desired username for the account"},
402 email: %Schema{
403 type: :string,
404 nullable: true,
405 description:
406 "The email address to be used for login. Required when `account_activation_required` is enabled.",
407 format: :email
408 },
409 password: %Schema{
410 type: :string,
411 description: "The password to be used for login",
412 format: :password
413 },
414 agreement: %Schema{
415 allOf: [BooleanLike],
416 description:
417 "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."
418 },
419 locale: %Schema{
420 type: :string,
421 nullable: true,
422 description: "The language of the confirmation email that will be sent"
423 },
424 # Pleroma-specific properties:
425 fullname: %Schema{type: :string, nullable: true, description: "Full name"},
426 bio: %Schema{type: :string, description: "Bio", nullable: true, default: ""},
427 captcha_solution: %Schema{
428 type: :string,
429 nullable: true,
430 description: "Provider-specific captcha solution"
431 },
432 captcha_token: %Schema{
433 type: :string,
434 nullable: true,
435 description: "Provider-specific captcha token"
436 },
437 captcha_answer_data: %Schema{
438 type: :string,
439 nullable: true,
440 description: "Provider-specific captcha data"
441 },
442 token: %Schema{
443 type: :string,
444 nullable: true,
445 description: "Invite token required when the registrations aren't public"
446 }
447 },
448 example: %{
449 "username" => "cofe",
450 "email" => "cofe@example.com",
451 "password" => "secret",
452 "agreement" => "true",
453 "bio" => "☕️"
454 }
455 }
456 end
457
458 # Note: this is a token response (if login succeeds!), but there's no oauth operation file yet.
459 defp create_response do
460 %Schema{
461 title: "AccountCreateResponse",
462 description: "Response schema for an account",
463 type: :object,
464 properties: %{
465 # The response when auto-login on create succeeds (token is issued):
466 token_type: %Schema{type: :string},
467 access_token: %Schema{type: :string},
468 refresh_token: %Schema{type: :string},
469 scope: %Schema{type: :string},
470 created_at: %Schema{type: :integer, format: :"date-time"},
471 me: %Schema{type: :string},
472 expires_in: %Schema{type: :integer},
473 #
474 # The response when registration succeeds but auto-login fails (no token):
475 identifier: %Schema{type: :string},
476 message: %Schema{type: :string}
477 },
478 # Note: example of successful registration with failed login response:
479 # example: %{
480 # "identifier" => "missing_confirmed_email",
481 # "message" => "You have been registered. Please check your email for further instructions."
482 # },
483 example: %{
484 "token_type" => "Bearer",
485 "access_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzk",
486 "refresh_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzz",
487 "created_at" => 1_585_918_714,
488 "expires_in" => 600,
489 "scope" => "read write follow push",
490 "me" => "https://gensokyo.2hu/users/raymoo"
491 }
492 }
493 end
494
495 defp update_credentials_request do
496 %Schema{
497 title: "AccountUpdateCredentialsRequest",
498 description: "POST body for creating an account",
499 type: :object,
500 properties: %{
501 bot: %Schema{
502 allOf: [BooleanLike],
503 nullable: true,
504 description: "Whether the account has a bot flag."
505 },
506 display_name: %Schema{
507 type: :string,
508 nullable: true,
509 description: "The display name to use for the profile."
510 },
511 note: %Schema{type: :string, description: "The account bio."},
512 avatar: %Schema{
513 type: :string,
514 nullable: true,
515 description: "Avatar image encoded using multipart/form-data",
516 format: :binary
517 },
518 header: %Schema{
519 type: :string,
520 nullable: true,
521 description: "Header image encoded using multipart/form-data",
522 format: :binary
523 },
524 locked: %Schema{
525 allOf: [BooleanLike],
526 nullable: true,
527 description: "Whether manual approval of follow requests is required."
528 },
529 accepts_chat_messages: %Schema{
530 allOf: [BooleanLike],
531 nullable: true,
532 description: "Whether the user accepts receiving chat messages."
533 },
534 fields_attributes: %Schema{
535 nullable: true,
536 oneOf: [
537 %Schema{type: :array, items: attribute_field()},
538 %Schema{type: :object, additionalProperties: attribute_field()}
539 ]
540 },
541 # NOTE: `source` field is not supported
542 #
543 # source: %Schema{
544 # type: :object,
545 # properties: %{
546 # privacy: %Schema{type: :string},
547 # sensitive: %Schema{type: :boolean},
548 # language: %Schema{type: :string}
549 # }
550 # },
551
552 # Pleroma-specific fields
553 no_rich_text: %Schema{
554 allOf: [BooleanLike],
555 nullable: true,
556 description: "html tags are stripped from all statuses requested from the API"
557 },
558 hide_followers: %Schema{
559 allOf: [BooleanLike],
560 nullable: true,
561 description: "user's followers will be hidden"
562 },
563 hide_follows: %Schema{
564 allOf: [BooleanLike],
565 nullable: true,
566 description: "user's follows will be hidden"
567 },
568 hide_followers_count: %Schema{
569 allOf: [BooleanLike],
570 nullable: true,
571 description: "user's follower count will be hidden"
572 },
573 hide_follows_count: %Schema{
574 allOf: [BooleanLike],
575 nullable: true,
576 description: "user's follow count will be hidden"
577 },
578 hide_favorites: %Schema{
579 allOf: [BooleanLike],
580 nullable: true,
581 description: "user's favorites timeline will be hidden"
582 },
583 show_role: %Schema{
584 allOf: [BooleanLike],
585 nullable: true,
586 description: "user's role (e.g admin, moderator) will be exposed to anyone in the
587 API"
588 },
589 default_scope: VisibilityScope,
590 pleroma_settings_store: %Schema{
591 type: :object,
592 nullable: true,
593 description: "Opaque user settings to be saved on the backend."
594 },
595 skip_thread_containment: %Schema{
596 allOf: [BooleanLike],
597 nullable: true,
598 description: "Skip filtering out broken threads"
599 },
600 allow_following_move: %Schema{
601 allOf: [BooleanLike],
602 nullable: true,
603 description: "Allows automatically follow moved following accounts"
604 },
605 pleroma_background_image: %Schema{
606 type: :string,
607 nullable: true,
608 description: "Sets the background image of the user.",
609 format: :binary
610 },
611 discoverable: %Schema{
612 allOf: [BooleanLike],
613 nullable: true,
614 description:
615 "Discovery of this account in search results and other services is allowed."
616 },
617 actor_type: ActorType
618 },
619 example: %{
620 bot: false,
621 display_name: "cofe",
622 note: "foobar",
623 fields_attributes: [%{name: "foo", value: "bar"}],
624 no_rich_text: false,
625 hide_followers: true,
626 hide_follows: false,
627 hide_followers_count: false,
628 hide_follows_count: false,
629 hide_favorites: false,
630 show_role: false,
631 default_scope: "private",
632 pleroma_settings_store: %{"pleroma-fe" => %{"key" => "val"}},
633 skip_thread_containment: false,
634 allow_following_move: false,
635 discoverable: false,
636 actor_type: "Person"
637 }
638 }
639 end
640
641 def array_of_accounts do
642 %Schema{
643 title: "ArrayOfAccounts",
644 type: :array,
645 items: Account,
646 example: [Account.schema().example]
647 }
648 end
649
650 defp array_of_relationships do
651 %Schema{
652 title: "ArrayOfRelationships",
653 description: "Response schema for account relationships",
654 type: :array,
655 items: AccountRelationship,
656 example: [
657 %{
658 "id" => "1",
659 "following" => true,
660 "showing_reblogs" => true,
661 "followed_by" => true,
662 "blocking" => false,
663 "blocked_by" => true,
664 "muting" => false,
665 "muting_notifications" => false,
666 "requested" => false,
667 "domain_blocking" => false,
668 "subscribing" => false,
669 "endorsed" => true
670 },
671 %{
672 "id" => "2",
673 "following" => true,
674 "showing_reblogs" => true,
675 "followed_by" => true,
676 "blocking" => false,
677 "blocked_by" => true,
678 "muting" => true,
679 "muting_notifications" => false,
680 "requested" => true,
681 "domain_blocking" => false,
682 "subscribing" => false,
683 "endorsed" => false
684 },
685 %{
686 "id" => "3",
687 "following" => true,
688 "showing_reblogs" => true,
689 "followed_by" => true,
690 "blocking" => true,
691 "blocked_by" => false,
692 "muting" => true,
693 "muting_notifications" => false,
694 "requested" => false,
695 "domain_blocking" => true,
696 "subscribing" => true,
697 "endorsed" => false
698 }
699 ]
700 }
701 end
702
703 defp follow_by_uri_request do
704 %Schema{
705 title: "AccountFollowsRequest",
706 description: "POST body for muting an account",
707 type: :object,
708 properties: %{
709 uri: %Schema{type: :string, nullable: true, format: :uri}
710 },
711 required: [:uri]
712 }
713 end
714
715 defp mute_request do
716 %Schema{
717 title: "AccountMuteRequest",
718 description: "POST body for muting an account",
719 type: :object,
720 properties: %{
721 notifications: %Schema{
722 allOf: [BooleanLike],
723 nullable: true,
724 description: "Mute notifications in addition to statuses? Defaults to true.",
725 default: true
726 }
727 },
728 example: %{
729 "notifications" => true
730 }
731 }
732 end
733
734 defp array_of_lists do
735 %Schema{
736 title: "ArrayOfLists",
737 description: "Response schema for lists",
738 type: :array,
739 items: List,
740 example: [
741 %{"id" => "123", "title" => "my list"},
742 %{"id" => "1337", "title" => "anotehr list"}
743 ]
744 }
745 end
746
747 defp array_of_statuses do
748 %Schema{
749 title: "ArrayOfStatuses",
750 type: :array,
751 items: Status
752 }
753 end
754
755 defp attribute_field do
756 %Schema{
757 title: "AccountAttributeField",
758 description: "Request schema for account custom fields",
759 type: :object,
760 properties: %{
761 name: %Schema{type: :string},
762 value: %Schema{type: :string}
763 },
764 required: [:name, :value],
765 example: %{
766 "name" => "Website",
767 "value" => "https://pleroma.com"
768 }
769 }
770 end
771 end