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