Merge remote-tracking branch 'upstream/develop' into aliases
[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 also_known_as: %Schema{
618 type: :array,
619 items: %Schema{type: :string},
620 nullable: true,
621 description: "List of alternate ActivityPub IDs"
622 },
623 pleroma_background_image: %Schema{
624 type: :string,
625 nullable: true,
626 description: "Sets the background image of the user.",
627 format: :binary
628 },
629 discoverable: %Schema{
630 allOf: [BooleanLike],
631 nullable: true,
632 description:
633 "Discovery (listing, indexing) of this account by external services (search bots etc.) is allowed."
634 },
635 actor_type: ActorType
636 },
637 example: %{
638 bot: false,
639 display_name: "cofe",
640 note: "foobar",
641 fields_attributes: [%{name: "foo", value: "bar"}],
642 no_rich_text: false,
643 hide_followers: true,
644 hide_follows: false,
645 hide_followers_count: false,
646 hide_follows_count: false,
647 hide_favorites: false,
648 show_role: false,
649 default_scope: "private",
650 pleroma_settings_store: %{"pleroma-fe" => %{"key" => "val"}},
651 skip_thread_containment: false,
652 allow_following_move: false,
653 also_known_as: ["https://foo.bar/users/foo"],
654 discoverable: false,
655 actor_type: "Person"
656 }
657 }
658 end
659
660 def array_of_accounts do
661 %Schema{
662 title: "ArrayOfAccounts",
663 type: :array,
664 items: Account,
665 example: [Account.schema().example]
666 }
667 end
668
669 defp array_of_relationships do
670 %Schema{
671 title: "ArrayOfRelationships",
672 description: "Response schema for account relationships",
673 type: :array,
674 items: AccountRelationship,
675 example: [
676 %{
677 "id" => "1",
678 "following" => true,
679 "showing_reblogs" => true,
680 "followed_by" => true,
681 "blocking" => false,
682 "blocked_by" => true,
683 "muting" => false,
684 "muting_notifications" => false,
685 "requested" => false,
686 "domain_blocking" => false,
687 "subscribing" => false,
688 "endorsed" => true
689 },
690 %{
691 "id" => "2",
692 "following" => true,
693 "showing_reblogs" => true,
694 "followed_by" => true,
695 "blocking" => false,
696 "blocked_by" => true,
697 "muting" => true,
698 "muting_notifications" => false,
699 "requested" => true,
700 "domain_blocking" => false,
701 "subscribing" => false,
702 "endorsed" => false
703 },
704 %{
705 "id" => "3",
706 "following" => true,
707 "showing_reblogs" => true,
708 "followed_by" => true,
709 "blocking" => true,
710 "blocked_by" => false,
711 "muting" => true,
712 "muting_notifications" => false,
713 "requested" => false,
714 "domain_blocking" => true,
715 "subscribing" => true,
716 "endorsed" => false
717 }
718 ]
719 }
720 end
721
722 defp follow_by_uri_request do
723 %Schema{
724 title: "AccountFollowsRequest",
725 description: "POST body for muting an account",
726 type: :object,
727 properties: %{
728 uri: %Schema{type: :string, nullable: true, format: :uri}
729 },
730 required: [:uri]
731 }
732 end
733
734 defp mute_request do
735 %Schema{
736 title: "AccountMuteRequest",
737 description: "POST body for muting an account",
738 type: :object,
739 properties: %{
740 notifications: %Schema{
741 allOf: [BooleanLike],
742 nullable: true,
743 description: "Mute notifications in addition to statuses? Defaults to true.",
744 default: true
745 },
746 expires_in: %Schema{
747 type: :integer,
748 nullable: true,
749 description: "Expire the mute in `expires_in` seconds. Default 0 for infinity",
750 default: 0
751 }
752 },
753 example: %{
754 "notifications" => true,
755 "expires_in" => 86_400
756 }
757 }
758 end
759
760 defp array_of_lists do
761 %Schema{
762 title: "ArrayOfLists",
763 description: "Response schema for lists",
764 type: :array,
765 items: List,
766 example: [
767 %{"id" => "123", "title" => "my list"},
768 %{"id" => "1337", "title" => "anotehr list"}
769 ]
770 }
771 end
772
773 defp array_of_statuses do
774 %Schema{
775 title: "ArrayOfStatuses",
776 type: :array,
777 items: Status
778 }
779 end
780
781 defp attribute_field do
782 %Schema{
783 title: "AccountAttributeField",
784 description: "Request schema for account custom fields",
785 type: :object,
786 properties: %{
787 name: %Schema{type: :string},
788 value: %Schema{type: :string}
789 },
790 required: [:name, :value],
791 example: %{
792 "name" => "Website",
793 "value" => "https://pleroma.com"
794 }
795 }
796 end
797 end