Merge branch '2074-streaming-api-oauth-scopes-validation' 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 responses: %{
339 200 => Operation.response("Accounts", "application/json", array_of_accounts())
340 }
341 }
342 end
343
344 def blocks_operation do
345 %Operation{
346 tags: ["accounts"],
347 summary: "Blocked users",
348 operationId: "AccountController.blocks",
349 description: "View your blocks. See also accounts/:id/{block,unblock}",
350 security: [%{"oAuth" => ["read:blocks"]}],
351 responses: %{
352 200 => Operation.response("Accounts", "application/json", array_of_accounts())
353 }
354 }
355 end
356
357 def endorsements_operation do
358 %Operation{
359 tags: ["accounts"],
360 summary: "Endorsements",
361 operationId: "AccountController.endorsements",
362 description: "Not implemented",
363 security: [%{"oAuth" => ["read:accounts"]}],
364 responses: %{
365 200 => empty_array_response()
366 }
367 }
368 end
369
370 def identity_proofs_operation do
371 %Operation{
372 tags: ["accounts"],
373 summary: "Identity proofs",
374 operationId: "AccountController.identity_proofs",
375 # Validators complains about unused path params otherwise
376 parameters: [
377 %Reference{"$ref": "#/components/parameters/accountIdOrNickname"}
378 ],
379 description: "Not implemented",
380 responses: %{
381 200 => empty_array_response()
382 }
383 }
384 end
385
386 defp create_request do
387 %Schema{
388 title: "AccountCreateRequest",
389 description: "POST body for creating an account",
390 type: :object,
391 required: [:username, :password, :agreement],
392 properties: %{
393 reason: %Schema{
394 type: :string,
395 nullable: true,
396 description:
397 "Text that will be reviewed by moderators if registrations require manual approval"
398 },
399 username: %Schema{type: :string, description: "The desired username for the account"},
400 email: %Schema{
401 type: :string,
402 nullable: true,
403 description:
404 "The email address to be used for login. Required when `account_activation_required` is enabled.",
405 format: :email
406 },
407 password: %Schema{
408 type: :string,
409 description: "The password to be used for login",
410 format: :password
411 },
412 agreement: %Schema{
413 allOf: [BooleanLike],
414 description:
415 "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."
416 },
417 locale: %Schema{
418 type: :string,
419 nullable: true,
420 description: "The language of the confirmation email that will be sent"
421 },
422 # Pleroma-specific properties:
423 fullname: %Schema{type: :string, nullable: true, description: "Full name"},
424 bio: %Schema{type: :string, description: "Bio", nullable: true, default: ""},
425 captcha_solution: %Schema{
426 type: :string,
427 nullable: true,
428 description: "Provider-specific captcha solution"
429 },
430 captcha_token: %Schema{
431 type: :string,
432 nullable: true,
433 description: "Provider-specific captcha token"
434 },
435 captcha_answer_data: %Schema{
436 type: :string,
437 nullable: true,
438 description: "Provider-specific captcha data"
439 },
440 token: %Schema{
441 type: :string,
442 nullable: true,
443 description: "Invite token required when the registrations aren't public"
444 }
445 },
446 example: %{
447 "username" => "cofe",
448 "email" => "cofe@example.com",
449 "password" => "secret",
450 "agreement" => "true",
451 "bio" => "☕️"
452 }
453 }
454 end
455
456 # Note: this is a token response (if login succeeds!), but there's no oauth operation file yet.
457 defp create_response do
458 %Schema{
459 title: "AccountCreateResponse",
460 description: "Response schema for an account",
461 type: :object,
462 properties: %{
463 # The response when auto-login on create succeeds (token is issued):
464 token_type: %Schema{type: :string},
465 access_token: %Schema{type: :string},
466 refresh_token: %Schema{type: :string},
467 scope: %Schema{type: :string},
468 created_at: %Schema{type: :integer, format: :"date-time"},
469 me: %Schema{type: :string},
470 expires_in: %Schema{type: :integer},
471 #
472 # The response when registration succeeds but auto-login fails (no token):
473 identifier: %Schema{type: :string},
474 message: %Schema{type: :string}
475 },
476 # Note: example of successful registration with failed login response:
477 # example: %{
478 # "identifier" => "missing_confirmed_email",
479 # "message" => "You have been registered. Please check your email for further instructions."
480 # },
481 example: %{
482 "token_type" => "Bearer",
483 "access_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzk",
484 "refresh_token" => "i9hAVVzGld86Pl5JtLtizKoXVvtTlSCJvwaugCxvZzz",
485 "created_at" => 1_585_918_714,
486 "expires_in" => 600,
487 "scope" => "read write follow push",
488 "me" => "https://gensokyo.2hu/users/raymoo"
489 }
490 }
491 end
492
493 defp update_credentials_request do
494 %Schema{
495 title: "AccountUpdateCredentialsRequest",
496 description: "POST body for creating an account",
497 type: :object,
498 properties: %{
499 bot: %Schema{
500 allOf: [BooleanLike],
501 nullable: true,
502 description: "Whether the account has a bot flag."
503 },
504 display_name: %Schema{
505 type: :string,
506 nullable: true,
507 description: "The display name to use for the profile."
508 },
509 note: %Schema{type: :string, description: "The account bio."},
510 avatar: %Schema{
511 type: :string,
512 nullable: true,
513 description: "Avatar image encoded using multipart/form-data",
514 format: :binary
515 },
516 header: %Schema{
517 type: :string,
518 nullable: true,
519 description: "Header image encoded using multipart/form-data",
520 format: :binary
521 },
522 locked: %Schema{
523 allOf: [BooleanLike],
524 nullable: true,
525 description: "Whether manual approval of follow requests is required."
526 },
527 accepts_chat_messages: %Schema{
528 allOf: [BooleanLike],
529 nullable: true,
530 description: "Whether the user accepts receiving chat messages."
531 },
532 fields_attributes: %Schema{
533 nullable: true,
534 oneOf: [
535 %Schema{type: :array, items: attribute_field()},
536 %Schema{type: :object, additionalProperties: attribute_field()}
537 ]
538 },
539 # NOTE: `source` field is not supported
540 #
541 # source: %Schema{
542 # type: :object,
543 # properties: %{
544 # privacy: %Schema{type: :string},
545 # sensitive: %Schema{type: :boolean},
546 # language: %Schema{type: :string}
547 # }
548 # },
549
550 # Pleroma-specific fields
551 no_rich_text: %Schema{
552 allOf: [BooleanLike],
553 nullable: true,
554 description: "html tags are stripped from all statuses requested from the API"
555 },
556 hide_followers: %Schema{
557 allOf: [BooleanLike],
558 nullable: true,
559 description: "user's followers will be hidden"
560 },
561 hide_follows: %Schema{
562 allOf: [BooleanLike],
563 nullable: true,
564 description: "user's follows will be hidden"
565 },
566 hide_followers_count: %Schema{
567 allOf: [BooleanLike],
568 nullable: true,
569 description: "user's follower count will be hidden"
570 },
571 hide_follows_count: %Schema{
572 allOf: [BooleanLike],
573 nullable: true,
574 description: "user's follow count will be hidden"
575 },
576 hide_favorites: %Schema{
577 allOf: [BooleanLike],
578 nullable: true,
579 description: "user's favorites timeline will be hidden"
580 },
581 show_role: %Schema{
582 allOf: [BooleanLike],
583 nullable: true,
584 description: "user's role (e.g admin, moderator) will be exposed to anyone in the
585 API"
586 },
587 default_scope: VisibilityScope,
588 pleroma_settings_store: %Schema{
589 type: :object,
590 nullable: true,
591 description: "Opaque user settings to be saved on the backend."
592 },
593 skip_thread_containment: %Schema{
594 allOf: [BooleanLike],
595 nullable: true,
596 description: "Skip filtering out broken threads"
597 },
598 allow_following_move: %Schema{
599 allOf: [BooleanLike],
600 nullable: true,
601 description: "Allows automatically follow moved following accounts"
602 },
603 pleroma_background_image: %Schema{
604 type: :string,
605 nullable: true,
606 description: "Sets the background image of the user.",
607 format: :binary
608 },
609 discoverable: %Schema{
610 allOf: [BooleanLike],
611 nullable: true,
612 description:
613 "Discovery of this account in search results and other services is allowed."
614 },
615 actor_type: ActorType
616 },
617 example: %{
618 bot: false,
619 display_name: "cofe",
620 note: "foobar",
621 fields_attributes: [%{name: "foo", value: "bar"}],
622 no_rich_text: false,
623 hide_followers: true,
624 hide_follows: false,
625 hide_followers_count: false,
626 hide_follows_count: false,
627 hide_favorites: false,
628 show_role: false,
629 default_scope: "private",
630 pleroma_settings_store: %{"pleroma-fe" => %{"key" => "val"}},
631 skip_thread_containment: false,
632 allow_following_move: false,
633 discoverable: false,
634 actor_type: "Person"
635 }
636 }
637 end
638
639 def array_of_accounts do
640 %Schema{
641 title: "ArrayOfAccounts",
642 type: :array,
643 items: Account,
644 example: [Account.schema().example]
645 }
646 end
647
648 defp array_of_relationships do
649 %Schema{
650 title: "ArrayOfRelationships",
651 description: "Response schema for account relationships",
652 type: :array,
653 items: AccountRelationship,
654 example: [
655 %{
656 "id" => "1",
657 "following" => true,
658 "showing_reblogs" => true,
659 "followed_by" => true,
660 "blocking" => false,
661 "blocked_by" => true,
662 "muting" => false,
663 "muting_notifications" => false,
664 "requested" => false,
665 "domain_blocking" => false,
666 "subscribing" => false,
667 "endorsed" => true
668 },
669 %{
670 "id" => "2",
671 "following" => true,
672 "showing_reblogs" => true,
673 "followed_by" => true,
674 "blocking" => false,
675 "blocked_by" => true,
676 "muting" => true,
677 "muting_notifications" => false,
678 "requested" => true,
679 "domain_blocking" => false,
680 "subscribing" => false,
681 "endorsed" => false
682 },
683 %{
684 "id" => "3",
685 "following" => true,
686 "showing_reblogs" => true,
687 "followed_by" => true,
688 "blocking" => true,
689 "blocked_by" => false,
690 "muting" => true,
691 "muting_notifications" => false,
692 "requested" => false,
693 "domain_blocking" => true,
694 "subscribing" => true,
695 "endorsed" => false
696 }
697 ]
698 }
699 end
700
701 defp follow_by_uri_request do
702 %Schema{
703 title: "AccountFollowsRequest",
704 description: "POST body for muting an account",
705 type: :object,
706 properties: %{
707 uri: %Schema{type: :string, nullable: true, format: :uri}
708 },
709 required: [:uri]
710 }
711 end
712
713 defp mute_request do
714 %Schema{
715 title: "AccountMuteRequest",
716 description: "POST body for muting an account",
717 type: :object,
718 properties: %{
719 notifications: %Schema{
720 allOf: [BooleanLike],
721 nullable: true,
722 description: "Mute notifications in addition to statuses? Defaults to true.",
723 default: true
724 }
725 },
726 example: %{
727 "notifications" => true
728 }
729 }
730 end
731
732 defp array_of_lists do
733 %Schema{
734 title: "ArrayOfLists",
735 description: "Response schema for lists",
736 type: :array,
737 items: List,
738 example: [
739 %{"id" => "123", "title" => "my list"},
740 %{"id" => "1337", "title" => "anotehr list"}
741 ]
742 }
743 end
744
745 defp array_of_statuses do
746 %Schema{
747 title: "ArrayOfStatuses",
748 type: :array,
749 items: Status
750 }
751 end
752
753 defp attribute_field do
754 %Schema{
755 title: "AccountAttributeField",
756 description: "Request schema for account custom fields",
757 type: :object,
758 properties: %{
759 name: %Schema{type: :string},
760 value: %Schema{type: :string}
761 },
762 required: [:name, :value],
763 example: %{
764 "name" => "Website",
765 "value" => "https://pleroma.com"
766 }
767 }
768 end
769 end