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