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