From 29355260ed2b7234852d6d097841954b33af2217 Mon Sep 17 00:00:00 2001 From: Thomas Nilles Date: Sun, 30 Nov 2025 13:01:24 -0500 Subject: [PATCH] init commit --- .dockerignore | 94 + .gitignore | 21 + .golangci.yaml | 101 + .goreleaser.yaml | 36 + CLAUDE.md | 236 + CODEOWNERS | 2 + Dockerfile | 81 + LICENSE | 253 +- README.md | 224 +- SECURITY.md | 7 + bin/memos/main.go | 187 + docker-compose.yml | 61 + export_import_messages.proto | 107 + go.mod | 98 + go.sum | 711 ++ internal/base/resource_name.go | 7 + internal/base/resource_name_test.go | 35 + internal/profile/profile.go | 92 + internal/util/util.go | 73 + internal/util/util_test.go | 31 + internal/version/version.go | 56 + internal/version/version_test.go | 103 + plugin/cron/README.md | 1 + plugin/cron/chain.go | 96 + plugin/cron/chain_test.go | 239 + plugin/cron/constantdelay.go | 27 + plugin/cron/constantdelay_test.go | 55 + plugin/cron/cron.go | 355 + plugin/cron/cron_test.go | 702 ++ plugin/cron/logger.go | 86 + plugin/cron/option.go | 45 + plugin/cron/option_test.go | 43 + plugin/cron/parser.go | 435 + plugin/cron/parser_test.go | 384 + plugin/cron/spec.go | 188 + plugin/cron/spec_test.go | 301 + plugin/filter/common_converter.go | 448 + plugin/filter/converter.go | 20 + plugin/filter/dialect.go | 212 + plugin/filter/expr.go | 127 + plugin/filter/filter.go | 48 + plugin/filter/templates.go | 146 + plugin/httpgetter/html_meta.go | 166 + plugin/httpgetter/html_meta_test.go | 32 + plugin/httpgetter/http_getter.go | 1 + plugin/httpgetter/image.go | 45 + plugin/httpgetter/util.go | 15 + plugin/idp/idp.go | 8 + plugin/idp/oauth2/oauth2.go | 123 + plugin/idp/oauth2/oauth2_test.go | 163 + plugin/storage/s3/s3.go | 92 + plugin/webhook/webhook.go | 90 + plugin/webhook/webhook_test.go | 1 + proto/README.md | 17 + proto/api/v1/README.md | 3 + proto/api/v1/activity_service.proto | 127 + proto/api/v1/attachment_service.proto | 171 + proto/api/v1/auth_service.proto | 93 + proto/api/v1/common.proto | 23 + proto/api/v1/idp_service.proto | 147 + proto/api/v1/inbox_service.proto | 149 + proto/api/v1/markdown_service.proto | 329 + proto/api/v1/memo_service.proto | 714 ++ proto/api/v1/shortcut_service.proto | 124 + proto/api/v1/user_service.proto | 554 + proto/api/v1/webhook_service.proto | 124 + proto/api/v1/workspace_service.proto | 177 + proto/buf.gen.yaml | 32 + proto/buf.lock | 6 + proto/buf.yaml | 19 + proto/gen/api/v1/activity_service.pb.go | 628 ++ proto/gen/api/v1/activity_service.pb.gw.go | 243 + proto/gen/api/v1/activity_service_grpc.pb.go | 163 + proto/gen/api/v1/attachment_service.pb.go | 687 ++ proto/gen/api/v1/attachment_service.pb.gw.go | 629 ++ .../gen/api/v1/attachment_service_grpc.pb.go | 325 + proto/gen/api/v1/auth_service.pb.go | 521 + proto/gen/api/v1/auth_service.pb.gw.go | 277 + proto/gen/api/v1/auth_service_grpc.pb.go | 210 + proto/gen/api/v1/common.pb.go | 244 + proto/gen/api/v1/idp_service.pb.go | 805 ++ proto/gen/api/v1/idp_service.pb.gw.go | 507 + proto/gen/api/v1/idp_service_grpc.pb.go | 285 + proto/gen/api/v1/inbox_service.pb.go | 622 ++ proto/gen/api/v1/inbox_service.pb.gw.go | 381 + proto/gen/api/v1/inbox_service_grpc.pb.go | 204 + proto/gen/api/v1/markdown_service.pb.go | 3114 ++++++ proto/gen/api/v1/markdown_service.pb.gw.go | 363 + proto/gen/api/v1/markdown_service_grpc.pb.go | 251 + proto/gen/api/v1/memo_service.pb.go | 2873 +++++ proto/gen/api/v1/memo_service.pb.gw.go | 1761 +++ proto/gen/api/v1/memo_service_grpc.pb.go | 804 ++ proto/gen/api/v1/shortcut_service.pb.go | 496 + proto/gen/api/v1/shortcut_service.pb.gw.go | 543 + proto/gen/api/v1/shortcut_service_grpc.pb.go | 284 + proto/gen/api/v1/user_service.pb.go | 2262 ++++ proto/gen/api/v1/user_service.pb.gw.go | 1475 +++ proto/gen/api/v1/user_service_grpc.pb.go | 725 ++ proto/gen/api/v1/webhook_service.pb.go | 497 + proto/gen/api/v1/webhook_service.pb.gw.go | 543 + proto/gen/api/v1/webhook_service_grpc.pb.go | 284 + proto/gen/api/v1/workspace_service.pb.go | 1039 ++ proto/gen/api/v1/workspace_service.pb.gw.go | 349 + proto/gen/api/v1/workspace_service_grpc.pb.go | 203 + proto/gen/apidocs.swagger.yaml | 4220 +++++++ proto/gen/store/activity.pb.go | 180 + proto/gen/store/attachment.pb.go | 287 + proto/gen/store/idp.pb.go | 467 + proto/gen/store/inbox.pb.go | 193 + proto/gen/store/memo.pb.go | 295 + proto/gen/store/user_setting.pb.go | 962 ++ proto/gen/store/workspace_setting.pb.go | 935 ++ proto/store/activity.proto | 14 + proto/store/attachment.proto | 33 + proto/store/idp.proto | 41 + proto/store/inbox.proto | 15 + proto/store/memo.proto | 29 + proto/store/user_setting.proto | 107 + proto/store/workspace_setting.proto | 120 + scripts/Dockerfile | 31 + scripts/build.sh | 29 + scripts/compose.yaml | 8 + scripts/entrypoint.sh | 27 + server/profiler/profiler.go | 120 + server/router/api/v1/acl.go | 262 + server/router/api/v1/acl_config.go | 34 + server/router/api/v1/activity_service.go | 126 + server/router/api/v1/attachment_service.go | 673 ++ server/router/api/v1/auth.go | 91 + server/router/api/v1/auth_service.go | 502 + .../api/v1/auth_service_client_info_test.go | 179 + server/router/api/v1/common.go | 70 + server/router/api/v1/health_service.go | 21 + server/router/api/v1/idp_service.go | 183 + server/router/api/v1/inbox_service.go | 226 + server/router/api/v1/logger_interceptor.go | 48 + server/router/api/v1/markdown_service.go | 279 + .../router/api/v1/memo_attachment_service.go | 102 + server/router/api/v1/memo_export_import.go | 427 + server/router/api/v1/memo_relation_service.go | 170 + server/router/api/v1/memo_service.go | 785 ++ .../router/api/v1/memo_service_converter.go | 149 + server/router/api/v1/memo_service_filter.go | 168 + server/router/api/v1/reaction_service.go | 90 + server/router/api/v1/resource_name.go | 162 + server/router/api/v1/shortcut_service.go | 337 + server/router/api/v1/test/idp_service_test.go | 519 + .../router/api/v1/test/inbox_service_test.go | 559 + .../api/v1/test/shortcut_service_test.go | 819 ++ server/router/api/v1/test/test_helper.go | 81 + .../api/v1/test/user_service_stats_test.go | 105 + .../api/v1/test/webhook_service_test.go | 406 + .../api/v1/test/workspace_service_test.go | 206 + server/router/api/v1/test_auth.go | 19 + server/router/api/v1/user_service.go | 831 ++ server/router/api/v1/user_service_stats.go | 168 + server/router/api/v1/v1.go | 137 + server/router/api/v1/webhook_service.go | 317 + server/router/api/v1/workspace_service.go | 306 + server/router/frontend/frontend.go | 61 + server/router/rss/rss.go | 179 + server/runner/memopayload/runner.go | 134 + server/runner/s3presign/runner.go | 134 + server/server.go | 227 + store/activity.go | 64 + store/attachment.go | 166 + store/cache.go | 9 + store/cache/cache.go | 327 + store/cache/cache_test.go | 209 + store/common.go | 24 + store/db/db.go | 32 + store/db/mysql/activity.go | 93 + store/db/mysql/attachment.go | 202 + store/db/mysql/common.go | 10 + store/db/mysql/idp.go | 126 + store/db/mysql/inbox.go | 141 + store/db/mysql/memo.go | 287 + store/db/mysql/memo_filter.go | 304 + store/db/mysql/memo_filter_test.go | 130 + store/db/mysql/memo_relation.go | 111 + store/db/mysql/migration_history.go | 53 + store/db/mysql/mysql.go | 68 + store/db/mysql/reaction.go | 104 + store/db/mysql/user.go | 162 + store/db/mysql/user_setting.go | 56 + store/db/mysql/workspace_setting.go | 65 + store/db/postgres/activity.go | 81 + store/db/postgres/attachment.go | 186 + store/db/postgres/common.go | 26 + store/db/postgres/idp.go | 117 + store/db/postgres/inbox.go | 141 + store/db/postgres/memo.go | 279 + store/db/postgres/memo_filter.go | 326 + store/db/postgres/memo_filter_test.go | 130 + store/db/postgres/memo_relation.go | 124 + store/db/postgres/migration_history.go | 57 + store/db/postgres/postgres.go | 57 + store/db/postgres/reaction.go | 79 + store/db/postgres/user.go | 166 + store/db/postgres/user_setting.go | 69 + store/db/postgres/workspace_setting.go | 72 + store/db/sqlite/activity.go | 83 + store/db/sqlite/attachment.go | 182 + store/db/sqlite/common.go | 9 + store/db/sqlite/idp.go | 117 + store/db/sqlite/inbox.go | 132 + store/db/sqlite/memo.go | 265 + store/db/sqlite/memo_filter.go | 304 + store/db/sqlite/memo_filter_test.go | 151 + store/db/sqlite/memo_relation.go | 125 + store/db/sqlite/migration_history.go | 57 + store/db/sqlite/reaction.go | 80 + store/db/sqlite/sqlite.go | 70 + store/db/sqlite/user.go | 170 + store/db/sqlite/user_setting.go | 68 + store/db/sqlite/workspace_setting.go | 72 + store/driver.go | 79 + store/idp.go | 182 + store/inbox.go | 64 + store/memo.go | 147 + store/memo_relation.go | 45 + store/migration/mysql/0.17/00__inbox.sql | 9 + .../mysql/0.17/01__delete_activity.sql | 1 + .../migration/mysql/0.18/00__extend_text.sql | 3 + store/migration/mysql/0.18/01__webhook.sql | 10 + .../migration/mysql/0.18/02__user_setting.sql | 4 + .../mysql/0.19/00__add_resource_name.sql | 15 + store/migration/mysql/0.20/00__reaction.sql | 9 + .../mysql/0.21/00__user_description.sql | 1 + store/migration/mysql/0.21/01__rename_uid.sql | 3 + .../mysql/0.22/00__resource_storage_type.sql | 11 + store/migration/mysql/0.22/01__memo_tags.sql | 3 + .../migration/mysql/0.22/02__memo_payload.sql | 3 + store/migration/mysql/0.22/03__drop_tag.sql | 1 + store/migration/mysql/0.23/00__reactions.sql | 12 + store/migration/mysql/0.24/00__memo.sql | 2 + .../migration/mysql/0.24/01__memo_pinned.sql | 8 + .../mysql/0.24/02__s3_reference_length.sql | 2 + .../mysql/0.25/00__remove_webhook.sql | 1 + store/migration/mysql/LATEST.sql | 121 + .../postgres/0.19/00__add_resource_name.sql | 15 + .../migration/postgres/0.20/00__reaction.sql | 9 + .../postgres/0.21/00__user_description.sql | 1 + .../postgres/0.21/01__rename_uid.sql | 3 + .../0.22/00__resource_storage_type.sql | 11 + .../migration/postgres/0.22/01__memo_tags.sql | 1 + .../postgres/0.22/02__memo_payload.sql | 1 + .../migration/postgres/0.22/03__drop_tag.sql | 1 + .../migration/postgres/0.23/00__reactions.sql | 12 + store/migration/postgres/0.24/00__memo.sql | 2 + .../postgres/0.24/01__memo_pinned.sql | 8 + .../postgres/0.25/00__remove_webhook.sql | 1 + store/migration/postgres/LATEST.sql | 121 + store/migration/sqlite/0.10/00__activity.sql | 9 + .../migration/sqlite/0.11/00__user_avatar.sql | 4 + store/migration/sqlite/0.11/01__idp.sql | 8 + store/migration/sqlite/0.11/02__storage.sql | 7 + .../sqlite/0.12/00__user_setting.sql | 6 + .../sqlite/0.12/01__system_setting.sql | 69 + .../0.12/03__resource_internal_path.sql | 4 + .../sqlite/0.12/04__resource_public_id.sql | 18 + .../sqlite/0.13/00__memo_relation.sql | 7 + .../0.13/01__remove_memo_organizer_id.sql | 22 + .../0.14/00__drop_resource_public_id.sql | 25 + .../sqlite/0.14/01__create_indexes.sql | 5 + .../sqlite/0.15/00__drop_user_open_id.sql | 25 + .../0.16/00__add_memo_id_to_resource.sql | 13 + .../sqlite/0.16/01__drop_shortcut_table.sql | 1 + store/migration/sqlite/0.17/00__inbox.sql | 9 + .../sqlite/0.17/01__delete_activities.sql | 1 + store/migration/sqlite/0.18/00__webhook.sql | 12 + .../sqlite/0.18/01__user_setting.sql | 4 + .../sqlite/0.19/00__add_resource_name.sql | 11 + store/migration/sqlite/0.2/00__user_role.sql | 60 + .../sqlite/0.2/01__memo_visibility.sql | 4 + store/migration/sqlite/0.20/00__reaction.sql | 9 + .../sqlite/0.21/00__user_description.sql | 1 + .../migration/sqlite/0.21/01__rename_uid.sql | 3 + .../sqlite/0.22/00__resource_storage_type.sql | 17 + store/migration/sqlite/0.22/01__memo_tags.sql | 3 + .../sqlite/0.22/02__memo_payload.sql | 1 + store/migration/sqlite/0.22/03__drop_tag.sql | 1 + store/migration/sqlite/0.23/00__reactions.sql | 12 + store/migration/sqlite/0.24/00__memo.sql | 7 + .../migration/sqlite/0.24/01__memo_pinned.sql | 11 + .../sqlite/0.25/00__remove_webhook.sql | 3 + .../0.3/00__memo_visibility_protected.sql | 43 + .../migration/sqlite/0.4/00__user_setting.sql | 9 + .../0.5/00__regenerate_foreign_keys.sql | 217 + .../sqlite/0.5/01__memo_resource.sql | 10 + .../sqlite/0.5/02__system_setting.sql | 7 + .../sqlite/0.5/03__resource_extermal_link.sql | 4 + .../sqlite/0.6/00__recreate_triggers.sql | 59 + store/migration/sqlite/0.7/00__remove_fk.sql | 191 + .../sqlite/0.7/01__remove_triggers.sql | 7 + .../sqlite/0.8/00__migration_history.sql | 5 + .../sqlite/0.8/01__user_username.sql | 50 + store/migration/sqlite/0.9/00__tag.sql | 6 + store/migration/sqlite/LATEST.sql | 130 + store/migration_history.go | 13 + store/migrator.go | 327 + store/reaction.go | 36 + store/seed/sqlite/00__reset.sql | 11 + store/seed/sqlite/01__dump.sql | 13 + store/store.go | 57 + store/test/README.md | 13 + store/test/activity_test.go | 34 + store/test/attachment_test.go | 63 + store/test/idp_test.go | 60 + store/test/inbox_test.go | 54 + store/test/memo_relation_test.go | 62 + store/test/memo_test.go | 118 + store/test/migrator_test.go | 17 + store/test/reaction_test.go | 48 + store/test/store.go | 124 + store/test/user_setting_test.go | 28 + store/test/user_test.go | 56 + store/test/workspace_setting_test.go | 31 + store/user.go | 159 + store/user_setting.go | 441 + store/workspace_setting.go | 245 + web/.gitignore | 7 + web/.prettierrc.js | 8 + web/README.md | 1 + web/components.json | 21 + web/eslint.config.mjs | 34 + web/index.html | 19 + web/package-lock.json | 9947 +++++++++++++++++ web/package.json | 93 + web/pnpm-lock.yaml | 7056 ++++++++++++ web/public/android-chrome-192x192.png | Bin 0 -> 25014 bytes web/public/android-chrome-512x512.png | Bin 0 -> 141510 bytes web/public/apple-touch-icon.png | Bin 0 -> 22496 bytes web/public/full-logo.webp | Bin 0 -> 25152 bytes web/public/logo.webp | Bin 0 -> 37158 bytes web/public/site.webmanifest | 10 + web/src/App.tsx | 117 + .../ActivityCalendar/ActivityCalendar.tsx | 180 + web/src/components/ActivityCalendar/index.ts | 1 + web/src/components/AppearanceSelect.tsx | 51 + web/src/components/AttachmentIcon.tsx | 108 + web/src/components/AuthFooter.tsx | 28 + web/src/components/BrandBanner.tsx | 27 + .../components/ChangeMemberPasswordDialog.tsx | 115 + .../components/CreateAccessTokenDialog.tsx | 139 + .../CreateIdentityProviderDialog.tsx | 433 + web/src/components/CreateShortcutDialog.tsx | 141 + web/src/components/CreateUserDialog.tsx | 135 + web/src/components/CreateWebhookDialog.tsx | 159 + web/src/components/DateTimeInput.tsx | 43 + web/src/components/Empty.tsx | 11 + web/src/components/ExportImport.tsx | 435 + .../components/HomeSidebar/HomeSidebar.tsx | 58 + .../HomeSidebar/HomeSidebarDrawer.tsx | 33 + .../HomeSidebar/ShortcutsSection.tsx | 120 + .../components/HomeSidebar/TagsSection.tsx | 141 + web/src/components/HomeSidebar/index.ts | 4 + .../components/Inbox/MemoCommentMessage.tsx | 142 + web/src/components/LeafletMap.tsx | 61 + web/src/components/LearnMore.tsx | 31 + web/src/components/LocaleSelect.tsx | 52 + .../components/MasonryView/MasonryView.tsx | 184 + web/src/components/MasonryView/README.md | 116 + web/src/components/MasonryView/index.ts | 3 + web/src/components/MemoActionMenu.tsx | 218 + web/src/components/MemoAttachment.tsx | 36 + web/src/components/MemoAttachmentListView.tsx | 112 + web/src/components/MemoContent/Blockquote.tsx | 19 + web/src/components/MemoContent/Bold.tsx | 19 + web/src/components/MemoContent/BoldItalic.tsx | 14 + web/src/components/MemoContent/Code.tsx | 9 + web/src/components/MemoContent/CodeBlock.tsx | 80 + .../EmbeddedContent/EmbeddedAttachment.tsx | 62 + .../EmbeddedContent/EmbeddedMemo.tsx | 97 + .../MemoContent/EmbeddedContent/Error.tsx | 9 + .../MemoContent/EmbeddedContent/index.tsx | 25 + .../MemoContent/EscapingCharacter.tsx | 9 + .../components/MemoContent/HTMLElement.tsx | 12 + web/src/components/MemoContent/Heading.tsx | 34 + web/src/components/MemoContent/Highlight.tsx | 9 + .../components/MemoContent/HorizontalRule.tsx | 12 + web/src/components/MemoContent/Image.tsx | 10 + web/src/components/MemoContent/Italic.tsx | 19 + web/src/components/MemoContent/LineBreak.tsx | 5 + web/src/components/MemoContent/Link.tsx | 81 + web/src/components/MemoContent/List.tsx | 63 + web/src/components/MemoContent/Math.tsx | 14 + .../components/MemoContent/MermaidBlock.tsx | 55 + .../MemoContent/OrderedListItem.tsx | 21 + web/src/components/MemoContent/Paragraph.tsx | 19 + .../MemoContent/ReferencedContent/Error.tsx | 9 + .../ReferencedContent/ReferencedMemo.tsx | 55 + .../MemoContent/ReferencedContent/index.tsx | 22 + web/src/components/MemoContent/Renderer.tsx | 139 + web/src/components/MemoContent/Spoiler.tsx | 21 + .../components/MemoContent/Strikethrough.tsx | 9 + web/src/components/MemoContent/Subscript.tsx | 9 + .../components/MemoContent/Superscript.tsx | 9 + web/src/components/MemoContent/Table.tsx | 37 + web/src/components/MemoContent/Tag.tsx | 60 + .../components/MemoContent/TaskListItem.tsx | 58 + web/src/components/MemoContent/Text.tsx | 9 + .../MemoContent/UnorderedListItem.tsx | 20 + web/src/components/MemoContent/index.tsx | 127 + .../components/MemoContent/types/context.ts | 18 + web/src/components/MemoContent/types/index.ts | 6 + .../MemoDetailSidebar/MemoDetailSidebar.tsx | 107 + .../MemoDetailSidebarDrawer.tsx | 36 + web/src/components/MemoDetailSidebar/index.ts | 4 + web/src/components/MemoDisplaySettingMenu.tsx | 68 + .../ActionButton/AddMemoRelationPopover.tsx | 210 + .../ActionButton/LocationSelector.tsx | 158 + .../MemoEditor/ActionButton/MarkdownMenu.tsx | 93 + .../MemoEditor/ActionButton/TagSelector.tsx | 67 + .../ActionButton/UploadAttachmentButton.tsx | 92 + .../ActionButton/UploadResourceButton.tsx | 0 .../ActionButton/VisibilitySelector.tsx | 45 + .../MemoEditor/AttachmentListView.tsx | 60 + .../MemoEditor/Editor/TagSuggestions.tsx | 130 + .../components/MemoEditor/Editor/index.tsx | 233 + .../MemoEditor/RelationListView.tsx | 55 + .../components/MemoEditor/SortableItem.tsx | 25 + web/src/components/MemoEditor/handlers.ts | 52 + web/src/components/MemoEditor/index.tsx | 582 + .../components/MemoEditor/types/context.ts | 18 + web/src/components/MemoEditor/types/index.ts | 1 + web/src/components/MemoFilters.tsx | 80 + web/src/components/MemoLocationView.tsx | 35 + web/src/components/MemoReactionListView.tsx | 49 + .../MemoRelationForceGraph.tsx | 81 + .../MemoRelationForceGraph/index.ts | 5 + .../MemoRelationForceGraph/types.ts | 10 + .../MemoRelationForceGraph/utils.ts | 36 + web/src/components/MemoRelationListView.tsx | 110 + web/src/components/MemoResource.tsx | 0 web/src/components/MemoView.tsx | 275 + web/src/components/MobileHeader.tsx | 30 + web/src/components/Navigation.tsx | 123 + web/src/components/NavigationDrawer.tsx | 39 + .../PagedMemoList/PagedMemoList.tsx | 230 + web/src/components/PagedMemoList/index.ts | 3 + web/src/components/PasswordSignInForm.tsx | 104 + web/src/components/PreviewImageDialog.tsx | 93 + web/src/components/ReactionSelector.tsx | 93 + web/src/components/ReactionView.tsx | 92 + web/src/components/RenameTagDialog.tsx | 89 + web/src/components/RequiredBadge.tsx | 11 + web/src/components/SearchBar.tsx | 49 + .../Settings/AccessTokenSection.tsx | 143 + web/src/components/Settings/MemberSection.tsx | 176 + .../Settings/MemoRelatedSettings.tsx | 187 + .../components/Settings/MyAccountSection.tsx | 69 + .../Settings/PreferencesSection.tsx | 84 + web/src/components/Settings/SSOSection.tsx | 122 + .../components/Settings/SectionMenuItem.tsx | 25 + .../components/Settings/StorageSection.tsx | 252 + .../Settings/UserSessionsSection.tsx | 158 + .../components/Settings/WebhookSection.tsx | 125 + .../components/Settings/WorkspaceSection.tsx | 193 + .../StatisticsView/MonthNavigator.tsx | 32 + .../components/StatisticsView/StatCard.tsx | 37 + .../StatisticsView/StatisticsView.tsx | 101 + web/src/components/StatisticsView/index.ts | 1 + web/src/components/TagTree.tsx | 151 + web/src/components/ThemeSelector.tsx | 32 + web/src/components/UpdateAccountDialog.tsx | 220 + .../UpdateCustomizedProfileDialog.new.tsx | 0 .../UpdateCustomizedProfileDialog.tsx | 167 + web/src/components/UserAvatar.tsx | 23 + web/src/components/UserBanner.tsx | 68 + web/src/components/VisibilityIcon.tsx | 28 + .../examples/WorkspaceSection.example.tsx | 0 web/src/components/kit/OverflowTip.tsx | 40 + web/src/components/kit/README.md | 1 + web/src/components/kit/SquareDiv.tsx | 45 + web/src/components/ui/badge.tsx | 34 + web/src/components/ui/button.tsx | 46 + web/src/components/ui/checkbox.tsx | 23 + web/src/components/ui/dialog.tsx | 127 + web/src/components/ui/dropdown-menu.tsx | 219 + web/src/components/ui/input.tsx | 18 + web/src/components/ui/label.tsx | 18 + web/src/components/ui/popover.tsx | 52 + web/src/components/ui/radio-group.tsx | 27 + web/src/components/ui/select.tsx | 148 + web/src/components/ui/separator.tsx | 25 + web/src/components/ui/sheet.tsx | 121 + web/src/components/ui/switch.tsx | 25 + web/src/components/ui/textarea.tsx | 17 + web/src/components/ui/tooltip.tsx | 59 + web/src/grpcweb.ts | 43 + web/src/helpers/consts.ts | 11 + web/src/helpers/utils.ts | 70 + web/src/hooks/index.ts | 5 + web/src/hooks/useAsyncEffect.ts | 9 + web/src/hooks/useCurrentUser.ts | 7 + web/src/hooks/useDialog.ts | 118 + web/src/hooks/useLoading.ts | 35 + web/src/hooks/useNavigateTo.ts | 20 + web/src/hooks/useResponsiveWidth.ts | 18 + web/src/hooks/useStatisticsData.ts | 27 + web/src/i18n.ts | 75 + web/src/index.css | 20 + web/src/layouts/HomeLayout.tsx | 32 + web/src/layouts/RootLayout.tsx | 70 + web/src/lib/utils.ts | 6 + web/src/locales/ar.json | 445 + web/src/locales/ca.json | 445 + web/src/locales/cs.json | 428 + web/src/locales/de.json | 353 + web/src/locales/en-GB.json | 1 + web/src/locales/en.json | 446 + web/src/locales/es.json | 276 + web/src/locales/fa.json | 394 + web/src/locales/fr.json | 428 + web/src/locales/hi.json | 191 + web/src/locales/hr.json | 277 + web/src/locales/hu.json | 302 + web/src/locales/id.json | 446 + web/src/locales/it.json | 263 + web/src/locales/ja.json | 312 + web/src/locales/ka-GE.json | 312 + web/src/locales/ko.json | 279 + web/src/locales/mr.json | 306 + web/src/locales/nb.json | 361 + web/src/locales/nl.json | 245 + web/src/locales/pl.json | 315 + web/src/locales/pt-BR.json | 439 + web/src/locales/pt-PT.json | 349 + web/src/locales/ru.json | 411 + web/src/locales/sl.json | 393 + web/src/locales/sv.json | 142 + web/src/locales/th.json | 312 + web/src/locales/tr.json | 395 + web/src/locales/uk.json | 394 + web/src/locales/vi.json | 352 + web/src/locales/zh-Hans.json | 433 + web/src/locales/zh-Hant.json | 420 + web/src/main.tsx | 27 + web/src/pages/AdminSignIn.tsx | 24 + web/src/pages/Archived.tsx | 55 + web/src/pages/Attachments.tsx | 181 + web/src/pages/AuthCallback.tsx | 83 + web/src/pages/Explore.tsx | 36 + web/src/pages/ExportImport.tsx | 12 + web/src/pages/Home.tsx | 80 + web/src/pages/Inboxes.tsx | 68 + web/src/pages/Loading.tsx | 13 + web/src/pages/MemoDetail.tsx | 181 + web/src/pages/NotFound.tsx | 15 + web/src/pages/PermissionDenied.tsx | 15 + web/src/pages/Setting.tsx | 161 + web/src/pages/SignIn.tsx | 108 + web/src/pages/SignUp.tsx | 140 + web/src/pages/UserProfile.tsx | 125 + web/src/router/MemoDetailRedirect.tsx | 8 + web/src/router/index.tsx | 194 + web/src/store/attachment.ts | 59 + web/src/store/common.ts | 13 + web/src/store/index.ts | 8 + web/src/store/memo.ts | 150 + web/src/store/memoFilter.ts | 93 + web/src/store/user.ts | 267 + web/src/store/view.ts | 49 + web/src/store/workspace.ts | 105 + web/src/themes/COLOR_GUIDE.md | 299 + web/src/themes/default.css | 152 + web/src/themes/paper.css | 152 + web/src/themes/whitewall.css | 152 + web/src/types/common.d.ts | 1 + web/src/types/i18n.d.ts | 1 + web/src/types/modules/setting.d.ts | 1 + .../types/proto/api/v1/activity_service.ts | 682 ++ .../types/proto/api/v1/attachment_service.ts | 1114 ++ web/src/types/proto/api/v1/auth_service.ts | 641 ++ web/src/types/proto/api/v1/common.ts | 167 + web/src/types/proto/api/v1/idp_service.ts | 1142 ++ web/src/types/proto/api/v1/inbox_service.ts | 786 ++ .../types/proto/api/v1/markdown_service.ts | 3438 ++++++ web/src/types/proto/api/v1/memo_service.ts | 4258 +++++++ .../types/proto/api/v1/shortcut_service.ts | 808 ++ web/src/types/proto/api/v1/user_service.ts | 3411 ++++++ web/src/types/proto/api/v1/webhook_service.ts | 799 ++ .../types/proto/api/v1/workspace_service.ts | 1340 +++ web/src/types/proto/google/api/annotations.ts | 9 + web/src/types/proto/google/api/client.ts | 2087 ++++ .../types/proto/google/api/field_behavior.ts | 145 + web/src/types/proto/google/api/http.ts | 670 ++ web/src/types/proto/google/api/httpbody.ts | 152 + .../types/proto/google/api/launch_stage.ts | 121 + web/src/types/proto/google/api/resource.ts | 501 + web/src/types/proto/google/protobuf/any.ts | 206 + .../types/proto/google/protobuf/descriptor.ts | 5847 ++++++++++ .../types/proto/google/protobuf/duration.ts | 172 + web/src/types/proto/google/protobuf/empty.ts | 71 + .../types/proto/google/protobuf/field_mask.ts | 291 + .../types/proto/google/protobuf/timestamp.ts | 201 + web/src/types/statistics.ts | 55 + web/src/types/view.d.ts | 5 + web/src/utils/attachment.ts | 45 + web/src/utils/i18n.ts | 53 + web/src/utils/memo.ts | 27 + web/src/utils/theme.ts | 44 + web/src/utils/user.ts | 5 + web/src/utils/uuid.ts | 5 + web/tsconfig.json | 23 + web/vite.config.mts | 57 + 607 files changed, 136371 insertions(+), 234 deletions(-) create mode 100644 .dockerignore create mode 100644 .gitignore create mode 100644 .golangci.yaml create mode 100644 .goreleaser.yaml create mode 100644 CLAUDE.md create mode 100644 CODEOWNERS create mode 100644 Dockerfile create mode 100644 SECURITY.md create mode 100644 bin/memos/main.go create mode 100644 docker-compose.yml create mode 100644 export_import_messages.proto create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/base/resource_name.go create mode 100644 internal/base/resource_name_test.go create mode 100644 internal/profile/profile.go create mode 100644 internal/util/util.go create mode 100644 internal/util/util_test.go create mode 100644 internal/version/version.go create mode 100644 internal/version/version_test.go create mode 100644 plugin/cron/README.md create mode 100644 plugin/cron/chain.go create mode 100644 plugin/cron/chain_test.go create mode 100644 plugin/cron/constantdelay.go create mode 100644 plugin/cron/constantdelay_test.go create mode 100644 plugin/cron/cron.go create mode 100644 plugin/cron/cron_test.go create mode 100644 plugin/cron/logger.go create mode 100644 plugin/cron/option.go create mode 100644 plugin/cron/option_test.go create mode 100644 plugin/cron/parser.go create mode 100644 plugin/cron/parser_test.go create mode 100644 plugin/cron/spec.go create mode 100644 plugin/cron/spec_test.go create mode 100644 plugin/filter/common_converter.go create mode 100644 plugin/filter/converter.go create mode 100644 plugin/filter/dialect.go create mode 100644 plugin/filter/expr.go create mode 100644 plugin/filter/filter.go create mode 100644 plugin/filter/templates.go create mode 100644 plugin/httpgetter/html_meta.go create mode 100644 plugin/httpgetter/html_meta_test.go create mode 100644 plugin/httpgetter/http_getter.go create mode 100644 plugin/httpgetter/image.go create mode 100644 plugin/httpgetter/util.go create mode 100644 plugin/idp/idp.go create mode 100644 plugin/idp/oauth2/oauth2.go create mode 100644 plugin/idp/oauth2/oauth2_test.go create mode 100644 plugin/storage/s3/s3.go create mode 100644 plugin/webhook/webhook.go create mode 100644 plugin/webhook/webhook_test.go create mode 100644 proto/README.md create mode 100644 proto/api/v1/README.md create mode 100644 proto/api/v1/activity_service.proto create mode 100644 proto/api/v1/attachment_service.proto create mode 100644 proto/api/v1/auth_service.proto create mode 100644 proto/api/v1/common.proto create mode 100644 proto/api/v1/idp_service.proto create mode 100644 proto/api/v1/inbox_service.proto create mode 100644 proto/api/v1/markdown_service.proto create mode 100644 proto/api/v1/memo_service.proto create mode 100644 proto/api/v1/shortcut_service.proto create mode 100644 proto/api/v1/user_service.proto create mode 100644 proto/api/v1/webhook_service.proto create mode 100644 proto/api/v1/workspace_service.proto create mode 100644 proto/buf.gen.yaml create mode 100644 proto/buf.lock create mode 100644 proto/buf.yaml create mode 100644 proto/gen/api/v1/activity_service.pb.go create mode 100644 proto/gen/api/v1/activity_service.pb.gw.go create mode 100644 proto/gen/api/v1/activity_service_grpc.pb.go create mode 100644 proto/gen/api/v1/attachment_service.pb.go create mode 100644 proto/gen/api/v1/attachment_service.pb.gw.go create mode 100644 proto/gen/api/v1/attachment_service_grpc.pb.go create mode 100644 proto/gen/api/v1/auth_service.pb.go create mode 100644 proto/gen/api/v1/auth_service.pb.gw.go create mode 100644 proto/gen/api/v1/auth_service_grpc.pb.go create mode 100644 proto/gen/api/v1/common.pb.go create mode 100644 proto/gen/api/v1/idp_service.pb.go create mode 100644 proto/gen/api/v1/idp_service.pb.gw.go create mode 100644 proto/gen/api/v1/idp_service_grpc.pb.go create mode 100644 proto/gen/api/v1/inbox_service.pb.go create mode 100644 proto/gen/api/v1/inbox_service.pb.gw.go create mode 100644 proto/gen/api/v1/inbox_service_grpc.pb.go create mode 100644 proto/gen/api/v1/markdown_service.pb.go create mode 100644 proto/gen/api/v1/markdown_service.pb.gw.go create mode 100644 proto/gen/api/v1/markdown_service_grpc.pb.go create mode 100644 proto/gen/api/v1/memo_service.pb.go create mode 100644 proto/gen/api/v1/memo_service.pb.gw.go create mode 100644 proto/gen/api/v1/memo_service_grpc.pb.go create mode 100644 proto/gen/api/v1/shortcut_service.pb.go create mode 100644 proto/gen/api/v1/shortcut_service.pb.gw.go create mode 100644 proto/gen/api/v1/shortcut_service_grpc.pb.go create mode 100644 proto/gen/api/v1/user_service.pb.go create mode 100644 proto/gen/api/v1/user_service.pb.gw.go create mode 100644 proto/gen/api/v1/user_service_grpc.pb.go create mode 100644 proto/gen/api/v1/webhook_service.pb.go create mode 100644 proto/gen/api/v1/webhook_service.pb.gw.go create mode 100644 proto/gen/api/v1/webhook_service_grpc.pb.go create mode 100644 proto/gen/api/v1/workspace_service.pb.go create mode 100644 proto/gen/api/v1/workspace_service.pb.gw.go create mode 100644 proto/gen/api/v1/workspace_service_grpc.pb.go create mode 100644 proto/gen/apidocs.swagger.yaml create mode 100644 proto/gen/store/activity.pb.go create mode 100644 proto/gen/store/attachment.pb.go create mode 100644 proto/gen/store/idp.pb.go create mode 100644 proto/gen/store/inbox.pb.go create mode 100644 proto/gen/store/memo.pb.go create mode 100644 proto/gen/store/user_setting.pb.go create mode 100644 proto/gen/store/workspace_setting.pb.go create mode 100644 proto/store/activity.proto create mode 100644 proto/store/attachment.proto create mode 100644 proto/store/idp.proto create mode 100644 proto/store/inbox.proto create mode 100644 proto/store/memo.proto create mode 100644 proto/store/user_setting.proto create mode 100644 proto/store/workspace_setting.proto create mode 100644 scripts/Dockerfile create mode 100644 scripts/build.sh create mode 100644 scripts/compose.yaml create mode 100644 scripts/entrypoint.sh create mode 100644 server/profiler/profiler.go create mode 100644 server/router/api/v1/acl.go create mode 100644 server/router/api/v1/acl_config.go create mode 100644 server/router/api/v1/activity_service.go create mode 100644 server/router/api/v1/attachment_service.go create mode 100644 server/router/api/v1/auth.go create mode 100644 server/router/api/v1/auth_service.go create mode 100644 server/router/api/v1/auth_service_client_info_test.go create mode 100644 server/router/api/v1/common.go create mode 100644 server/router/api/v1/health_service.go create mode 100644 server/router/api/v1/idp_service.go create mode 100644 server/router/api/v1/inbox_service.go create mode 100644 server/router/api/v1/logger_interceptor.go create mode 100644 server/router/api/v1/markdown_service.go create mode 100644 server/router/api/v1/memo_attachment_service.go create mode 100644 server/router/api/v1/memo_export_import.go create mode 100644 server/router/api/v1/memo_relation_service.go create mode 100644 server/router/api/v1/memo_service.go create mode 100644 server/router/api/v1/memo_service_converter.go create mode 100644 server/router/api/v1/memo_service_filter.go create mode 100644 server/router/api/v1/reaction_service.go create mode 100644 server/router/api/v1/resource_name.go create mode 100644 server/router/api/v1/shortcut_service.go create mode 100644 server/router/api/v1/test/idp_service_test.go create mode 100644 server/router/api/v1/test/inbox_service_test.go create mode 100644 server/router/api/v1/test/shortcut_service_test.go create mode 100644 server/router/api/v1/test/test_helper.go create mode 100644 server/router/api/v1/test/user_service_stats_test.go create mode 100644 server/router/api/v1/test/webhook_service_test.go create mode 100644 server/router/api/v1/test/workspace_service_test.go create mode 100644 server/router/api/v1/test_auth.go create mode 100644 server/router/api/v1/user_service.go create mode 100644 server/router/api/v1/user_service_stats.go create mode 100644 server/router/api/v1/v1.go create mode 100644 server/router/api/v1/webhook_service.go create mode 100644 server/router/api/v1/workspace_service.go create mode 100644 server/router/frontend/frontend.go create mode 100644 server/router/rss/rss.go create mode 100644 server/runner/memopayload/runner.go create mode 100644 server/runner/s3presign/runner.go create mode 100644 server/server.go create mode 100644 store/activity.go create mode 100644 store/attachment.go create mode 100644 store/cache.go create mode 100644 store/cache/cache.go create mode 100644 store/cache/cache_test.go create mode 100644 store/common.go create mode 100644 store/db/db.go create mode 100644 store/db/mysql/activity.go create mode 100644 store/db/mysql/attachment.go create mode 100644 store/db/mysql/common.go create mode 100644 store/db/mysql/idp.go create mode 100644 store/db/mysql/inbox.go create mode 100644 store/db/mysql/memo.go create mode 100644 store/db/mysql/memo_filter.go create mode 100644 store/db/mysql/memo_filter_test.go create mode 100644 store/db/mysql/memo_relation.go create mode 100644 store/db/mysql/migration_history.go create mode 100644 store/db/mysql/mysql.go create mode 100644 store/db/mysql/reaction.go create mode 100644 store/db/mysql/user.go create mode 100644 store/db/mysql/user_setting.go create mode 100644 store/db/mysql/workspace_setting.go create mode 100644 store/db/postgres/activity.go create mode 100644 store/db/postgres/attachment.go create mode 100644 store/db/postgres/common.go create mode 100644 store/db/postgres/idp.go create mode 100644 store/db/postgres/inbox.go create mode 100644 store/db/postgres/memo.go create mode 100644 store/db/postgres/memo_filter.go create mode 100644 store/db/postgres/memo_filter_test.go create mode 100644 store/db/postgres/memo_relation.go create mode 100644 store/db/postgres/migration_history.go create mode 100644 store/db/postgres/postgres.go create mode 100644 store/db/postgres/reaction.go create mode 100644 store/db/postgres/user.go create mode 100644 store/db/postgres/user_setting.go create mode 100644 store/db/postgres/workspace_setting.go create mode 100644 store/db/sqlite/activity.go create mode 100644 store/db/sqlite/attachment.go create mode 100644 store/db/sqlite/common.go create mode 100644 store/db/sqlite/idp.go create mode 100644 store/db/sqlite/inbox.go create mode 100644 store/db/sqlite/memo.go create mode 100644 store/db/sqlite/memo_filter.go create mode 100644 store/db/sqlite/memo_filter_test.go create mode 100644 store/db/sqlite/memo_relation.go create mode 100644 store/db/sqlite/migration_history.go create mode 100644 store/db/sqlite/reaction.go create mode 100644 store/db/sqlite/sqlite.go create mode 100644 store/db/sqlite/user.go create mode 100644 store/db/sqlite/user_setting.go create mode 100644 store/db/sqlite/workspace_setting.go create mode 100644 store/driver.go create mode 100644 store/idp.go create mode 100644 store/inbox.go create mode 100644 store/memo.go create mode 100644 store/memo_relation.go create mode 100644 store/migration/mysql/0.17/00__inbox.sql create mode 100644 store/migration/mysql/0.17/01__delete_activity.sql create mode 100644 store/migration/mysql/0.18/00__extend_text.sql create mode 100644 store/migration/mysql/0.18/01__webhook.sql create mode 100644 store/migration/mysql/0.18/02__user_setting.sql create mode 100644 store/migration/mysql/0.19/00__add_resource_name.sql create mode 100644 store/migration/mysql/0.20/00__reaction.sql create mode 100644 store/migration/mysql/0.21/00__user_description.sql create mode 100644 store/migration/mysql/0.21/01__rename_uid.sql create mode 100644 store/migration/mysql/0.22/00__resource_storage_type.sql create mode 100644 store/migration/mysql/0.22/01__memo_tags.sql create mode 100644 store/migration/mysql/0.22/02__memo_payload.sql create mode 100644 store/migration/mysql/0.22/03__drop_tag.sql create mode 100644 store/migration/mysql/0.23/00__reactions.sql create mode 100644 store/migration/mysql/0.24/00__memo.sql create mode 100644 store/migration/mysql/0.24/01__memo_pinned.sql create mode 100644 store/migration/mysql/0.24/02__s3_reference_length.sql create mode 100644 store/migration/mysql/0.25/00__remove_webhook.sql create mode 100644 store/migration/mysql/LATEST.sql create mode 100644 store/migration/postgres/0.19/00__add_resource_name.sql create mode 100644 store/migration/postgres/0.20/00__reaction.sql create mode 100644 store/migration/postgres/0.21/00__user_description.sql create mode 100644 store/migration/postgres/0.21/01__rename_uid.sql create mode 100644 store/migration/postgres/0.22/00__resource_storage_type.sql create mode 100644 store/migration/postgres/0.22/01__memo_tags.sql create mode 100644 store/migration/postgres/0.22/02__memo_payload.sql create mode 100644 store/migration/postgres/0.22/03__drop_tag.sql create mode 100644 store/migration/postgres/0.23/00__reactions.sql create mode 100644 store/migration/postgres/0.24/00__memo.sql create mode 100644 store/migration/postgres/0.24/01__memo_pinned.sql create mode 100644 store/migration/postgres/0.25/00__remove_webhook.sql create mode 100644 store/migration/postgres/LATEST.sql create mode 100644 store/migration/sqlite/0.10/00__activity.sql create mode 100644 store/migration/sqlite/0.11/00__user_avatar.sql create mode 100644 store/migration/sqlite/0.11/01__idp.sql create mode 100644 store/migration/sqlite/0.11/02__storage.sql create mode 100644 store/migration/sqlite/0.12/00__user_setting.sql create mode 100644 store/migration/sqlite/0.12/01__system_setting.sql create mode 100644 store/migration/sqlite/0.12/03__resource_internal_path.sql create mode 100644 store/migration/sqlite/0.12/04__resource_public_id.sql create mode 100644 store/migration/sqlite/0.13/00__memo_relation.sql create mode 100644 store/migration/sqlite/0.13/01__remove_memo_organizer_id.sql create mode 100644 store/migration/sqlite/0.14/00__drop_resource_public_id.sql create mode 100644 store/migration/sqlite/0.14/01__create_indexes.sql create mode 100644 store/migration/sqlite/0.15/00__drop_user_open_id.sql create mode 100644 store/migration/sqlite/0.16/00__add_memo_id_to_resource.sql create mode 100644 store/migration/sqlite/0.16/01__drop_shortcut_table.sql create mode 100644 store/migration/sqlite/0.17/00__inbox.sql create mode 100644 store/migration/sqlite/0.17/01__delete_activities.sql create mode 100644 store/migration/sqlite/0.18/00__webhook.sql create mode 100644 store/migration/sqlite/0.18/01__user_setting.sql create mode 100644 store/migration/sqlite/0.19/00__add_resource_name.sql create mode 100644 store/migration/sqlite/0.2/00__user_role.sql create mode 100644 store/migration/sqlite/0.2/01__memo_visibility.sql create mode 100644 store/migration/sqlite/0.20/00__reaction.sql create mode 100644 store/migration/sqlite/0.21/00__user_description.sql create mode 100644 store/migration/sqlite/0.21/01__rename_uid.sql create mode 100644 store/migration/sqlite/0.22/00__resource_storage_type.sql create mode 100644 store/migration/sqlite/0.22/01__memo_tags.sql create mode 100644 store/migration/sqlite/0.22/02__memo_payload.sql create mode 100644 store/migration/sqlite/0.22/03__drop_tag.sql create mode 100644 store/migration/sqlite/0.23/00__reactions.sql create mode 100644 store/migration/sqlite/0.24/00__memo.sql create mode 100644 store/migration/sqlite/0.24/01__memo_pinned.sql create mode 100644 store/migration/sqlite/0.25/00__remove_webhook.sql create mode 100644 store/migration/sqlite/0.3/00__memo_visibility_protected.sql create mode 100644 store/migration/sqlite/0.4/00__user_setting.sql create mode 100644 store/migration/sqlite/0.5/00__regenerate_foreign_keys.sql create mode 100644 store/migration/sqlite/0.5/01__memo_resource.sql create mode 100644 store/migration/sqlite/0.5/02__system_setting.sql create mode 100644 store/migration/sqlite/0.5/03__resource_extermal_link.sql create mode 100644 store/migration/sqlite/0.6/00__recreate_triggers.sql create mode 100644 store/migration/sqlite/0.7/00__remove_fk.sql create mode 100644 store/migration/sqlite/0.7/01__remove_triggers.sql create mode 100644 store/migration/sqlite/0.8/00__migration_history.sql create mode 100644 store/migration/sqlite/0.8/01__user_username.sql create mode 100644 store/migration/sqlite/0.9/00__tag.sql create mode 100644 store/migration/sqlite/LATEST.sql create mode 100644 store/migration_history.go create mode 100644 store/migrator.go create mode 100644 store/reaction.go create mode 100644 store/seed/sqlite/00__reset.sql create mode 100644 store/seed/sqlite/01__dump.sql create mode 100644 store/store.go create mode 100644 store/test/README.md create mode 100644 store/test/activity_test.go create mode 100644 store/test/attachment_test.go create mode 100644 store/test/idp_test.go create mode 100644 store/test/inbox_test.go create mode 100644 store/test/memo_relation_test.go create mode 100644 store/test/memo_test.go create mode 100644 store/test/migrator_test.go create mode 100644 store/test/reaction_test.go create mode 100644 store/test/store.go create mode 100644 store/test/user_setting_test.go create mode 100644 store/test/user_test.go create mode 100644 store/test/workspace_setting_test.go create mode 100644 store/user.go create mode 100644 store/user_setting.go create mode 100644 store/workspace_setting.go create mode 100644 web/.gitignore create mode 100644 web/.prettierrc.js create mode 100644 web/README.md create mode 100644 web/components.json create mode 100644 web/eslint.config.mjs create mode 100644 web/index.html create mode 100644 web/package-lock.json create mode 100644 web/package.json create mode 100644 web/pnpm-lock.yaml create mode 100644 web/public/android-chrome-192x192.png create mode 100644 web/public/android-chrome-512x512.png create mode 100644 web/public/apple-touch-icon.png create mode 100644 web/public/full-logo.webp create mode 100644 web/public/logo.webp create mode 100644 web/public/site.webmanifest create mode 100644 web/src/App.tsx create mode 100644 web/src/components/ActivityCalendar/ActivityCalendar.tsx create mode 100644 web/src/components/ActivityCalendar/index.ts create mode 100644 web/src/components/AppearanceSelect.tsx create mode 100644 web/src/components/AttachmentIcon.tsx create mode 100644 web/src/components/AuthFooter.tsx create mode 100644 web/src/components/BrandBanner.tsx create mode 100644 web/src/components/ChangeMemberPasswordDialog.tsx create mode 100644 web/src/components/CreateAccessTokenDialog.tsx create mode 100644 web/src/components/CreateIdentityProviderDialog.tsx create mode 100644 web/src/components/CreateShortcutDialog.tsx create mode 100644 web/src/components/CreateUserDialog.tsx create mode 100644 web/src/components/CreateWebhookDialog.tsx create mode 100644 web/src/components/DateTimeInput.tsx create mode 100644 web/src/components/Empty.tsx create mode 100644 web/src/components/ExportImport.tsx create mode 100644 web/src/components/HomeSidebar/HomeSidebar.tsx create mode 100644 web/src/components/HomeSidebar/HomeSidebarDrawer.tsx create mode 100644 web/src/components/HomeSidebar/ShortcutsSection.tsx create mode 100644 web/src/components/HomeSidebar/TagsSection.tsx create mode 100644 web/src/components/HomeSidebar/index.ts create mode 100644 web/src/components/Inbox/MemoCommentMessage.tsx create mode 100644 web/src/components/LeafletMap.tsx create mode 100644 web/src/components/LearnMore.tsx create mode 100644 web/src/components/LocaleSelect.tsx create mode 100644 web/src/components/MasonryView/MasonryView.tsx create mode 100644 web/src/components/MasonryView/README.md create mode 100644 web/src/components/MasonryView/index.ts create mode 100644 web/src/components/MemoActionMenu.tsx create mode 100644 web/src/components/MemoAttachment.tsx create mode 100644 web/src/components/MemoAttachmentListView.tsx create mode 100644 web/src/components/MemoContent/Blockquote.tsx create mode 100644 web/src/components/MemoContent/Bold.tsx create mode 100644 web/src/components/MemoContent/BoldItalic.tsx create mode 100644 web/src/components/MemoContent/Code.tsx create mode 100644 web/src/components/MemoContent/CodeBlock.tsx create mode 100644 web/src/components/MemoContent/EmbeddedContent/EmbeddedAttachment.tsx create mode 100644 web/src/components/MemoContent/EmbeddedContent/EmbeddedMemo.tsx create mode 100644 web/src/components/MemoContent/EmbeddedContent/Error.tsx create mode 100644 web/src/components/MemoContent/EmbeddedContent/index.tsx create mode 100644 web/src/components/MemoContent/EscapingCharacter.tsx create mode 100644 web/src/components/MemoContent/HTMLElement.tsx create mode 100644 web/src/components/MemoContent/Heading.tsx create mode 100644 web/src/components/MemoContent/Highlight.tsx create mode 100644 web/src/components/MemoContent/HorizontalRule.tsx create mode 100644 web/src/components/MemoContent/Image.tsx create mode 100644 web/src/components/MemoContent/Italic.tsx create mode 100644 web/src/components/MemoContent/LineBreak.tsx create mode 100644 web/src/components/MemoContent/Link.tsx create mode 100644 web/src/components/MemoContent/List.tsx create mode 100644 web/src/components/MemoContent/Math.tsx create mode 100644 web/src/components/MemoContent/MermaidBlock.tsx create mode 100644 web/src/components/MemoContent/OrderedListItem.tsx create mode 100644 web/src/components/MemoContent/Paragraph.tsx create mode 100644 web/src/components/MemoContent/ReferencedContent/Error.tsx create mode 100644 web/src/components/MemoContent/ReferencedContent/ReferencedMemo.tsx create mode 100644 web/src/components/MemoContent/ReferencedContent/index.tsx create mode 100644 web/src/components/MemoContent/Renderer.tsx create mode 100644 web/src/components/MemoContent/Spoiler.tsx create mode 100644 web/src/components/MemoContent/Strikethrough.tsx create mode 100644 web/src/components/MemoContent/Subscript.tsx create mode 100644 web/src/components/MemoContent/Superscript.tsx create mode 100644 web/src/components/MemoContent/Table.tsx create mode 100644 web/src/components/MemoContent/Tag.tsx create mode 100644 web/src/components/MemoContent/TaskListItem.tsx create mode 100644 web/src/components/MemoContent/Text.tsx create mode 100644 web/src/components/MemoContent/UnorderedListItem.tsx create mode 100644 web/src/components/MemoContent/index.tsx create mode 100644 web/src/components/MemoContent/types/context.ts create mode 100644 web/src/components/MemoContent/types/index.ts create mode 100644 web/src/components/MemoDetailSidebar/MemoDetailSidebar.tsx create mode 100644 web/src/components/MemoDetailSidebar/MemoDetailSidebarDrawer.tsx create mode 100644 web/src/components/MemoDetailSidebar/index.ts create mode 100644 web/src/components/MemoDisplaySettingMenu.tsx create mode 100644 web/src/components/MemoEditor/ActionButton/AddMemoRelationPopover.tsx create mode 100644 web/src/components/MemoEditor/ActionButton/LocationSelector.tsx create mode 100644 web/src/components/MemoEditor/ActionButton/MarkdownMenu.tsx create mode 100644 web/src/components/MemoEditor/ActionButton/TagSelector.tsx create mode 100644 web/src/components/MemoEditor/ActionButton/UploadAttachmentButton.tsx create mode 100644 web/src/components/MemoEditor/ActionButton/UploadResourceButton.tsx create mode 100644 web/src/components/MemoEditor/ActionButton/VisibilitySelector.tsx create mode 100644 web/src/components/MemoEditor/AttachmentListView.tsx create mode 100644 web/src/components/MemoEditor/Editor/TagSuggestions.tsx create mode 100644 web/src/components/MemoEditor/Editor/index.tsx create mode 100644 web/src/components/MemoEditor/RelationListView.tsx create mode 100644 web/src/components/MemoEditor/SortableItem.tsx create mode 100644 web/src/components/MemoEditor/handlers.ts create mode 100644 web/src/components/MemoEditor/index.tsx create mode 100644 web/src/components/MemoEditor/types/context.ts create mode 100644 web/src/components/MemoEditor/types/index.ts create mode 100644 web/src/components/MemoFilters.tsx create mode 100644 web/src/components/MemoLocationView.tsx create mode 100644 web/src/components/MemoReactionListView.tsx create mode 100644 web/src/components/MemoRelationForceGraph/MemoRelationForceGraph.tsx create mode 100644 web/src/components/MemoRelationForceGraph/index.ts create mode 100644 web/src/components/MemoRelationForceGraph/types.ts create mode 100644 web/src/components/MemoRelationForceGraph/utils.ts create mode 100644 web/src/components/MemoRelationListView.tsx create mode 100644 web/src/components/MemoResource.tsx create mode 100644 web/src/components/MemoView.tsx create mode 100644 web/src/components/MobileHeader.tsx create mode 100644 web/src/components/Navigation.tsx create mode 100644 web/src/components/NavigationDrawer.tsx create mode 100644 web/src/components/PagedMemoList/PagedMemoList.tsx create mode 100644 web/src/components/PagedMemoList/index.ts create mode 100644 web/src/components/PasswordSignInForm.tsx create mode 100644 web/src/components/PreviewImageDialog.tsx create mode 100644 web/src/components/ReactionSelector.tsx create mode 100644 web/src/components/ReactionView.tsx create mode 100644 web/src/components/RenameTagDialog.tsx create mode 100644 web/src/components/RequiredBadge.tsx create mode 100644 web/src/components/SearchBar.tsx create mode 100644 web/src/components/Settings/AccessTokenSection.tsx create mode 100644 web/src/components/Settings/MemberSection.tsx create mode 100644 web/src/components/Settings/MemoRelatedSettings.tsx create mode 100644 web/src/components/Settings/MyAccountSection.tsx create mode 100644 web/src/components/Settings/PreferencesSection.tsx create mode 100644 web/src/components/Settings/SSOSection.tsx create mode 100644 web/src/components/Settings/SectionMenuItem.tsx create mode 100644 web/src/components/Settings/StorageSection.tsx create mode 100644 web/src/components/Settings/UserSessionsSection.tsx create mode 100644 web/src/components/Settings/WebhookSection.tsx create mode 100644 web/src/components/Settings/WorkspaceSection.tsx create mode 100644 web/src/components/StatisticsView/MonthNavigator.tsx create mode 100644 web/src/components/StatisticsView/StatCard.tsx create mode 100644 web/src/components/StatisticsView/StatisticsView.tsx create mode 100644 web/src/components/StatisticsView/index.ts create mode 100644 web/src/components/TagTree.tsx create mode 100644 web/src/components/ThemeSelector.tsx create mode 100644 web/src/components/UpdateAccountDialog.tsx create mode 100644 web/src/components/UpdateCustomizedProfileDialog.new.tsx create mode 100644 web/src/components/UpdateCustomizedProfileDialog.tsx create mode 100644 web/src/components/UserAvatar.tsx create mode 100644 web/src/components/UserBanner.tsx create mode 100644 web/src/components/VisibilityIcon.tsx create mode 100644 web/src/components/examples/WorkspaceSection.example.tsx create mode 100644 web/src/components/kit/OverflowTip.tsx create mode 100644 web/src/components/kit/README.md create mode 100644 web/src/components/kit/SquareDiv.tsx create mode 100644 web/src/components/ui/badge.tsx create mode 100644 web/src/components/ui/button.tsx create mode 100644 web/src/components/ui/checkbox.tsx create mode 100644 web/src/components/ui/dialog.tsx create mode 100644 web/src/components/ui/dropdown-menu.tsx create mode 100644 web/src/components/ui/input.tsx create mode 100644 web/src/components/ui/label.tsx create mode 100644 web/src/components/ui/popover.tsx create mode 100644 web/src/components/ui/radio-group.tsx create mode 100644 web/src/components/ui/select.tsx create mode 100644 web/src/components/ui/separator.tsx create mode 100644 web/src/components/ui/sheet.tsx create mode 100644 web/src/components/ui/switch.tsx create mode 100644 web/src/components/ui/textarea.tsx create mode 100644 web/src/components/ui/tooltip.tsx create mode 100644 web/src/grpcweb.ts create mode 100644 web/src/helpers/consts.ts create mode 100644 web/src/helpers/utils.ts create mode 100644 web/src/hooks/index.ts create mode 100644 web/src/hooks/useAsyncEffect.ts create mode 100644 web/src/hooks/useCurrentUser.ts create mode 100644 web/src/hooks/useDialog.ts create mode 100644 web/src/hooks/useLoading.ts create mode 100644 web/src/hooks/useNavigateTo.ts create mode 100644 web/src/hooks/useResponsiveWidth.ts create mode 100644 web/src/hooks/useStatisticsData.ts create mode 100644 web/src/i18n.ts create mode 100644 web/src/index.css create mode 100644 web/src/layouts/HomeLayout.tsx create mode 100644 web/src/layouts/RootLayout.tsx create mode 100644 web/src/lib/utils.ts create mode 100644 web/src/locales/ar.json create mode 100644 web/src/locales/ca.json create mode 100644 web/src/locales/cs.json create mode 100644 web/src/locales/de.json create mode 100644 web/src/locales/en-GB.json create mode 100644 web/src/locales/en.json create mode 100644 web/src/locales/es.json create mode 100644 web/src/locales/fa.json create mode 100644 web/src/locales/fr.json create mode 100644 web/src/locales/hi.json create mode 100644 web/src/locales/hr.json create mode 100644 web/src/locales/hu.json create mode 100644 web/src/locales/id.json create mode 100644 web/src/locales/it.json create mode 100644 web/src/locales/ja.json create mode 100644 web/src/locales/ka-GE.json create mode 100644 web/src/locales/ko.json create mode 100644 web/src/locales/mr.json create mode 100644 web/src/locales/nb.json create mode 100644 web/src/locales/nl.json create mode 100644 web/src/locales/pl.json create mode 100644 web/src/locales/pt-BR.json create mode 100644 web/src/locales/pt-PT.json create mode 100644 web/src/locales/ru.json create mode 100644 web/src/locales/sl.json create mode 100644 web/src/locales/sv.json create mode 100644 web/src/locales/th.json create mode 100644 web/src/locales/tr.json create mode 100644 web/src/locales/uk.json create mode 100644 web/src/locales/vi.json create mode 100644 web/src/locales/zh-Hans.json create mode 100644 web/src/locales/zh-Hant.json create mode 100644 web/src/main.tsx create mode 100644 web/src/pages/AdminSignIn.tsx create mode 100644 web/src/pages/Archived.tsx create mode 100644 web/src/pages/Attachments.tsx create mode 100644 web/src/pages/AuthCallback.tsx create mode 100644 web/src/pages/Explore.tsx create mode 100644 web/src/pages/ExportImport.tsx create mode 100644 web/src/pages/Home.tsx create mode 100644 web/src/pages/Inboxes.tsx create mode 100644 web/src/pages/Loading.tsx create mode 100644 web/src/pages/MemoDetail.tsx create mode 100644 web/src/pages/NotFound.tsx create mode 100644 web/src/pages/PermissionDenied.tsx create mode 100644 web/src/pages/Setting.tsx create mode 100644 web/src/pages/SignIn.tsx create mode 100644 web/src/pages/SignUp.tsx create mode 100644 web/src/pages/UserProfile.tsx create mode 100644 web/src/router/MemoDetailRedirect.tsx create mode 100644 web/src/router/index.tsx create mode 100644 web/src/store/attachment.ts create mode 100644 web/src/store/common.ts create mode 100644 web/src/store/index.ts create mode 100644 web/src/store/memo.ts create mode 100644 web/src/store/memoFilter.ts create mode 100644 web/src/store/user.ts create mode 100644 web/src/store/view.ts create mode 100644 web/src/store/workspace.ts create mode 100644 web/src/themes/COLOR_GUIDE.md create mode 100644 web/src/themes/default.css create mode 100644 web/src/themes/paper.css create mode 100644 web/src/themes/whitewall.css create mode 100644 web/src/types/common.d.ts create mode 100644 web/src/types/i18n.d.ts create mode 100644 web/src/types/modules/setting.d.ts create mode 100644 web/src/types/proto/api/v1/activity_service.ts create mode 100644 web/src/types/proto/api/v1/attachment_service.ts create mode 100644 web/src/types/proto/api/v1/auth_service.ts create mode 100644 web/src/types/proto/api/v1/common.ts create mode 100644 web/src/types/proto/api/v1/idp_service.ts create mode 100644 web/src/types/proto/api/v1/inbox_service.ts create mode 100644 web/src/types/proto/api/v1/markdown_service.ts create mode 100644 web/src/types/proto/api/v1/memo_service.ts create mode 100644 web/src/types/proto/api/v1/shortcut_service.ts create mode 100644 web/src/types/proto/api/v1/user_service.ts create mode 100644 web/src/types/proto/api/v1/webhook_service.ts create mode 100644 web/src/types/proto/api/v1/workspace_service.ts create mode 100644 web/src/types/proto/google/api/annotations.ts create mode 100644 web/src/types/proto/google/api/client.ts create mode 100644 web/src/types/proto/google/api/field_behavior.ts create mode 100644 web/src/types/proto/google/api/http.ts create mode 100644 web/src/types/proto/google/api/httpbody.ts create mode 100644 web/src/types/proto/google/api/launch_stage.ts create mode 100644 web/src/types/proto/google/api/resource.ts create mode 100644 web/src/types/proto/google/protobuf/any.ts create mode 100644 web/src/types/proto/google/protobuf/descriptor.ts create mode 100644 web/src/types/proto/google/protobuf/duration.ts create mode 100644 web/src/types/proto/google/protobuf/empty.ts create mode 100644 web/src/types/proto/google/protobuf/field_mask.ts create mode 100644 web/src/types/proto/google/protobuf/timestamp.ts create mode 100644 web/src/types/statistics.ts create mode 100644 web/src/types/view.d.ts create mode 100644 web/src/utils/attachment.ts create mode 100644 web/src/utils/i18n.ts create mode 100644 web/src/utils/memo.ts create mode 100644 web/src/utils/theme.ts create mode 100644 web/src/utils/user.ts create mode 100644 web/src/utils/uuid.ts create mode 100644 web/tsconfig.json create mode 100644 web/vite.config.mts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f3dbab1 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,94 @@ +# Git +.git +.gitignore +.gitattributes +.github/ + +# Documentation +*.md +!README.md +CODEOWNERS +LICENSE + +# Development files +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# OS files +.DS_Store +Thumbs.db + +# Go +# These are handled by go mod +vendor/ + +# Node.js +web/node_modules/ +web/.next/ +web/dist/ +web/build/ +web/*.log +web/.env.local +web/.env.development.local +web/.env.test.local +web/.env.production.local + +# Build artifacts +# Note: Keep bin/ for source code, but exclude built binaries +dist/ +*.exe +*.exe~ +*.dll +*.so +*.dylib +*.test +*.out + +# Coverage +*.cover +*.coverprofile +coverage.txt +coverage.html + +# Temporary files +tmp/ +temp/ +*.tmp +*.temp + +# IDE +*.iml +.project +.classpath +.settings/ + +# Docker +Dockerfile* +docker-compose*.yml +.dockerignore + +# CI/CD +.travis.yml +.circleci/ +.github/workflows/ +Jenkinsfile + +# Logs +*.log + +# Database files (if any) +*.db +*.sqlite +*.sqlite3 + +# Configuration +.env +.env.* +config.yaml +config.json + +# Scripts +scripts/ \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e512e7a --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +# temp folder +tmp + +# Frontend asset +web/dist + +# build folder +build + +.DS_Store + +# Jetbrains +.idea + +# Docker Compose Environment File +.env + +dist + +# VSCode settings +.vscode diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..b97938f --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,101 @@ +version: "2" + +linters: + enable: + - revive + - govet + - staticcheck + - misspell + - gocritic + - sqlclosecheck + - rowserrcheck + - nilerr + - godot + - forbidigo + - mirror + - bodyclose + disable: + - errcheck + settings: + exhaustive: + explicit-exhaustive-switch: false + staticcheck: + checks: + - all + - -ST1000 + - -ST1003 + - -ST1021 + - -QF1003 + revive: + # Default to run all linters so that new rules in the future could automatically be added to the static check. + enable-all-rules: true + rules: + # The following rules are too strict and make coding harder. We do not enable them for now. + - name: file-header + disabled: true + - name: line-length-limit + disabled: true + - name: function-length + disabled: true + - name: max-public-structs + disabled: true + - name: function-result-limit + disabled: true + - name: banned-characters + disabled: true + - name: argument-limit + disabled: true + - name: cognitive-complexity + disabled: true + - name: cyclomatic + disabled: true + - name: confusing-results + disabled: true + - name: add-constant + disabled: true + - name: flag-parameter + disabled: true + - name: nested-structs + disabled: true + - name: import-shadowing + disabled: true + - name: early-return + disabled: true + - name: use-any + disabled: true + - name: exported + disabled: true + - name: unhandled-error + disabled: true + - name: if-return + disabled: true + - name: max-control-nesting + disabled: true + - name: redefines-builtin-id + disabled: true + - name: package-comments + disabled: true + gocritic: + disabled-checks: + - ifElseChain + govet: + settings: + printf: # The name of the analyzer, run `go tool vet help` to see the list of all analyzers + funcs: # Run `go tool vet help printf` to see the full configuration of `printf`. + - common.Errorf + enable-all: true + disable: + - fieldalignment + - shadow + forbidigo: + forbid: + - pattern: 'fmt\.Errorf(# Please use errors\.Wrap\|Wrapf\|Errorf instead)?' + - pattern: 'ioutil\.ReadDir(# Please use os\.ReadDir)?' + +formatters: + enable: + - goimports + settings: + goimports: + local-prefixes: + - github.com/usememos/memos diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..016dd0e --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,36 @@ +version: 1 + +before: + hooks: + # You may remove this if you don't use go modules. + - go mod tidy + +builds: + - main: ./bin/memos + binary: memos + goos: + - linux + - darwin + +archives: + - format: tar.gz + # this name template makes the OS and Arch compatible with the results of `uname`. + name_template: >- + {{ .ProjectName }}_{{ .Tag }}_{{ .Os }}_{{ .Arch }} + +changelog: + sort: asc + filters: + exclude: + - "^docs:" + - "^test:" + +checksum: + disable: true + +release: + draft: true + replace_existing_draft: true + make_latest: true + mode: replace + skip_upload: false diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..3b7e9a2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,236 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Memos is a self-hosted note-taking and knowledge management platform with a Go backend and React/TypeScript frontend. The architecture follows clean separation of concerns with gRPC APIs, REST gateway, and database abstraction. + +## Development Commands + +### Backend (Go) +```bash +# Run in development mode +go run ./bin/memos/main.go --mode dev --port 8081 + +# Build binary +go build -o ./build/memos ./bin/memos/main.go +# OR use build script +./scripts/build.sh + +# Run tests +go test -v ./... +go test -cover ./... + +# Run specific test packages +go test -v ./store/test/ +go test -v ./server/router/api/v1/test/ +``` + +### Frontend (React/TypeScript) +```bash +cd web/ + +# Development server (http://localhost:3001) +pnpm dev + +# Build for production +pnpm build + +# Build for release (outputs to server/router/frontend/dist) +pnpm release + +# Lint and type check +pnpm lint +``` + +### Full Development Setup +1. **Backend**: `go run ./bin/memos/main.go --mode dev --port 8081` +2. **Frontend**: `cd web && pnpm dev` +3. **Access**: Backend API at `http://localhost:8081`, Frontend at `http://localhost:3001` + +## Architecture Overview + +### Backend Structure +- **`/bin/memos/main.go`** - Application entrypoint with CLI and server initialization +- **`/server/`** - HTTP/gRPC server with Echo framework and cmux for protocol multiplexing +- **`/server/router/api/v1/`** - gRPC services with REST gateway via grpc-gateway +- **`/store/`** - Data access layer with multi-database support (SQLite/PostgreSQL/MySQL) +- **`/store/db/`** - Database-specific implementations with shared interface +- **`/proto/`** - Protocol buffer definitions for APIs and data models +- **`/internal/`** - Shared utilities, profile management, and version handling +- **`/plugin/`** - Modular plugins (S3 storage, OAuth, webhooks, etc.) + +### Frontend Structure +- **`/web/src/`** - React/TypeScript application +- **`/web/src/components/`** - Reusable UI components with shadcn/ui +- **`/web/src/pages/`** - Page-level components +- **`/web/src/store/`** - MobX state management +- **`/web/src/types/`** - TypeScript type definitions generated from protobuf + +### Key Architecture Patterns + +#### Server Architecture +- **Protocol Multiplexing**: Single port serves both HTTP and gRPC via cmux +- **gRPC-first**: Core APIs defined in protobuf, REST via grpc-gateway +- **Layered**: Router → Service → Store → Database +- **Middleware**: Authentication, logging, CORS handled via interceptors + +#### Database Layer +- **Multi-database**: Unified interface for SQLite, PostgreSQL, MySQL +- **Migration System**: Version-based schema migrations in `/store/migration/` +- **Driver Pattern**: `/store/db/{sqlite,postgres,mysql}/` with common interface +- **Caching**: Built-in cache layer for workspace settings, users, user settings + +#### Authentication & Security +- **JWT-based**: Secret key generated per workspace +- **gRPC Interceptors**: Authentication middleware for all API calls +- **Context Propagation**: User context flows through request lifecycle +- **Development vs Production**: Different secret handling based on mode + +## Database Operations + +### Supported Databases +- **SQLite** (default): `--driver sqlite --data ./data` +- **PostgreSQL**: `--driver postgres --dsn "postgres://..."` +- **MySQL**: `--driver mysql --dsn "user:pass@tcp(host:port)/db"` + +### Migration System +- Database schema managed via `/store/migration/{sqlite,postgres,mysql}/` +- Automatic migration on startup via `store.Migrate(ctx)` +- Version-based migration files (e.g., `0.22/00__memo_tags.sql`) + +## Testing Approach + +### Backend Testing +- **Store Tests**: `/store/test/*_test.go` with in-memory SQLite +- **API Tests**: `/server/router/api/v1/test/*_test.go` with full service setup +- **Test Helpers**: + - `NewTestingStore()` for isolated database testing + - `NewTestService()` for API integration testing +- **Test Patterns**: Context-based authentication, proper cleanup, realistic data + +### Frontend Testing +- Currently relies on TypeScript compilation and ESLint +- No dedicated test framework configured + +### Running Tests +```bash +# All tests +go test -v ./... + +# Specific packages +go test -v ./store/test/ +go test -v ./server/router/api/v1/test/ + +# With coverage +go test -cover ./... +``` + +## Development Modes + +### Production Mode +```bash +go run ./bin/memos/main.go --mode prod --port 5230 +``` +- Uses workspace-generated secret key +- Serves built frontend from `/server/router/frontend/dist/` +- Optimized for deployment + +### Development Mode +```bash +go run ./bin/memos/main.go --mode dev --port 8081 +``` +- Fixed secret key "usememos" +- Enables debugging features +- Separate frontend development server recommended + +### Demo Mode +```bash +go run ./bin/memos/main.go --mode demo +``` +- Specialized configuration for demonstration purposes + +## Key Configuration + +### Environment Variables +All CLI flags can be set via environment variables with `MEMOS_` prefix: +- `MEMOS_MODE` - Server mode (dev/prod/demo) +- `MEMOS_PORT` - Server port +- `MEMOS_DATA` - Data directory +- `MEMOS_DRIVER` - Database driver +- `MEMOS_DSN` - Database connection string +- `MEMOS_INSTANCE_URL` - Public instance URL + +### Runtime Configuration +- **Profile**: `/internal/profile/` handles configuration validation +- **Secrets**: Auto-generated workspace secret in production +- **Data Directory**: Configurable storage location for SQLite and assets + +## Frontend Technology Stack + +### Core Framework +- **React 18** with TypeScript +- **Vite** for build tooling and development server +- **React Router** for navigation +- **MobX** for state management + +### UI Components +- **Radix UI** primitives for accessibility +- **Tailwind CSS** for styling with custom themes +- **Lucide React** for icons +- **shadcn/ui** component patterns + +### Key Libraries +- **dayjs** for date manipulation +- **highlight.js** for code syntax highlighting +- **katex** for math rendering +- **mermaid** for diagram rendering +- **react-leaflet** for maps +- **i18next** for internationalization + +## Protocol Buffer Workflow + +### Code Generation +- **Source**: `/proto/api/v1/*.proto` and `/proto/store/*.proto` +- **Generated**: `/proto/gen/` for Go, `/web/src/types/proto/` for TypeScript +- **Build Tool**: Buf for protobuf compilation +- **API Docs**: Generated swagger at `/proto/gen/apidocs.swagger.yaml` + +### API Design +- gRPC services in `/proto/api/v1/` +- Resource-oriented design (User, Memo, Attachment, etc.) +- REST gateway auto-generated from protobuf annotations + +## File Organization Principles + +### Backend +- **Domain-driven**: Each entity (user, memo, attachment) has dedicated files +- **Layered**: Clear separation between API, business logic, and data layers +- **Database-agnostic**: Common interfaces with driver-specific implementations + +### Frontend +- **Component-based**: Reusable components in `/components/` +- **Feature-based**: Related functionality grouped together +- **Type-safe**: Strong TypeScript integration with generated protobuf types + +## Common Development Workflows + +### Adding New API Endpoint +1. Define service method in `/proto/api/v1/{service}.proto` +2. Generate code: `buf generate` +3. Implement service method in `/server/router/api/v1/{service}_service.go` +4. Add any required store methods in `/store/{entity}.go` +5. Update database layer if needed in `/store/db/{driver}/{entity}.go` + +### Database Schema Changes +1. Create migration file in `/store/migration/{driver}/{version}/` +2. Update store interface in `/store/{entity}.go` +3. Implement in each database driver +4. Update protobuf if external API changes needed + +### Frontend Component Development +1. Create component in `/web/src/components/` +2. Follow existing patterns for styling and state management +3. Use TypeScript for type safety +4. Import and use in pages or other components \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..01b8d66 --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,2 @@ +# These owners will be the default owners for everything in the repo. +* @boojack diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d59ac17 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,81 @@ +# Multi-stage Dockerfile for Memos +# Stage 1: Build the frontend +FROM node:22-alpine AS frontend-builder + +WORKDIR /app/web + +# Install pnpm +RUN npm install -g pnpm + +# Copy package files +COPY web/package*.json web/pnpm-lock.yaml ./ + +# Install dependencies +RUN pnpm install --frozen-lockfile + +# Copy frontend source +COPY web/ . + +# Build the frontend +RUN pnpm build + +# Stage 2: Build the backend +FROM golang:1.24-alpine AS backend-builder + +WORKDIR /app + +# Install build dependencies +RUN apk add --no-cache git + +# Copy go mod files +COPY go.mod go.sum ./ + +# Download dependencies +RUN go mod download + +# Copy source code +COPY . . + +# Copy built frontend from previous stage to the correct embedded location +COPY --from=frontend-builder /app/web/dist ./server/router/frontend/dist + +# Build the application (frontend files are now embedded) +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o memos ./bin/memos/main.go + +# Stage 3: Final runtime image +FROM alpine:latest + +# Install runtime dependencies +RUN apk --no-cache add ca-certificates tzdata wget + +# Create non-root user +RUN addgroup -g 1000 memos && \ + adduser -D -s /bin/sh -u 1000 -G memos memos + +# Set working directory +WORKDIR /usr/local/memos + +# Copy binary from builder +COPY --from=backend-builder /app/memos . + +# Create data directory +RUN mkdir -p /var/opt/memos && \ + chown -R memos:memos /var/opt/memos /usr/local/memos + +# Switch to non-root user +USER memos + +# Expose port (default port from main.go) +EXPOSE 8081 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8081/api/ping || exit 1 + +# Set environment variables +ENV MEMOS_MODE=prod +ENV MEMOS_PORT=8081 +ENV MEMOS_DATA=/var/opt/memos + +# Run the application +CMD ["./memos"] \ No newline at end of file diff --git a/LICENSE b/LICENSE index 91258f6..ad324ee 100644 --- a/LICENSE +++ b/LICENSE @@ -1,232 +1,21 @@ -GNU GENERAL PUBLIC LICENSE -Version 3, 29 June 2007 - -Copyright © 2007 Free Software Foundation, Inc. - -Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. - -Preamble - -The GNU General Public License is a free, copyleft license for software and other kinds of works. - -The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. - -When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. - -To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. - -For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. - -Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. - -For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. - -Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. - -Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. - -The precise terms and conditions for copying, distribution and modification follow. - -TERMS AND CONDITIONS - -0. Definitions. - -“This License” refers to version 3 of the GNU General Public License. - -“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. - -“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations. - -To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work. - -A “covered work” means either the unmodified Program or a work based on the Program. - -To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. - -To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. - -An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. - -1. Source Code. -The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work. - -A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. - -The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. - -The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. - -The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. - -The Corresponding Source for a work in source code form is that same work. - -2. Basic Permissions. -All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. - -You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. - -Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. - -3. Protecting Users' Legal Rights From Anti-Circumvention Law. -No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. - -When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. - -4. Conveying Verbatim Copies. -You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. - -You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. - -5. Conveying Modified Source Versions. -You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”. - - c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. - -A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. - -6. Conveying Non-Source Forms. -You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: - - a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. - - d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. - -A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. - -A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. - -“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. - -If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). - -The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. - -Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. - -7. Additional Terms. -“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. - -When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. - -Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or authors of the material; or - - e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. - -All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. - -If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. - -Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. - -8. Termination. -You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). - -However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. - -Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. - -Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. - -9. Acceptance Not Required for Having Copies. -You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. - -10. Automatic Licensing of Downstream Recipients. -Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. - -An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. - -You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. - -11. Patents. -A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”. - -A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. - -Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. - -In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. - -If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. - -If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. - -A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. - -Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. - -12. No Surrender of Others' Freedom. -If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. - -13. Use with the GNU Affero General Public License. -Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. - -14. Revised Versions of this License. -The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. - -Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. - -If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. - -Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. - -15. Disclaimer of Warranty. -THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - -16. Limitation of Liability. -IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. - -17. Interpretation of Sections 15 and 16. -If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. - -END OF TERMS AND CONDITIONS - -How to Apply These Terms to Your New Programs - -If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. - -To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found. - - memos - Copyright (C) 2025 TBNilles - - This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. - - This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. - - You should have received a copy of the GNU General Public License along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - -If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: - - memos Copyright (C) 2025 TBNilles - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”. - -You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . - -The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . +MIT License + +Copyright (c) 2025 Memos + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index f53b827..175d6f7 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,223 @@ -# memos +# Memos -A fork from memos \ No newline at end of file +Memos + +A modern, open-source, self-hosted knowledge management and note-taking platform designed for privacy-conscious users and organizations. Memos provides a lightweight yet powerful solution for capturing, organizing, and sharing thoughts with comprehensive Markdown support and cross-platform accessibility. + +
+ +[![Home Page](https://img.shields.io/badge/Home-www.usememos.com-blue)](https://www.usememos.com) +[![Documentation](https://img.shields.io/badge/Docs-Available-green)](https://www.usememos.com/docs) +[![Live Demo](https://img.shields.io/badge/Demo-Try%20Now-orange)](https://demo.usememos.com/) +[![Blog](https://img.shields.io/badge/Blog-Read%20More-lightblue)](https://www.usememos.com/blog) + +[![Docker Pulls](https://img.shields.io/docker/pulls/neosmemo/memos.svg)](https://hub.docker.com/r/neosmemo/memos) +[![Docker Image Size](https://img.shields.io/docker/image-size/neosmemo/memos?sort=semver)](https://hub.docker.com/r/neosmemo/memos) +[![Discord](https://img.shields.io/badge/discord-chat-5865f2?logo=discord&logoColor=f5f5f5)](https://discord.gg/tfPJa4UmAv) + +
+ +![Memos Application Screenshot](https://www.usememos.com/demo.png) + +## Table of Contents + +- [Overview](#overview) +- [Key Features](#key-features) +- [Quick Start](#quick-start) +- [Installation Methods](#installation-methods) +- [Development Setup](#development-setup) +- [Contributing](#contributing) +- [License](#license) + +## Overview + +Memos is a lightweight, self-hosted alternative to cloud-based note-taking services. Built with privacy and performance in mind, it offers a comprehensive platform for personal knowledge management without compromising data ownership or security. + +## Key Features + +### Data Privacy and Security + +- **Complete Data Ownership**: All application data is stored locally in your chosen database +- **Self-Hosted Architecture**: Full control over your data infrastructure and access policies +- **No External Dependencies**: Runtime operations require no third-party services or cloud connections + +### Content Creation and Management + +- **Plain Text Efficiency**: Streamlined text input with immediate save functionality +- **Advanced Markdown Support**: Comprehensive Markdown rendering with syntax highlighting +- **Rich Media Integration**: Support for images, links, and embedded content + +### Technical Excellence + +- **High-Performance Backend**: Built with Go for optimal resource utilization and scalability +- **Modern Frontend**: React.js-based user interface with responsive design +- **Lightweight Deployment**: Minimal system requirements with efficient resource consumption +- **Cross-Platform Compatibility**: Supports Linux, macOS, Windows, and containerized environments + +### Customization and Extensibility + +- **Configurable Interface**: Customizable server branding, themes, and user interface elements +- **API-First Design**: RESTful API with comprehensive documentation for third-party integrations +- **Multi-Database Support**: Compatible with SQLite, PostgreSQL, and MySQL databases + +### Cost-Effective Solution + +- **Open Source License**: MIT licensed with full source code availability +- **Zero Licensing Costs**: No subscription fees, usage limits, or premium tiers +- **Community-Driven Development**: Active community contribution and transparent development process + +## Quick Start + +### Prerequisites + +- Docker or Docker Compose installed on your system +- Minimum 512MB RAM and 1GB available disk space + +### Docker Deployment + +Deploy Memos in production mode using Docker: + +```bash +# Create data directory +mkdir -p ~/.memos + +# Run Memos container +docker run -d \ + --name memos \ + --restart unless-stopped \ + -p 5230:5230 \ + -v ~/.memos:/var/opt/memos \ + neosmemo/memos:stable +``` + +Access the application at `http://localhost:5230` and complete the initial setup process. + +### Docker Compose Deployment + +For advanced configurations, use Docker Compose: + +```yaml +# docker-compose.yml +version: "3.8" +services: + memos: + image: neosmemo/memos:stable + container_name: memos + restart: unless-stopped + ports: + - "5230:5230" + volumes: + - ./data:/var/opt/memos + environment: + - MEMOS_MODE=prod + - MEMOS_PORT=5230 +``` + +Deploy with: + +```bash +docker-compose up -d +``` + +> **Note**: The data directory (`~/.memos/` or `./data/`) stores all application data including the database, uploaded files, and configuration. Ensure this directory is included in your backup strategy. +> +> **Platform Compatibility**: The above commands are optimized for Unix-like systems (Linux, macOS). For Windows deployments, please refer to the [Windows-specific documentation](https://www.usememos.com/docs/install/container-install#docker-on-windows). + +## Installation Methods + +Memos supports multiple installation approaches to accommodate different deployment scenarios: + +### Container Deployment + +- **Docker Hub**: Official images available at `neosmemo/memos` +- **GitHub Container Registry**: Alternative registry with the same image versions +- **Kubernetes**: Helm charts and YAML manifests for cluster deployments + +### Binary Installation + +- **Pre-compiled Binaries**: Available for Linux, macOS, and Windows on the releases page + +### Source Installation + +- **Go Build**: Compile from source using Go 1.24 or later +- **Development Mode**: Local development setup with hot reloading + +For detailed installation instructions, refer to the [comprehensive installation guide](https://www.usememos.com/docs/install). + +## Development Setup + +### Prerequisites + +- Go 1.24 or later +- Node.js 22+ and pnpm +- Git for version control + +### Backend Development + +```bash +# Clone the repository +git clone https://github.com/usememos/memos.git +cd memos + +# Install Go dependencies +go mod download + +# Run the backend server +go run ./bin/memos/main.go --mode dev --port 8081 +``` + +### Frontend Development + +```bash +# Navigate to web directory +cd web + +# Install dependencies +pnpm install + +# Start development server +pnpm dev +``` + +The development servers will be available at: + +- Backend API: `http://localhost:8081` +- Frontend: `http://localhost:3001` + +## Contributing + +Memos is an open-source project that welcomes contributions from developers, designers, and users worldwide. We maintain a collaborative and inclusive development environment that values quality, innovation, and community feedback. + +### Ways to Contribute + +- **Code Contributions**: Bug fixes, feature implementations, and performance improvements +- **Documentation**: API documentation, user guides, and technical specifications +- **Testing**: Quality assurance, test case development, and bug reporting +- **Localization**: Translation support for multiple languages and regions +- **Community Support**: Helping users on Discord, GitHub discussions, and forums + +## License + +Memos is released under the MIT License, providing maximum flexibility for both personal and commercial use. This license allows for: + +- **Commercial Use**: Deploy Memos in commercial environments without licensing fees +- **Modification**: Adapt and customize the codebase for specific requirements +- **Distribution**: Share modified versions while maintaining license attribution +- **Private Use**: Use Memos internally without disclosure requirements + +See the [LICENSE](./LICENSE) file for complete licensing terms. + +## Project Status + +> **Development Status**: Memos is actively maintained and under continuous development. While the core functionality is stable and production-ready, users should expect regular updates, feature additions, and potential breaking changes as the project evolves. +> +> **Version Compatibility**: We maintain backward compatibility for data storage and API interfaces where possible. Migration guides are provided for major version transitions. + +## Support and Community + +- **Documentation**: [Official Documentation](https://www.usememos.com/docs) +- **Community Chat**: [Discord Server](https://discord.gg/tfPJa4UmAv) +- **Issue Tracking**: [GitHub Issues](https://github.com/usememos/memos/issues) +- **Discussions**: [GitHub Discussions](https://github.com/usememos/memos/discussions) + +[![Star History Chart](https://api.star-history.com/svg?repos=usememos/memos&type=Date)](https://star-history.com/#usememos/memos&Date) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..48ab17a --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,7 @@ +# Security Policy + +## Reporting a bug + +Report security bugs via GitHub [issues](https://github.com/usememos/memos/issues). + +For more information, please contact [usememos@gmail.com](usememos@gmail.com). diff --git a/bin/memos/main.go b/bin/memos/main.go new file mode 100644 index 0000000..e7f94ce --- /dev/null +++ b/bin/memos/main.go @@ -0,0 +1,187 @@ +package main + +import ( + "context" + "fmt" + "log/slog" + "net/http" + "os" + "os/signal" + "syscall" + + "github.com/spf13/cobra" + "github.com/spf13/viper" + + "github.com/usememos/memos/internal/profile" + "github.com/usememos/memos/internal/version" + "github.com/usememos/memos/server" + "github.com/usememos/memos/store" + "github.com/usememos/memos/store/db" +) + +const ( + greetingBanner = ` +███╗ ███╗███████╗███╗ ███╗ ██████╗ ███████╗ +████╗ ████║██╔════╝████╗ ████║██╔═══██╗██╔════╝ +██╔████╔██║█████╗ ██╔████╔██║██║ ██║███████╗ +██║╚██╔╝██║██╔══╝ ██║╚██╔╝██║██║ ██║╚════██║ +██║ ╚═╝ ██║███████╗██║ ╚═╝ ██║╚██████╔╝███████║ +╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ +` +) + +var ( + rootCmd = &cobra.Command{ + Use: "memos", + Short: `An open source, lightweight note-taking service. Easily capture and share your great thoughts.`, + Run: func(_ *cobra.Command, _ []string) { + instanceProfile := &profile.Profile{ + Mode: viper.GetString("mode"), + Addr: viper.GetString("addr"), + Port: viper.GetInt("port"), + UNIXSock: viper.GetString("unix-sock"), + Data: viper.GetString("data"), + Driver: viper.GetString("driver"), + DSN: viper.GetString("dsn"), + InstanceURL: viper.GetString("instance-url"), + Version: version.GetCurrentVersion(viper.GetString("mode")), + } + if err := instanceProfile.Validate(); err != nil { + panic(err) + } + + ctx, cancel := context.WithCancel(context.Background()) + dbDriver, err := db.NewDBDriver(instanceProfile) + if err != nil { + cancel() + slog.Error("failed to create db driver", "error", err) + return + } + + storeInstance := store.New(dbDriver, instanceProfile) + if err := storeInstance.Migrate(ctx); err != nil { + cancel() + slog.Error("failed to migrate", "error", err) + return + } + + s, err := server.NewServer(ctx, instanceProfile, storeInstance) + if err != nil { + cancel() + slog.Error("failed to create server", "error", err) + return + } + + c := make(chan os.Signal, 1) + // Trigger graceful shutdown on SIGINT or SIGTERM. + // The default signal sent by the `kill` command is SIGTERM, + // which is taken as the graceful shutdown signal for many systems, eg., Kubernetes, Gunicorn. + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + + if err := s.Start(ctx); err != nil { + if err != http.ErrServerClosed { + slog.Error("failed to start server", "error", err) + cancel() + } + } + + printGreetings(instanceProfile) + + go func() { + <-c + s.Shutdown(ctx) + cancel() + }() + + // Wait for CTRL-C. + <-ctx.Done() + }, + } +) + +func init() { + viper.SetDefault("mode", "dev") + viper.SetDefault("driver", "sqlite") + viper.SetDefault("port", 8081) + + rootCmd.PersistentFlags().String("mode", "dev", `mode of server, can be "prod" or "dev" or "demo"`) + rootCmd.PersistentFlags().String("addr", "", "address of server") + rootCmd.PersistentFlags().Int("port", 8081, "port of server") + rootCmd.PersistentFlags().String("unix-sock", "", "path to the unix socket, overrides --addr and --port") + rootCmd.PersistentFlags().String("data", "", "data directory") + rootCmd.PersistentFlags().String("driver", "sqlite", "database driver") + rootCmd.PersistentFlags().String("dsn", "", "database source name(aka. DSN)") + rootCmd.PersistentFlags().String("instance-url", "", "the url of your memos instance") + + if err := viper.BindPFlag("mode", rootCmd.PersistentFlags().Lookup("mode")); err != nil { + panic(err) + } + if err := viper.BindPFlag("addr", rootCmd.PersistentFlags().Lookup("addr")); err != nil { + panic(err) + } + if err := viper.BindPFlag("port", rootCmd.PersistentFlags().Lookup("port")); err != nil { + panic(err) + } + if err := viper.BindPFlag("unix-sock", rootCmd.PersistentFlags().Lookup("unix-sock")); err != nil { + panic(err) + } + if err := viper.BindPFlag("data", rootCmd.PersistentFlags().Lookup("data")); err != nil { + panic(err) + } + if err := viper.BindPFlag("driver", rootCmd.PersistentFlags().Lookup("driver")); err != nil { + panic(err) + } + if err := viper.BindPFlag("dsn", rootCmd.PersistentFlags().Lookup("dsn")); err != nil { + panic(err) + } + if err := viper.BindPFlag("instance-url", rootCmd.PersistentFlags().Lookup("instance-url")); err != nil { + panic(err) + } + + viper.SetEnvPrefix("memos") + viper.AutomaticEnv() + if err := viper.BindEnv("instance-url", "MEMOS_INSTANCE_URL"); err != nil { + panic(err) + } +} + +func printGreetings(profile *profile.Profile) { + if profile.IsDev() { + println("Development mode is enabled") + println("DSN: ", profile.DSN) + } + fmt.Printf(`--- +Server profile +version: %s +data: %s +addr: %s +port: %d +unix-sock: %s +mode: %s +driver: %s +--- +`, profile.Version, profile.Data, profile.Addr, profile.Port, profile.UNIXSock, profile.Mode, profile.Driver) + + print(greetingBanner) + if len(profile.UNIXSock) == 0 { + if len(profile.Addr) == 0 { + fmt.Printf("Version %s has been started on port %d\n", profile.Version, profile.Port) + } else { + fmt.Printf("Version %s has been started on address '%s' and port %d\n", profile.Version, profile.Addr, profile.Port) + } + } else { + fmt.Printf("Version %s has been started on unix socket %s\n", profile.Version, profile.UNIXSock) + } + fmt.Printf(`--- +See more in: +👉Website: %s +👉GitHub: %s +--- +`, "https://usememos.com", "https://github.com/usememos/memos") +} + +func main() { + if err := rootCmd.Execute(); err != nil { + panic(err) + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..edb463c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,61 @@ +version: '3.8' + +services: + # PostgreSQL Database + postgres: + image: postgres:16-alpine + container_name: memos-postgres + restart: unless-stopped + environment: + POSTGRES_DB: memos + POSTGRES_USER: memos + POSTGRES_PASSWORD: zcA3LEfW + PGDATA: /var/lib/postgresql/data/pgdata + volumes: + - postgres_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U memos -d memos"] + interval: 10s + timeout: 5s + retries: 5 + networks: + - memos-network + + # Memos Application + memos: + build: + context: . + dockerfile: Dockerfile + container_name: memos-app + restart: unless-stopped + ports: + - "8081:8081" + environment: + # Database configuration + MEMOS_DRIVER: postgres + MEMOS_DSN: "postgres://memos:zcA3LEfW@postgres:5432/memos?sslmode=disable" + + # Application configuration + MEMOS_MODE: prod + MEMOS_PORT: 8081 + MEMOS_DATA: /var/opt/memos + + # Optional: Set your instance URL + # MEMOS_INSTANCE_URL: "https://your-domain.com" + volumes: + - memos_data:/var/opt/memos + depends_on: + postgres: + condition: service_healthy + networks: + - memos-network + +volumes: + postgres_data: + driver: local + memos_data: + driver: local + +networks: + memos-network: + driver: bridge \ No newline at end of file diff --git a/export_import_messages.proto b/export_import_messages.proto new file mode 100644 index 0000000..8231999 --- /dev/null +++ b/export_import_messages.proto @@ -0,0 +1,107 @@ +// Add these message definitions to the end of memo_service.proto + +message ExportMemosRequest { + // Optional. Format for the export (currently only "json" is supported) + string format = 1 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Filter to apply to memos for export + // Uses the same filter format as ListMemosRequest + string filter = 2 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Whether to exclude archived memos from export + // Default: false (include archived memos) + bool exclude_archived = 3 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Whether to include attachments in the export + // Default: true + bool include_attachments = 4 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Whether to include memo relations in the export + // Default: true + bool include_relations = 5 [(google.api.field_behavior) = OPTIONAL]; +} + +message ExportMemosResponse { + // The exported data as bytes + bytes data = 1; + + // The format of the exported data + string format = 2; + + // Suggested filename for the export + string filename = 3; + + // Number of memos exported + int32 memo_count = 4; + + // Size of the export data in bytes + int64 size_bytes = 5; +} + +message ImportMemosRequest { + // Required. The data to import (JSON format) + bytes data = 1 [(google.api.field_behavior) = REQUIRED]; + + // Optional. Format of the import data (currently only "json" is supported) + string format = 2 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Whether to overwrite existing memos with the same UID + // Default: false (skip existing memos) + bool overwrite_existing = 3 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Whether to validate only (dry run mode) + // If true, the import will be validated but no data will be created + bool validate_only = 4 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Whether to preserve original timestamps + // Default: true + bool preserve_timestamps = 5 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Whether to skip importing attachments + // Default: false (import attachments if present) + bool skip_attachments = 6 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Whether to skip importing memo relations + // Default: false (import relations if present) + bool skip_relations = 7 [(google.api.field_behavior) = OPTIONAL]; +} + +message ImportMemosResponse { + // Number of memos successfully imported + int32 imported_count = 1; + + // Number of memos skipped (due to errors or existing UIDs) + int32 skipped_count = 2; + + // Number of memos that failed validation (in validate_only mode) + int32 validation_errors = 3; + + // List of error messages for failed imports + repeated string errors = 4; + + // List of warning messages for potential issues + repeated string warnings = 5; + + // Summary of the import operation + ImportSummary summary = 6; +} + +message ImportSummary { + // Total number of memos in the import data + int32 total_memos = 1; + + // Number of new memos created + int32 created_count = 2; + + // Number of existing memos updated + int32 updated_count = 3; + + // Number of attachments imported + int32 attachments_imported = 4; + + // Number of relations imported + int32 relations_imported = 5; + + // Import duration in milliseconds + int64 duration_ms = 6; +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..8b2c16e --- /dev/null +++ b/go.mod @@ -0,0 +1,98 @@ +module github.com/usememos/memos + +go 1.24 + +require ( + github.com/aws/aws-sdk-go-v2 v1.36.5 + github.com/aws/aws-sdk-go-v2/config v1.29.17 + github.com/aws/aws-sdk-go-v2/credentials v1.17.70 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.82 + github.com/aws/aws-sdk-go-v2/service/s3 v1.83.0 + github.com/go-sql-driver/mysql v1.9.3 + github.com/google/cel-go v0.25.0 + github.com/google/uuid v1.6.0 + github.com/gorilla/feeds v1.2.0 + github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 + github.com/improbable-eng/grpc-web v0.15.0 + github.com/joho/godotenv v1.5.1 + github.com/labstack/echo/v4 v4.13.4 + github.com/lib/pq v1.10.9 + github.com/lithammer/shortuuid/v4 v4.2.0 + github.com/pkg/errors v0.9.1 + github.com/spf13/cobra v1.9.1 + github.com/spf13/viper v1.20.1 + github.com/stretchr/testify v1.10.0 + github.com/usememos/gomark v0.0.0-20250328014447-c9fa41c01bc4 + golang.org/x/crypto v0.38.0 + golang.org/x/mod v0.25.0 + golang.org/x/net v0.40.0 + golang.org/x/oauth2 v0.30.0 + google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a + google.golang.org/grpc v1.72.2 + modernc.org/sqlite v1.37.1 +) + +require ( + cel.dev/expr v0.24.0 // indirect + filippo.io/edwards25519 v1.1.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.1 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/desertbit/timer v1.0.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-viper/mapstructure/v2 v2.2.1 // indirect + github.com/ncruces/go-strftime v0.1.9 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect + github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect + github.com/rogpeppe/go-internal v1.11.0 // indirect + github.com/rs/cors v1.11.1 // indirect + github.com/sagikazarmark/locafero v0.9.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.14.0 // indirect + github.com/spf13/cast v1.9.1 // indirect + github.com/stoewer/go-strcase v1.3.0 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b // indirect + golang.org/x/image v0.27.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a // indirect + modernc.org/libc v1.65.7 // indirect + modernc.org/mathutil v1.7.1 // indirect + modernc.org/memory v1.11.0 // indirect + nhooyr.io/websocket v1.8.17 // indirect +) + +require ( + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 // indirect + github.com/aws/smithy-go v1.22.4 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/disintegration/imaging v1.6.2 + github.com/golang-jwt/jwt/v5 v5.2.2 + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/labstack/gommon v0.4.2 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/soheilhy/cmux v0.1.5 + github.com/spf13/pflag v1.0.6 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.25.0 // indirect + golang.org/x/time v0.11.0 // indirect + google.golang.org/protobuf v1.36.6 + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2ec02c8 --- /dev/null +++ b/go.sum @@ -0,0 +1,711 @@ +cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= +cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= +github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= +github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= +github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= +github.com/aws/aws-sdk-go-v2 v1.36.5 h1:0OF9RiEMEdDdZEMqF9MRjevyxAQcf6gY+E7vwBILFj0= +github.com/aws/aws-sdk-go-v2 v1.36.5/go.mod h1:EYrzvCCN9CMUTa5+6lf6MM4tq3Zjp8UhSGR/cBsjai0= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11 h1:12SpdwU8Djs+YGklkinSSlcrPyj3H4VifVsKf78KbwA= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.11/go.mod h1:dd+Lkp6YmMryke+qxW/VnKyhMBDTYP41Q2Bb+6gNZgY= +github.com/aws/aws-sdk-go-v2/config v1.29.17 h1:jSuiQ5jEe4SAMH6lLRMY9OVC+TqJLP5655pBGjmnjr0= +github.com/aws/aws-sdk-go-v2/config v1.29.17/go.mod h1:9P4wwACpbeXs9Pm9w1QTh6BwWwJjwYvJ1iCt5QbCXh8= +github.com/aws/aws-sdk-go-v2/credentials v1.17.70 h1:ONnH5CM16RTXRkS8Z1qg7/s2eDOhHhaXVd72mmyv4/0= +github.com/aws/aws-sdk-go-v2/credentials v1.17.70/go.mod h1:M+lWhhmomVGgtuPOhO85u4pEa3SmssPTdcYpP/5J/xc= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32 h1:KAXP9JSHO1vKGCr5f4O6WmlVKLFFXgWYAGoJosorxzU= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.32/go.mod h1:h4Sg6FQdexC1yYG9RDnOvLbW1a/P986++/Y/a+GyEM8= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.82 h1:EO13QJTCD1Ig2IrQnoHTRrn981H9mB7afXsZ89WptI4= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.17.82/go.mod h1:AGh1NCg0SH+uyJamiJA5tTQcql4MMRDXGRdMmCxCXzY= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36 h1:SsytQyTMHMDPspp+spo7XwXTP44aJZZAC7fBV2C5+5s= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.36/go.mod h1:Q1lnJArKRXkenyog6+Y+zr7WDpk4e6XlR6gs20bbeNo= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36 h1:i2vNHQiXUvKhs3quBR6aqlgJaiaexz/aNvdCktW/kAM= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.36/go.mod h1:UdyGa7Q91id/sdyHPwth+043HhmP6yP9MBHgbZM0xo8= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3 h1:bIqFDwgGXXN1Kpp99pDOdKMTTb5d2KyU5X/BZxjOkRo= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.3/go.mod h1:H5O/EsxDWyU+LP/V8i5sm8cxoZgc2fdNR9bxlOFrQTo= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36 h1:GMYy2EOWfzdP3wfVAGXBNKY5vK4K8vMET4sYOYltmqs= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.36/go.mod h1:gDhdAV6wL3PmPqBhiPbnlS447GoWs8HTTOYef9/9Inw= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4 h1:CXV68E2dNqhuynZJPB80bhPQwAKqBWVer887figW6Jc= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.4/go.mod h1:/xFi9KtvBXP97ppCz1TAEvU1Uf66qvid89rbem3wCzQ= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4 h1:nAP2GYbfh8dd2zGZqFRSMlq+/F6cMPBUuCsGAMkN074= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.4/go.mod h1:LT10DsiGjLWh4GbjInf9LQejkYEhBgBCjLG5+lvk4EE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17 h1:t0E6FzREdtCsiLIoLCWsYliNsRBgyGD/MCK571qk4MI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.17/go.mod h1:ygpklyoaypuyDvOM5ujWGrYWpAK3h7ugnmKCU/76Ys4= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17 h1:qcLWgdhq45sDM9na4cvXax9dyLitn8EYBRl8Ak4XtG4= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.17/go.mod h1:M+jkjBFZ2J6DJrjMv2+vkBbuht6kxJYtJiwoVgX4p4U= +github.com/aws/aws-sdk-go-v2/service/s3 v1.83.0 h1:5Y75q0RPQoAbieyOuGLhjV9P3txvYgXv2lg0UwJOfmE= +github.com/aws/aws-sdk-go-v2/service/s3 v1.83.0/go.mod h1:kUklwasNoCn5YpyAqC/97r6dzTA1SRKJfKq16SXeoDU= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.5 h1:AIRJ3lfb2w/1/8wOOSqYb9fUKGwQbtysJ2H1MofRUPg= +github.com/aws/aws-sdk-go-v2/service/sso v1.25.5/go.mod h1:b7SiVprpU+iGazDUqvRSLf5XmCdn+JtT1on7uNL6Ipc= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3 h1:BpOxT3yhLwSJ77qIY3DoHAQjZsc4HEGfMCE4NGy3uFg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.30.3/go.mod h1:vq/GQR1gOFLquZMSrxUK/cpvKCNVYibNyJ1m7JrU88E= +github.com/aws/aws-sdk-go-v2/service/sts v1.34.0 h1:NFOJ/NXEGV4Rq//71Hs1jC/NvPs1ezajK+yQmkwnPV0= +github.com/aws/aws-sdk-go-v2/service/sts v1.34.0/go.mod h1:7ph2tGpfQvwzgistp2+zga9f+bCjlQJPkPUmMgDSD7w= +github.com/aws/smithy-go v1.22.4 h1:uqXzVZNuNexwc/xrh6Tb56u89WDlJY6HS+KC0S4QSjw= +github.com/aws/smithy-go v1.22.4/go.mod h1:t1ufH5HMublsJYulve2RKmHDC15xu1f26kHCp/HgceI= +github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE= +github.com/desertbit/timer v1.0.1 h1:yRpYNn5Vaaj6QXecdLMPMJsW81JLiI1eokUft5nBmeo= +github.com/desertbit/timer v1.0.1/go.mod h1:htRrYeY5V/t4iu1xCJ5XsQvp4xve8QulXXctAzxqcwE= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c= +github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= +github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= +github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= +github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss= +github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/cel-go v0.25.0 h1:jsFw9Fhn+3y2kBbltZR4VEz5xKkcIFRPDnuEzAGv5GY= +github.com/google/cel-go v0.25.0/go.mod h1:hjEb6r5SuOSlhCHmFoLzu8HGCERvIsDAbxDAyNU/MmI= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs= +github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/feeds v1.2.0 h1:O6pBiXJ5JHhPvqy53NsjKOThq+dNFm8+DFrxBEdzSCc= +github.com/gorilla/feeds v1.2.0/go.mod h1:WMib8uJP3BbY+X8Szd1rA5Pzhdfh+HCCAYT2z7Fza6Y= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= +github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= +github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= +github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= +github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= +github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= +github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= +github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= +github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ= +github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo/v4 v4.13.4 h1:oTZZW+T3s9gAu5L8vmzihV7/lkXGZuITzTQkTEhcXEA= +github.com/labstack/echo/v4 v4.13.4/go.mod h1:g63b33BZ5vZzcIUF8AtRH40DrTlXnx4UMC8rBdndmjQ= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= +github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/lithammer/shortuuid/v4 v4.2.0 h1:LMFOzVB3996a7b8aBuEXxqOBflbfPQAiVzkIcHO0h8c= +github.com/lithammer/shortuuid/v4 v4.2.0/go.mod h1:D5noHZ2oFw/YaKCfGy0YxyE7M0wMbezmMjPdhyEFe6Y= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= +github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/grpc-proxy v0.0.0-20181017164139-0f1106ef9c76/go.mod h1:x5OoJHDHqxHS801UIuhqGl6QdSAEJvtausosHSdazIo= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= +github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= +github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= +github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA= +github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k= +github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= +github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= +github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= +github.com/spf13/cast v1.9.1 h1:LxnjuHJpEjFUlBvGDef9duW2jIfjfm9a2f4tCgyvsEw= +github.com/spf13/cast v1.9.1/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4= +github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4= +github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= +github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/usememos/gomark v0.0.0-20250328014447-c9fa41c01bc4 h1:WUVmhqDHt+5nhHGnsdfZ8no8zdwhKLPQ5AT/IP57egI= +github.com/usememos/gomark v0.0.0-20250328014447-c9fa41c01bc4/go.mod h1:7CZRoYFQyyljzplOTeyODFR26O+wr0BbnpTWVLGfKJA= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= +go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= +go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ= +go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE= +go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A= +go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU= +go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk= +go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w= +go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= +go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= +go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8= +golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= +golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b h1:QoALfVG9rhQ/M7vYDScfPdWjGL9dlsVVM5VGh7aKoAA= +golang.org/x/exp v0.0.0-20250531010427-b6e5de432a8b/go.mod h1:U6Lno4MTRCDY+Ba7aCcauB9T60gsv5s4ralQzP72ZoQ= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w= +golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= +golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= +golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ= +golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= +golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= +golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a h1:SGktgSolFCo75dnHJF2yMvnns6jCmHFJ0vE4Vn2JKvQ= +google.golang.org/genproto/googleapis/api v0.0.0-20250528174236-200df99c418a/go.mod h1:a77HrdMjoeKbnd2jmgcWdaS++ZLZAEq3orIOAEIKiVw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a h1:v2PbRU4K3llS09c7zodFpNePeamkAwG3mPrAery9VeE= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250528174236-200df99c418a/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= +google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.72.2 h1:TdbGzwb82ty4OusHWepvFWGLgIbNo1/SUynEN0ssqv8= +google.golang.org/grpc v1.72.2/go.mod h1:wH5Aktxcg25y1I3w7H69nHfXdOG3UiadoBtjh3izSDM= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= +gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s= +modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= +modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU= +modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE= +modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8= +modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= +modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= +modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= +modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00= +modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU= +modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= +modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= +modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI= +modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= +modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= +modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= +modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= +modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= +modernc.org/sqlite v1.37.1 h1:EgHJK/FPoqC+q2YBXg7fUmES37pCHFc97sI7zSayBEs= +modernc.org/sqlite v1.37.1/go.mod h1:XwdRtsE1MpiBcL54+MbKcaDvcuej+IYSMfLN6gSKV8g= +modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= +modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= +modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= +modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= +nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= +nhooyr.io/websocket v1.8.17 h1:KEVeLJkUywCKVsnLIDlD/5gtayKp8VoCkksHCGGfT9Y= +nhooyr.io/websocket v1.8.17/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/internal/base/resource_name.go b/internal/base/resource_name.go new file mode 100644 index 0000000..828bfa3 --- /dev/null +++ b/internal/base/resource_name.go @@ -0,0 +1,7 @@ +package base + +import "regexp" + +var ( + UIDMatcher = regexp.MustCompile("^[a-zA-Z0-9]([a-zA-Z0-9-]{0,30}[a-zA-Z0-9])?$") +) diff --git a/internal/base/resource_name_test.go b/internal/base/resource_name_test.go new file mode 100644 index 0000000..cef922a --- /dev/null +++ b/internal/base/resource_name_test.go @@ -0,0 +1,35 @@ +package base + +import ( + "testing" +) + +func TestUIDMatcher(t *testing.T) { + tests := []struct { + input string + expected bool + }{ + {"", false}, + {"-abc123", false}, + {"012345678901234567890123456789", true}, + {"1abc-123", true}, + {"A123B456C789", true}, + {"a", true}, + {"ab", true}, + {"a*b&c", false}, + {"a--b", true}, + {"a-1b-2c", true}, + {"a1234567890123456789012345678901", true}, + {"abc123", true}, + {"abc123-", false}, + } + + for _, test := range tests { + t.Run(test.input, func(*testing.T) { + result := UIDMatcher.MatchString(test.input) + if result != test.expected { + t.Errorf("For input '%s', expected %v but got %v", test.input, test.expected, result) + } + }) + } +} diff --git a/internal/profile/profile.go b/internal/profile/profile.go new file mode 100644 index 0000000..8d551d6 --- /dev/null +++ b/internal/profile/profile.go @@ -0,0 +1,92 @@ +package profile + +import ( + "fmt" + "log/slog" + "os" + "path/filepath" + "runtime" + "strings" + + "github.com/pkg/errors" +) + +// Profile is the configuration to start main server. +type Profile struct { + // Mode can be "prod" or "dev" or "demo" + Mode string + // Addr is the binding address for server + Addr string + // Port is the binding port for server + Port int + // UNIXSock is the IPC binding path. Overrides Addr and Port + UNIXSock string + // Data is the data directory + Data string + // DSN points to where memos stores its own data + DSN string + // Driver is the database driver + // sqlite, mysql + Driver string + // Version is the current version of server + Version string + // InstanceURL is the url of your memos instance. + InstanceURL string +} + +func (p *Profile) IsDev() bool { + return p.Mode != "prod" +} + +func checkDataDir(dataDir string) (string, error) { + // Convert to absolute path if relative path is supplied. + if !filepath.IsAbs(dataDir) { + relativeDir := filepath.Join(filepath.Dir(os.Args[0]), dataDir) + absDir, err := filepath.Abs(relativeDir) + if err != nil { + return "", err + } + dataDir = absDir + } + + // Trim trailing \ or / in case user supplies + dataDir = strings.TrimRight(dataDir, "\\/") + if _, err := os.Stat(dataDir); err != nil { + return "", errors.Wrapf(err, "unable to access data folder %s", dataDir) + } + return dataDir, nil +} + +func (p *Profile) Validate() error { + if p.Mode != "demo" && p.Mode != "dev" && p.Mode != "prod" { + p.Mode = "demo" + } + + if p.Mode == "prod" && p.Data == "" { + if runtime.GOOS == "windows" { + p.Data = filepath.Join(os.Getenv("ProgramData"), "memos") + if _, err := os.Stat(p.Data); os.IsNotExist(err) { + if err := os.MkdirAll(p.Data, 0770); err != nil { + slog.Error("failed to create data directory", slog.String("data", p.Data), slog.String("error", err.Error())) + return err + } + } + } else { + p.Data = "/var/opt/memos" + } + } + + dataDir, err := checkDataDir(p.Data) + if err != nil { + slog.Error("failed to check dsn", slog.String("data", dataDir), slog.String("error", err.Error())) + return err + } + + p.Data = dataDir + if p.Driver == "sqlite" && p.DSN == "" { + dbFile := fmt.Sprintf("memos_%s.db", p.Mode) + p.DSN = filepath.Join(dataDir, dbFile) + } + + return nil +} diff --git a/internal/util/util.go b/internal/util/util.go new file mode 100644 index 0000000..6a2e84c --- /dev/null +++ b/internal/util/util.go @@ -0,0 +1,73 @@ +package util + +import ( + "crypto/rand" + "math/big" + "net/mail" + "strconv" + "strings" + + "github.com/google/uuid" +) + +// ConvertStringToInt32 converts a string to int32. +func ConvertStringToInt32(src string) (int32, error) { + parsed, err := strconv.ParseInt(src, 10, 32) + if err != nil { + return 0, err + } + return int32(parsed), nil +} + +// HasPrefixes returns true if the string s has any of the given prefixes. +func HasPrefixes(src string, prefixes ...string) bool { + for _, prefix := range prefixes { + if strings.HasPrefix(src, prefix) { + return true + } + } + return false +} + +// ValidateEmail validates the email. +func ValidateEmail(email string) bool { + if _, err := mail.ParseAddress(email); err != nil { + return false + } + return true +} + +func GenUUID() string { + return uuid.New().String() +} + +var letters = []rune("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") + +// RandomString returns a random string with length n. +func RandomString(n int) (string, error) { + var sb strings.Builder + sb.Grow(n) + for i := 0; i < n; i++ { + // The reason for using crypto/rand instead of math/rand is that + // the former relies on hardware to generate random numbers and + // thus has a stronger source of random numbers. + randNum, err := rand.Int(rand.Reader, big.NewInt(int64(len(letters)))) + if err != nil { + return "", err + } + if _, err := sb.WriteRune(letters[randNum.Uint64()]); err != nil { + return "", err + } + } + return sb.String(), nil +} + +// ReplaceString replaces all occurrences of old in slice with new. +func ReplaceString(slice []string, old, new string) []string { + for i, s := range slice { + if s == old { + slice[i] = new + } + } + return slice +} diff --git a/internal/util/util_test.go b/internal/util/util_test.go new file mode 100644 index 0000000..20b2b8a --- /dev/null +++ b/internal/util/util_test.go @@ -0,0 +1,31 @@ +package util + +import ( + "testing" +) + +func TestValidateEmail(t *testing.T) { + tests := []struct { + email string + want bool + }{ + { + email: "t@gmail.com", + want: true, + }, + { + email: "@usememos.com", + want: false, + }, + { + email: "1@gmail", + want: true, + }, + } + for _, test := range tests { + result := ValidateEmail(test.email) + if result != test.want { + t.Errorf("Validate Email %s: got result %v, want %v.", test.email, result, test.want) + } + } +} diff --git a/internal/version/version.go b/internal/version/version.go new file mode 100644 index 0000000..fc6b8c2 --- /dev/null +++ b/internal/version/version.go @@ -0,0 +1,56 @@ +package version + +import ( + "fmt" + "strings" + + "golang.org/x/mod/semver" +) + +// Version is the service current released version. +// Semantic versioning: https://semver.org/ +var Version = "0.25.0" + +// DevVersion is the service current development version. +var DevVersion = "0.25.0" + +func GetCurrentVersion(mode string) string { + if mode == "dev" || mode == "demo" { + return DevVersion + } + return Version +} + +func GetMinorVersion(version string) string { + versionList := strings.Split(version, ".") + if len(versionList) < 3 { + return "" + } + return versionList[0] + "." + versionList[1] +} + +// IsVersionGreaterOrEqualThan returns true if version is greater than or equal to target. +func IsVersionGreaterOrEqualThan(version, target string) bool { + return semver.Compare(fmt.Sprintf("v%s", version), fmt.Sprintf("v%s", target)) > -1 +} + +// IsVersionGreaterThan returns true if version is greater than target. +func IsVersionGreaterThan(version, target string) bool { + return semver.Compare(fmt.Sprintf("v%s", version), fmt.Sprintf("v%s", target)) > 0 +} + +type SortVersion []string + +func (s SortVersion) Len() int { + return len(s) +} + +func (s SortVersion) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s SortVersion) Less(i, j int) bool { + v1 := fmt.Sprintf("v%s", s[i]) + v2 := fmt.Sprintf("v%s", s[j]) + return semver.Compare(v1, v2) == -1 +} diff --git a/internal/version/version_test.go b/internal/version/version_test.go new file mode 100644 index 0000000..ce1fde8 --- /dev/null +++ b/internal/version/version_test.go @@ -0,0 +1,103 @@ +package version + +import ( + "sort" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsVersionGreaterOrEqualThan(t *testing.T) { + tests := []struct { + version string + target string + want bool + }{ + { + version: "0.9.1", + target: "0.9.1", + want: true, + }, + { + version: "0.10.0", + target: "0.9.1", + want: true, + }, + { + version: "0.9.0", + target: "0.9.1", + want: false, + }, + } + for _, test := range tests { + result := IsVersionGreaterOrEqualThan(test.version, test.target) + if result != test.want { + t.Errorf("got result %v, want %v.", result, test.want) + } + } +} + +func TestIsVersionGreaterThan(t *testing.T) { + tests := []struct { + version string + target string + want bool + }{ + { + version: "0.9.1", + target: "0.9.1", + want: false, + }, + { + version: "0.10.0", + target: "0.8.0", + want: true, + }, + { + version: "0.23", + target: "0.22", + want: true, + }, + { + version: "0.8.0", + target: "0.10.0", + want: false, + }, + { + version: "0.9.0", + target: "0.9.1", + want: false, + }, + { + version: "0.22", + target: "0.22", + want: false, + }, + } + for _, test := range tests { + result := IsVersionGreaterThan(test.version, test.target) + if result != test.want { + t.Errorf("got result %v, want %v.", result, test.want) + } + } +} + +func TestSortVersion(t *testing.T) { + tests := []struct { + versionList []string + want []string + }{ + { + versionList: []string{"0.9.1", "0.10.0", "0.8.0"}, + want: []string{"0.8.0", "0.9.1", "0.10.0"}, + }, + { + versionList: []string{"1.9.1", "0.9.1", "0.10.0", "0.8.0"}, + want: []string{"0.8.0", "0.9.1", "0.10.0", "1.9.1"}, + }, + } + for _, test := range tests { + sort.Sort(SortVersion(test.versionList)) + assert.Equal(t, test.versionList, test.want) + } +} diff --git a/plugin/cron/README.md b/plugin/cron/README.md new file mode 100644 index 0000000..148a0f1 --- /dev/null +++ b/plugin/cron/README.md @@ -0,0 +1 @@ +Fork from https://github.com/robfig/cron diff --git a/plugin/cron/chain.go b/plugin/cron/chain.go new file mode 100644 index 0000000..82b387a --- /dev/null +++ b/plugin/cron/chain.go @@ -0,0 +1,96 @@ +package cron + +import ( + "errors" + "fmt" + "runtime" + "sync" + "time" +) + +// JobWrapper decorates the given Job with some behavior. +type JobWrapper func(Job) Job + +// Chain is a sequence of JobWrappers that decorates submitted jobs with +// cross-cutting behaviors like logging or synchronization. +type Chain struct { + wrappers []JobWrapper +} + +// NewChain returns a Chain consisting of the given JobWrappers. +func NewChain(c ...JobWrapper) Chain { + return Chain{c} +} + +// Then decorates the given job with all JobWrappers in the chain. +// +// This: +// +// NewChain(m1, m2, m3).Then(job) +// +// is equivalent to: +// +// m1(m2(m3(job))) +func (c Chain) Then(j Job) Job { + for i := range c.wrappers { + j = c.wrappers[len(c.wrappers)-i-1](j) + } + return j +} + +// Recover panics in wrapped jobs and log them with the provided logger. +func Recover(logger Logger) JobWrapper { + return func(j Job) Job { + return FuncJob(func() { + defer func() { + if r := recover(); r != nil { + const size = 64 << 10 + buf := make([]byte, size) + buf = buf[:runtime.Stack(buf, false)] + err, ok := r.(error) + if !ok { + err = errors.New("panic: " + fmt.Sprint(r)) + } + logger.Error(err, "panic", "stack", "...\n"+string(buf)) + } + }() + j.Run() + }) + } +} + +// DelayIfStillRunning serializes jobs, delaying subsequent runs until the +// previous one is complete. Jobs running after a delay of more than a minute +// have the delay logged at Info. +func DelayIfStillRunning(logger Logger) JobWrapper { + return func(j Job) Job { + var mu sync.Mutex + return FuncJob(func() { + start := time.Now() + mu.Lock() + defer mu.Unlock() + if dur := time.Since(start); dur > time.Minute { + logger.Info("delay", "duration", dur) + } + j.Run() + }) + } +} + +// SkipIfStillRunning skips an invocation of the Job if a previous invocation is +// still running. It logs skips to the given logger at Info level. +func SkipIfStillRunning(logger Logger) JobWrapper { + return func(j Job) Job { + var ch = make(chan struct{}, 1) + ch <- struct{}{} + return FuncJob(func() { + select { + case v := <-ch: + defer func() { ch <- v }() + j.Run() + default: + logger.Info("skip") + } + }) + } +} diff --git a/plugin/cron/chain_test.go b/plugin/cron/chain_test.go new file mode 100644 index 0000000..55d99ad --- /dev/null +++ b/plugin/cron/chain_test.go @@ -0,0 +1,239 @@ +//nolint:all +package cron + +import ( + "io" + "log" + "reflect" + "sync" + "testing" + "time" +) + +func appendingJob(slice *[]int, value int) Job { + var m sync.Mutex + return FuncJob(func() { + m.Lock() + *slice = append(*slice, value) + m.Unlock() + }) +} + +func appendingWrapper(slice *[]int, value int) JobWrapper { + return func(j Job) Job { + return FuncJob(func() { + appendingJob(slice, value).Run() + j.Run() + }) + } +} + +func TestChain(t *testing.T) { + var nums []int + var ( + append1 = appendingWrapper(&nums, 1) + append2 = appendingWrapper(&nums, 2) + append3 = appendingWrapper(&nums, 3) + append4 = appendingJob(&nums, 4) + ) + NewChain(append1, append2, append3).Then(append4).Run() + if !reflect.DeepEqual(nums, []int{1, 2, 3, 4}) { + t.Error("unexpected order of calls:", nums) + } +} + +func TestChainRecover(t *testing.T) { + panickingJob := FuncJob(func() { + panic("panickingJob panics") + }) + + t.Run("panic exits job by default", func(*testing.T) { + defer func() { + if err := recover(); err == nil { + t.Errorf("panic expected, but none received") + } + }() + NewChain().Then(panickingJob). + Run() + }) + + t.Run("Recovering JobWrapper recovers", func(*testing.T) { + NewChain(Recover(PrintfLogger(log.New(io.Discard, "", 0)))). + Then(panickingJob). + Run() + }) + + t.Run("composed with the *IfStillRunning wrappers", func(*testing.T) { + NewChain(Recover(PrintfLogger(log.New(io.Discard, "", 0)))). + Then(panickingJob). + Run() + }) +} + +type countJob struct { + m sync.Mutex + started int + done int + delay time.Duration +} + +func (j *countJob) Run() { + j.m.Lock() + j.started++ + j.m.Unlock() + time.Sleep(j.delay) + j.m.Lock() + j.done++ + j.m.Unlock() +} + +func (j *countJob) Started() int { + defer j.m.Unlock() + j.m.Lock() + return j.started +} + +func (j *countJob) Done() int { + defer j.m.Unlock() + j.m.Lock() + return j.done +} + +func TestChainDelayIfStillRunning(t *testing.T) { + t.Run("runs immediately", func(*testing.T) { + var j countJob + wrappedJob := NewChain(DelayIfStillRunning(DiscardLogger)).Then(&j) + go wrappedJob.Run() + time.Sleep(2 * time.Millisecond) // Give the job 2ms to complete. + if c := j.Done(); c != 1 { + t.Errorf("expected job run once, immediately, got %d", c) + } + }) + + t.Run("second run immediate if first done", func(*testing.T) { + var j countJob + wrappedJob := NewChain(DelayIfStillRunning(DiscardLogger)).Then(&j) + go func() { + go wrappedJob.Run() + time.Sleep(time.Millisecond) + go wrappedJob.Run() + }() + time.Sleep(3 * time.Millisecond) // Give both jobs 3ms to complete. + if c := j.Done(); c != 2 { + t.Errorf("expected job run twice, immediately, got %d", c) + } + }) + + t.Run("second run delayed if first not done", func(*testing.T) { + var j countJob + j.delay = 10 * time.Millisecond + wrappedJob := NewChain(DelayIfStillRunning(DiscardLogger)).Then(&j) + go func() { + go wrappedJob.Run() + time.Sleep(time.Millisecond) + go wrappedJob.Run() + }() + + // After 5ms, the first job is still in progress, and the second job was + // run but should be waiting for it to finish. + time.Sleep(5 * time.Millisecond) + started, done := j.Started(), j.Done() + if started != 1 || done != 0 { + t.Error("expected first job started, but not finished, got", started, done) + } + + // Verify that the second job completes. + time.Sleep(25 * time.Millisecond) + started, done = j.Started(), j.Done() + if started != 2 || done != 2 { + t.Error("expected both jobs done, got", started, done) + } + }) +} + +func TestChainSkipIfStillRunning(t *testing.T) { + t.Run("runs immediately", func(*testing.T) { + var j countJob + wrappedJob := NewChain(SkipIfStillRunning(DiscardLogger)).Then(&j) + go wrappedJob.Run() + time.Sleep(2 * time.Millisecond) // Give the job 2ms to complete. + if c := j.Done(); c != 1 { + t.Errorf("expected job run once, immediately, got %d", c) + } + }) + + t.Run("second run immediate if first done", func(*testing.T) { + var j countJob + wrappedJob := NewChain(SkipIfStillRunning(DiscardLogger)).Then(&j) + go func() { + go wrappedJob.Run() + time.Sleep(time.Millisecond) + go wrappedJob.Run() + }() + time.Sleep(3 * time.Millisecond) // Give both jobs 3ms to complete. + if c := j.Done(); c != 2 { + t.Errorf("expected job run twice, immediately, got %d", c) + } + }) + + t.Run("second run skipped if first not done", func(*testing.T) { + var j countJob + j.delay = 10 * time.Millisecond + wrappedJob := NewChain(SkipIfStillRunning(DiscardLogger)).Then(&j) + go func() { + go wrappedJob.Run() + time.Sleep(time.Millisecond) + go wrappedJob.Run() + }() + + // After 5ms, the first job is still in progress, and the second job was + // aleady skipped. + time.Sleep(5 * time.Millisecond) + started, done := j.Started(), j.Done() + if started != 1 || done != 0 { + t.Error("expected first job started, but not finished, got", started, done) + } + + // Verify that the first job completes and second does not run. + time.Sleep(25 * time.Millisecond) + started, done = j.Started(), j.Done() + if started != 1 || done != 1 { + t.Error("expected second job skipped, got", started, done) + } + }) + + t.Run("skip 10 jobs on rapid fire", func(*testing.T) { + var j countJob + j.delay = 10 * time.Millisecond + wrappedJob := NewChain(SkipIfStillRunning(DiscardLogger)).Then(&j) + for i := 0; i < 11; i++ { + go wrappedJob.Run() + } + time.Sleep(200 * time.Millisecond) + done := j.Done() + if done != 1 { + t.Error("expected 1 jobs executed, 10 jobs dropped, got", done) + } + }) + + t.Run("different jobs independent", func(*testing.T) { + var j1, j2 countJob + j1.delay = 10 * time.Millisecond + j2.delay = 10 * time.Millisecond + chain := NewChain(SkipIfStillRunning(DiscardLogger)) + wrappedJob1 := chain.Then(&j1) + wrappedJob2 := chain.Then(&j2) + for i := 0; i < 11; i++ { + go wrappedJob1.Run() + go wrappedJob2.Run() + } + time.Sleep(100 * time.Millisecond) + var ( + done1 = j1.Done() + done2 = j2.Done() + ) + if done1 != 1 || done2 != 1 { + t.Error("expected both jobs executed once, got", done1, "and", done2) + } + }) +} diff --git a/plugin/cron/constantdelay.go b/plugin/cron/constantdelay.go new file mode 100644 index 0000000..cd6e7b1 --- /dev/null +++ b/plugin/cron/constantdelay.go @@ -0,0 +1,27 @@ +package cron + +import "time" + +// ConstantDelaySchedule represents a simple recurring duty cycle, e.g. "Every 5 minutes". +// It does not support jobs more frequent than once a second. +type ConstantDelaySchedule struct { + Delay time.Duration +} + +// Every returns a crontab Schedule that activates once every duration. +// Delays of less than a second are not supported (will round up to 1 second). +// Any fields less than a Second are truncated. +func Every(duration time.Duration) ConstantDelaySchedule { + if duration < time.Second { + duration = time.Second + } + return ConstantDelaySchedule{ + Delay: duration - time.Duration(duration.Nanoseconds())%time.Second, + } +} + +// Next returns the next time this should be run. +// This rounds so that the next activation time will be on the second. +func (schedule ConstantDelaySchedule) Next(t time.Time) time.Time { + return t.Add(schedule.Delay - time.Duration(t.Nanosecond())*time.Nanosecond) +} diff --git a/plugin/cron/constantdelay_test.go b/plugin/cron/constantdelay_test.go new file mode 100644 index 0000000..40e5c4e --- /dev/null +++ b/plugin/cron/constantdelay_test.go @@ -0,0 +1,55 @@ +//nolint:all +package cron + +import ( + "testing" + "time" +) + +func TestConstantDelayNext(t *testing.T) { + tests := []struct { + time string + delay time.Duration + expected string + }{ + // Simple cases + {"Mon Jul 9 14:45 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"}, + {"Mon Jul 9 14:59 2012", 15 * time.Minute, "Mon Jul 9 15:14 2012"}, + {"Mon Jul 9 14:59:59 2012", 15 * time.Minute, "Mon Jul 9 15:14:59 2012"}, + + // Wrap around hours + {"Mon Jul 9 15:45 2012", 35 * time.Minute, "Mon Jul 9 16:20 2012"}, + + // Wrap around days + {"Mon Jul 9 23:46 2012", 14 * time.Minute, "Tue Jul 10 00:00 2012"}, + {"Mon Jul 9 23:45 2012", 35 * time.Minute, "Tue Jul 10 00:20 2012"}, + {"Mon Jul 9 23:35:51 2012", 44*time.Minute + 24*time.Second, "Tue Jul 10 00:20:15 2012"}, + {"Mon Jul 9 23:35:51 2012", 25*time.Hour + 44*time.Minute + 24*time.Second, "Thu Jul 11 01:20:15 2012"}, + + // Wrap around months + {"Mon Jul 9 23:35 2012", 91*24*time.Hour + 25*time.Minute, "Thu Oct 9 00:00 2012"}, + + // Wrap around minute, hour, day, month, and year + {"Mon Dec 31 23:59:45 2012", 15 * time.Second, "Tue Jan 1 00:00:00 2013"}, + + // Round to nearest second on the delay + {"Mon Jul 9 14:45 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"}, + + // Round up to 1 second if the duration is less. + {"Mon Jul 9 14:45:00 2012", 15 * time.Millisecond, "Mon Jul 9 14:45:01 2012"}, + + // Round to nearest second when calculating the next time. + {"Mon Jul 9 14:45:00.005 2012", 15 * time.Minute, "Mon Jul 9 15:00 2012"}, + + // Round to nearest second for both. + {"Mon Jul 9 14:45:00.005 2012", 15*time.Minute + 50*time.Nanosecond, "Mon Jul 9 15:00 2012"}, + } + + for _, c := range tests { + actual := Every(c.delay).Next(getTime(c.time)) + expected := getTime(c.expected) + if actual != expected { + t.Errorf("%s, \"%s\": (expected) %v != %v (actual)", c.time, c.delay, expected, actual) + } + } +} diff --git a/plugin/cron/cron.go b/plugin/cron/cron.go new file mode 100644 index 0000000..03411dd --- /dev/null +++ b/plugin/cron/cron.go @@ -0,0 +1,355 @@ +package cron + +import ( + "context" + "sort" + "sync" + "time" +) + +// Cron keeps track of any number of entries, invoking the associated func as +// specified by the schedule. It may be started, stopped, and the entries may +// be inspected while running. +type Cron struct { + entries []*Entry + chain Chain + stop chan struct{} + add chan *Entry + remove chan EntryID + snapshot chan chan []Entry + running bool + logger Logger + runningMu sync.Mutex + location *time.Location + parser ScheduleParser + nextID EntryID + jobWaiter sync.WaitGroup +} + +// ScheduleParser is an interface for schedule spec parsers that return a Schedule. +type ScheduleParser interface { + Parse(spec string) (Schedule, error) +} + +// Job is an interface for submitted cron jobs. +type Job interface { + Run() +} + +// Schedule describes a job's duty cycle. +type Schedule interface { + // Next returns the next activation time, later than the given time. + // Next is invoked initially, and then each time the job is run. + Next(time.Time) time.Time +} + +// EntryID identifies an entry within a Cron instance. +type EntryID int + +// Entry consists of a schedule and the func to execute on that schedule. +type Entry struct { + // ID is the cron-assigned ID of this entry, which may be used to look up a + // snapshot or remove it. + ID EntryID + + // Schedule on which this job should be run. + Schedule Schedule + + // Next time the job will run, or the zero time if Cron has not been + // started or this entry's schedule is unsatisfiable + Next time.Time + + // Prev is the last time this job was run, or the zero time if never. + Prev time.Time + + // WrappedJob is the thing to run when the Schedule is activated. + WrappedJob Job + + // Job is the thing that was submitted to cron. + // It is kept around so that user code that needs to get at the job later, + // e.g. via Entries() can do so. + Job Job +} + +// Valid returns true if this is not the zero entry. +func (e Entry) Valid() bool { return e.ID != 0 } + +// byTime is a wrapper for sorting the entry array by time +// (with zero time at the end). +type byTime []*Entry + +func (s byTime) Len() int { return len(s) } +func (s byTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s byTime) Less(i, j int) bool { + // Two zero times should return false. + // Otherwise, zero is "greater" than any other time. + // (To sort it at the end of the list.) + if s[i].Next.IsZero() { + return false + } + if s[j].Next.IsZero() { + return true + } + return s[i].Next.Before(s[j].Next) +} + +// New returns a new Cron job runner, modified by the given options. +// +// Available Settings +// +// Time Zone +// Description: The time zone in which schedules are interpreted +// Default: time.Local +// +// Parser +// Description: Parser converts cron spec strings into cron.Schedules. +// Default: Accepts this spec: https://en.wikipedia.org/wiki/Cron +// +// Chain +// Description: Wrap submitted jobs to customize behavior. +// Default: A chain that recovers panics and logs them to stderr. +// +// See "cron.With*" to modify the default behavior. +func New(opts ...Option) *Cron { + c := &Cron{ + entries: nil, + chain: NewChain(), + add: make(chan *Entry), + stop: make(chan struct{}), + snapshot: make(chan chan []Entry), + remove: make(chan EntryID), + running: false, + runningMu: sync.Mutex{}, + logger: DefaultLogger, + location: time.Local, + parser: standardParser, + } + for _, opt := range opts { + opt(c) + } + return c +} + +// FuncJob is a wrapper that turns a func() into a cron.Job. +type FuncJob func() + +func (f FuncJob) Run() { f() } + +// AddFunc adds a func to the Cron to be run on the given schedule. +// The spec is parsed using the time zone of this Cron instance as the default. +// An opaque ID is returned that can be used to later remove it. +func (c *Cron) AddFunc(spec string, cmd func()) (EntryID, error) { + return c.AddJob(spec, FuncJob(cmd)) +} + +// AddJob adds a Job to the Cron to be run on the given schedule. +// The spec is parsed using the time zone of this Cron instance as the default. +// An opaque ID is returned that can be used to later remove it. +func (c *Cron) AddJob(spec string, cmd Job) (EntryID, error) { + schedule, err := c.parser.Parse(spec) + if err != nil { + return 0, err + } + return c.Schedule(schedule, cmd), nil +} + +// Schedule adds a Job to the Cron to be run on the given schedule. +// The job is wrapped with the configured Chain. +func (c *Cron) Schedule(schedule Schedule, cmd Job) EntryID { + c.runningMu.Lock() + defer c.runningMu.Unlock() + c.nextID++ + entry := &Entry{ + ID: c.nextID, + Schedule: schedule, + WrappedJob: c.chain.Then(cmd), + Job: cmd, + } + if !c.running { + c.entries = append(c.entries, entry) + } else { + c.add <- entry + } + return entry.ID +} + +// Entries returns a snapshot of the cron entries. +func (c *Cron) Entries() []Entry { + c.runningMu.Lock() + defer c.runningMu.Unlock() + if c.running { + replyChan := make(chan []Entry, 1) + c.snapshot <- replyChan + return <-replyChan + } + return c.entrySnapshot() +} + +// Location gets the time zone location. +func (c *Cron) Location() *time.Location { + return c.location +} + +// Entry returns a snapshot of the given entry, or nil if it couldn't be found. +func (c *Cron) Entry(id EntryID) Entry { + for _, entry := range c.Entries() { + if id == entry.ID { + return entry + } + } + return Entry{} +} + +// Remove an entry from being run in the future. +func (c *Cron) Remove(id EntryID) { + c.runningMu.Lock() + defer c.runningMu.Unlock() + if c.running { + c.remove <- id + } else { + c.removeEntry(id) + } +} + +// Start the cron scheduler in its own goroutine, or no-op if already started. +func (c *Cron) Start() { + c.runningMu.Lock() + defer c.runningMu.Unlock() + if c.running { + return + } + c.running = true + go c.runScheduler() +} + +// Run the cron scheduler, or no-op if already running. +func (c *Cron) Run() { + c.runningMu.Lock() + if c.running { + c.runningMu.Unlock() + return + } + c.running = true + c.runningMu.Unlock() + c.runScheduler() +} + +// runScheduler runs the scheduler.. this is private just due to the need to synchronize +// access to the 'running' state variable. +func (c *Cron) runScheduler() { + c.logger.Info("start") + + // Figure out the next activation times for each entry. + now := c.now() + for _, entry := range c.entries { + entry.Next = entry.Schedule.Next(now) + c.logger.Info("schedule", "now", now, "entry", entry.ID, "next", entry.Next) + } + + for { + // Determine the next entry to run. + sort.Sort(byTime(c.entries)) + + var timer *time.Timer + if len(c.entries) == 0 || c.entries[0].Next.IsZero() { + // If there are no entries yet, just sleep - it still handles new entries + // and stop requests. + timer = time.NewTimer(100000 * time.Hour) + } else { + timer = time.NewTimer(c.entries[0].Next.Sub(now)) + } + + for { + select { + case now = <-timer.C: + now = now.In(c.location) + c.logger.Info("wake", "now", now) + + // Run every entry whose next time was less than now + for _, e := range c.entries { + if e.Next.After(now) || e.Next.IsZero() { + break + } + c.startJob(e.WrappedJob) + e.Prev = e.Next + e.Next = e.Schedule.Next(now) + c.logger.Info("run", "now", now, "entry", e.ID, "next", e.Next) + } + + case newEntry := <-c.add: + timer.Stop() + now = c.now() + newEntry.Next = newEntry.Schedule.Next(now) + c.entries = append(c.entries, newEntry) + c.logger.Info("added", "now", now, "entry", newEntry.ID, "next", newEntry.Next) + + case replyChan := <-c.snapshot: + replyChan <- c.entrySnapshot() + continue + + case <-c.stop: + timer.Stop() + c.logger.Info("stop") + return + + case id := <-c.remove: + timer.Stop() + now = c.now() + c.removeEntry(id) + c.logger.Info("removed", "entry", id) + } + + break + } + } +} + +// startJob runs the given job in a new goroutine. +func (c *Cron) startJob(j Job) { + c.jobWaiter.Add(1) + go func() { + defer c.jobWaiter.Done() + j.Run() + }() +} + +// now returns current time in c location. +func (c *Cron) now() time.Time { + return time.Now().In(c.location) +} + +// Stop stops the cron scheduler if it is running; otherwise it does nothing. +// A context is returned so the caller can wait for running jobs to complete. +func (c *Cron) Stop() context.Context { + c.runningMu.Lock() + defer c.runningMu.Unlock() + if c.running { + c.stop <- struct{}{} + c.running = false + } + ctx, cancel := context.WithCancel(context.Background()) + go func() { + c.jobWaiter.Wait() + cancel() + }() + return ctx +} + +// entrySnapshot returns a copy of the current cron entry list. +func (c *Cron) entrySnapshot() []Entry { + var entries = make([]Entry, len(c.entries)) + for i, e := range c.entries { + entries[i] = *e + } + return entries +} + +func (c *Cron) removeEntry(id EntryID) { + var entries []*Entry + for _, e := range c.entries { + if e.ID != id { + entries = append(entries, e) + } + } + c.entries = entries +} diff --git a/plugin/cron/cron_test.go b/plugin/cron/cron_test.go new file mode 100644 index 0000000..32b3aa4 --- /dev/null +++ b/plugin/cron/cron_test.go @@ -0,0 +1,702 @@ +//nolint:all +package cron + +import ( + "bytes" + "fmt" + "log" + "strings" + "sync" + "sync/atomic" + "testing" + "time" +) + +// Many tests schedule a job for every second, and then wait at most a second +// for it to run. This amount is just slightly larger than 1 second to +// compensate for a few milliseconds of runtime. +const OneSecond = 1*time.Second + 50*time.Millisecond + +type syncWriter struct { + wr bytes.Buffer + m sync.Mutex +} + +func (sw *syncWriter) Write(data []byte) (n int, err error) { + sw.m.Lock() + n, err = sw.wr.Write(data) + sw.m.Unlock() + return +} + +func (sw *syncWriter) String() string { + sw.m.Lock() + defer sw.m.Unlock() + return sw.wr.String() +} + +func newBufLogger(sw *syncWriter) Logger { + return PrintfLogger(log.New(sw, "", log.LstdFlags)) +} + +func TestFuncPanicRecovery(t *testing.T) { + var buf syncWriter + cron := New(WithParser(secondParser), + WithChain(Recover(newBufLogger(&buf)))) + cron.Start() + defer cron.Stop() + cron.AddFunc("* * * * * ?", func() { + panic("YOLO") + }) + + select { + case <-time.After(OneSecond): + if !strings.Contains(buf.String(), "YOLO") { + t.Error("expected a panic to be logged, got none") + } + return + } +} + +type DummyJob struct{} + +func (DummyJob) Run() { + panic("YOLO") +} + +func TestJobPanicRecovery(t *testing.T) { + var job DummyJob + + var buf syncWriter + cron := New(WithParser(secondParser), + WithChain(Recover(newBufLogger(&buf)))) + cron.Start() + defer cron.Stop() + cron.AddJob("* * * * * ?", job) + + select { + case <-time.After(OneSecond): + if !strings.Contains(buf.String(), "YOLO") { + t.Error("expected a panic to be logged, got none") + } + return + } +} + +// Start and stop cron with no entries. +func TestNoEntries(t *testing.T) { + cron := newWithSeconds() + cron.Start() + + select { + case <-time.After(OneSecond): + t.Fatal("expected cron will be stopped immediately") + case <-stop(cron): + } +} + +// Start, stop, then add an entry. Verify entry doesn't run. +func TestStopCausesJobsToNotRun(t *testing.T) { + wg := &sync.WaitGroup{} + wg.Add(1) + + cron := newWithSeconds() + cron.Start() + cron.Stop() + cron.AddFunc("* * * * * ?", func() { wg.Done() }) + + select { + case <-time.After(OneSecond): + // No job ran! + case <-wait(wg): + t.Fatal("expected stopped cron does not run any job") + } +} + +// Add a job, start cron, expect it runs. +func TestAddBeforeRunning(t *testing.T) { + wg := &sync.WaitGroup{} + wg.Add(1) + + cron := newWithSeconds() + cron.AddFunc("* * * * * ?", func() { wg.Done() }) + cron.Start() + defer cron.Stop() + + // Give cron 2 seconds to run our job (which is always activated). + select { + case <-time.After(OneSecond): + t.Fatal("expected job runs") + case <-wait(wg): + } +} + +// Start cron, add a job, expect it runs. +func TestAddWhileRunning(t *testing.T) { + wg := &sync.WaitGroup{} + wg.Add(1) + + cron := newWithSeconds() + cron.Start() + defer cron.Stop() + cron.AddFunc("* * * * * ?", func() { wg.Done() }) + + select { + case <-time.After(OneSecond): + t.Fatal("expected job runs") + case <-wait(wg): + } +} + +// Test for #34. Adding a job after calling start results in multiple job invocations +func TestAddWhileRunningWithDelay(t *testing.T) { + cron := newWithSeconds() + cron.Start() + defer cron.Stop() + time.Sleep(5 * time.Second) + var calls int64 + cron.AddFunc("* * * * * *", func() { atomic.AddInt64(&calls, 1) }) + + <-time.After(OneSecond) + if atomic.LoadInt64(&calls) != 1 { + t.Errorf("called %d times, expected 1\n", calls) + } +} + +// Add a job, remove a job, start cron, expect nothing runs. +func TestRemoveBeforeRunning(t *testing.T) { + wg := &sync.WaitGroup{} + wg.Add(1) + + cron := newWithSeconds() + id, _ := cron.AddFunc("* * * * * ?", func() { wg.Done() }) + cron.Remove(id) + cron.Start() + defer cron.Stop() + + select { + case <-time.After(OneSecond): + // Success, shouldn't run + case <-wait(wg): + t.FailNow() + } +} + +// Start cron, add a job, remove it, expect it doesn't run. +func TestRemoveWhileRunning(t *testing.T) { + wg := &sync.WaitGroup{} + wg.Add(1) + + cron := newWithSeconds() + cron.Start() + defer cron.Stop() + id, _ := cron.AddFunc("* * * * * ?", func() { wg.Done() }) + cron.Remove(id) + + select { + case <-time.After(OneSecond): + case <-wait(wg): + t.FailNow() + } +} + +// Test timing with Entries. +func TestSnapshotEntries(t *testing.T) { + wg := &sync.WaitGroup{} + wg.Add(1) + + cron := New() + cron.AddFunc("@every 2s", func() { wg.Done() }) + cron.Start() + defer cron.Stop() + + // Cron should fire in 2 seconds. After 1 second, call Entries. + select { + case <-time.After(OneSecond): + cron.Entries() + } + + // Even though Entries was called, the cron should fire at the 2 second mark. + select { + case <-time.After(OneSecond): + t.Error("expected job runs at 2 second mark") + case <-wait(wg): + } +} + +// Test that the entries are correctly sorted. +// Add a bunch of long-in-the-future entries, and an immediate entry, and ensure +// that the immediate entry runs immediately. +// Also: Test that multiple jobs run in the same instant. +func TestMultipleEntries(t *testing.T) { + wg := &sync.WaitGroup{} + wg.Add(2) + + cron := newWithSeconds() + cron.AddFunc("0 0 0 1 1 ?", func() {}) + cron.AddFunc("* * * * * ?", func() { wg.Done() }) + id1, _ := cron.AddFunc("* * * * * ?", func() { t.Fatal() }) + id2, _ := cron.AddFunc("* * * * * ?", func() { t.Fatal() }) + cron.AddFunc("0 0 0 31 12 ?", func() {}) + cron.AddFunc("* * * * * ?", func() { wg.Done() }) + + cron.Remove(id1) + cron.Start() + cron.Remove(id2) + defer cron.Stop() + + select { + case <-time.After(OneSecond): + t.Error("expected job run in proper order") + case <-wait(wg): + } +} + +// Test running the same job twice. +func TestRunningJobTwice(t *testing.T) { + wg := &sync.WaitGroup{} + wg.Add(2) + + cron := newWithSeconds() + cron.AddFunc("0 0 0 1 1 ?", func() {}) + cron.AddFunc("0 0 0 31 12 ?", func() {}) + cron.AddFunc("* * * * * ?", func() { wg.Done() }) + + cron.Start() + defer cron.Stop() + + select { + case <-time.After(2 * OneSecond): + t.Error("expected job fires 2 times") + case <-wait(wg): + } +} + +func TestRunningMultipleSchedules(t *testing.T) { + wg := &sync.WaitGroup{} + wg.Add(2) + + cron := newWithSeconds() + cron.AddFunc("0 0 0 1 1 ?", func() {}) + cron.AddFunc("0 0 0 31 12 ?", func() {}) + cron.AddFunc("* * * * * ?", func() { wg.Done() }) + cron.Schedule(Every(time.Minute), FuncJob(func() {})) + cron.Schedule(Every(time.Second), FuncJob(func() { wg.Done() })) + cron.Schedule(Every(time.Hour), FuncJob(func() {})) + + cron.Start() + defer cron.Stop() + + select { + case <-time.After(2 * OneSecond): + t.Error("expected job fires 2 times") + case <-wait(wg): + } +} + +// Test that the cron is run in the local time zone (as opposed to UTC). +func TestLocalTimezone(t *testing.T) { + wg := &sync.WaitGroup{} + wg.Add(2) + + now := time.Now() + // FIX: Issue #205 + // This calculation doesn't work in seconds 58 or 59. + // Take the easy way out and sleep. + if now.Second() >= 58 { + time.Sleep(2 * time.Second) + now = time.Now() + } + spec := fmt.Sprintf("%d,%d %d %d %d %d ?", + now.Second()+1, now.Second()+2, now.Minute(), now.Hour(), now.Day(), now.Month()) + + cron := newWithSeconds() + cron.AddFunc(spec, func() { wg.Done() }) + cron.Start() + defer cron.Stop() + + select { + case <-time.After(OneSecond * 2): + t.Error("expected job fires 2 times") + case <-wait(wg): + } +} + +// Test that the cron is run in the given time zone (as opposed to local). +func TestNonLocalTimezone(t *testing.T) { + wg := &sync.WaitGroup{} + wg.Add(2) + + loc, err := time.LoadLocation("Atlantic/Cape_Verde") + if err != nil { + fmt.Printf("Failed to load time zone Atlantic/Cape_Verde: %+v", err) + t.Fail() + } + + now := time.Now().In(loc) + // FIX: Issue #205 + // This calculation doesn't work in seconds 58 or 59. + // Take the easy way out and sleep. + if now.Second() >= 58 { + time.Sleep(2 * time.Second) + now = time.Now().In(loc) + } + spec := fmt.Sprintf("%d,%d %d %d %d %d ?", + now.Second()+1, now.Second()+2, now.Minute(), now.Hour(), now.Day(), now.Month()) + + cron := New(WithLocation(loc), WithParser(secondParser)) + cron.AddFunc(spec, func() { wg.Done() }) + cron.Start() + defer cron.Stop() + + select { + case <-time.After(OneSecond * 2): + t.Error("expected job fires 2 times") + case <-wait(wg): + } +} + +// Test that calling stop before start silently returns without +// blocking the stop channel. +func TestStopWithoutStart(t *testing.T) { + cron := New() + cron.Stop() +} + +type testJob struct { + wg *sync.WaitGroup + name string +} + +func (t testJob) Run() { + t.wg.Done() +} + +// Test that adding an invalid job spec returns an error +func TestInvalidJobSpec(t *testing.T) { + cron := New() + _, err := cron.AddJob("this will not parse", nil) + if err == nil { + t.Errorf("expected an error with invalid spec, got nil") + } +} + +// Test blocking run method behaves as Start() +func TestBlockingRun(t *testing.T) { + wg := &sync.WaitGroup{} + wg.Add(1) + + cron := newWithSeconds() + cron.AddFunc("* * * * * ?", func() { wg.Done() }) + + var unblockChan = make(chan struct{}) + + go func() { + cron.Run() + close(unblockChan) + }() + defer cron.Stop() + + select { + case <-time.After(OneSecond): + t.Error("expected job fires") + case <-unblockChan: + t.Error("expected that Run() blocks") + case <-wait(wg): + } +} + +// Test that double-running is a no-op +func TestStartNoop(t *testing.T) { + var tickChan = make(chan struct{}, 2) + + cron := newWithSeconds() + cron.AddFunc("* * * * * ?", func() { + tickChan <- struct{}{} + }) + + cron.Start() + defer cron.Stop() + + // Wait for the first firing to ensure the runner is going + <-tickChan + + cron.Start() + + <-tickChan + + // Fail if this job fires again in a short period, indicating a double-run + select { + case <-time.After(time.Millisecond): + case <-tickChan: + t.Error("expected job fires exactly twice") + } +} + +// Simple test using Runnables. +func TestJob(t *testing.T) { + wg := &sync.WaitGroup{} + wg.Add(1) + + cron := newWithSeconds() + cron.AddJob("0 0 0 30 Feb ?", testJob{wg, "job0"}) + cron.AddJob("0 0 0 1 1 ?", testJob{wg, "job1"}) + job2, _ := cron.AddJob("* * * * * ?", testJob{wg, "job2"}) + cron.AddJob("1 0 0 1 1 ?", testJob{wg, "job3"}) + cron.Schedule(Every(5*time.Second+5*time.Nanosecond), testJob{wg, "job4"}) + job5 := cron.Schedule(Every(5*time.Minute), testJob{wg, "job5"}) + + // Test getting an Entry pre-Start. + if actualName := cron.Entry(job2).Job.(testJob).name; actualName != "job2" { + t.Error("wrong job retrieved:", actualName) + } + if actualName := cron.Entry(job5).Job.(testJob).name; actualName != "job5" { + t.Error("wrong job retrieved:", actualName) + } + + cron.Start() + defer cron.Stop() + + select { + case <-time.After(OneSecond): + t.FailNow() + case <-wait(wg): + } + + // Ensure the entries are in the right order. + expecteds := []string{"job2", "job4", "job5", "job1", "job3", "job0"} + + var actuals []string + for _, entry := range cron.Entries() { + actuals = append(actuals, entry.Job.(testJob).name) + } + + for i, expected := range expecteds { + if actuals[i] != expected { + t.Fatalf("Jobs not in the right order. (expected) %s != %s (actual)", expecteds, actuals) + } + } + + // Test getting Entries. + if actualName := cron.Entry(job2).Job.(testJob).name; actualName != "job2" { + t.Error("wrong job retrieved:", actualName) + } + if actualName := cron.Entry(job5).Job.(testJob).name; actualName != "job5" { + t.Error("wrong job retrieved:", actualName) + } +} + +// Issue #206 +// Ensure that the next run of a job after removing an entry is accurate. +func TestScheduleAfterRemoval(t *testing.T) { + var wg1 sync.WaitGroup + var wg2 sync.WaitGroup + wg1.Add(1) + wg2.Add(1) + + // The first time this job is run, set a timer and remove the other job + // 750ms later. Correct behavior would be to still run the job again in + // 250ms, but the bug would cause it to run instead 1s later. + + var calls int + var mu sync.Mutex + + cron := newWithSeconds() + hourJob := cron.Schedule(Every(time.Hour), FuncJob(func() {})) + cron.Schedule(Every(time.Second), FuncJob(func() { + mu.Lock() + defer mu.Unlock() + switch calls { + case 0: + wg1.Done() + calls++ + case 1: + time.Sleep(750 * time.Millisecond) + cron.Remove(hourJob) + calls++ + case 2: + calls++ + wg2.Done() + case 3: + panic("unexpected 3rd call") + } + })) + + cron.Start() + defer cron.Stop() + + // the first run might be any length of time 0 - 1s, since the schedule + // rounds to the second. wait for the first run to true up. + wg1.Wait() + + select { + case <-time.After(2 * OneSecond): + t.Error("expected job fires 2 times") + case <-wait(&wg2): + } +} + +type ZeroSchedule struct{} + +func (*ZeroSchedule) Next(time.Time) time.Time { + return time.Time{} +} + +// Tests that job without time does not run +func TestJobWithZeroTimeDoesNotRun(t *testing.T) { + cron := newWithSeconds() + var calls int64 + cron.AddFunc("* * * * * *", func() { atomic.AddInt64(&calls, 1) }) + cron.Schedule(new(ZeroSchedule), FuncJob(func() { t.Error("expected zero task will not run") })) + cron.Start() + defer cron.Stop() + <-time.After(OneSecond) + if atomic.LoadInt64(&calls) != 1 { + t.Errorf("called %d times, expected 1\n", calls) + } +} + +func TestStopAndWait(t *testing.T) { + t.Run("nothing running, returns immediately", func(*testing.T) { + cron := newWithSeconds() + cron.Start() + ctx := cron.Stop() + select { + case <-ctx.Done(): + case <-time.After(time.Millisecond): + t.Error("context was not done immediately") + } + }) + + t.Run("repeated calls to Stop", func(*testing.T) { + cron := newWithSeconds() + cron.Start() + _ = cron.Stop() + time.Sleep(time.Millisecond) + ctx := cron.Stop() + select { + case <-ctx.Done(): + case <-time.After(time.Millisecond): + t.Error("context was not done immediately") + } + }) + + t.Run("a couple fast jobs added, still returns immediately", func(*testing.T) { + cron := newWithSeconds() + cron.AddFunc("* * * * * *", func() {}) + cron.Start() + cron.AddFunc("* * * * * *", func() {}) + cron.AddFunc("* * * * * *", func() {}) + cron.AddFunc("* * * * * *", func() {}) + time.Sleep(time.Second) + ctx := cron.Stop() + select { + case <-ctx.Done(): + case <-time.After(time.Millisecond): + t.Error("context was not done immediately") + } + }) + + t.Run("a couple fast jobs and a slow job added, waits for slow job", func(*testing.T) { + cron := newWithSeconds() + cron.AddFunc("* * * * * *", func() {}) + cron.Start() + cron.AddFunc("* * * * * *", func() { time.Sleep(2 * time.Second) }) + cron.AddFunc("* * * * * *", func() {}) + time.Sleep(time.Second) + + ctx := cron.Stop() + + // Verify that it is not done for at least 750ms + select { + case <-ctx.Done(): + t.Error("context was done too quickly immediately") + case <-time.After(750 * time.Millisecond): + // expected, because the job sleeping for 1 second is still running + } + + // Verify that it IS done in the next 500ms (giving 250ms buffer) + select { + case <-ctx.Done(): + // expected + case <-time.After(1500 * time.Millisecond): + t.Error("context not done after job should have completed") + } + }) + + t.Run("repeated calls to stop, waiting for completion and after", func(*testing.T) { + cron := newWithSeconds() + cron.AddFunc("* * * * * *", func() {}) + cron.AddFunc("* * * * * *", func() { time.Sleep(2 * time.Second) }) + cron.Start() + cron.AddFunc("* * * * * *", func() {}) + time.Sleep(time.Second) + ctx := cron.Stop() + ctx2 := cron.Stop() + + // Verify that it is not done for at least 1500ms + select { + case <-ctx.Done(): + t.Error("context was done too quickly immediately") + case <-ctx2.Done(): + t.Error("context2 was done too quickly immediately") + case <-time.After(1500 * time.Millisecond): + // expected, because the job sleeping for 2 seconds is still running + } + + // Verify that it IS done in the next 1s (giving 500ms buffer) + select { + case <-ctx.Done(): + // expected + case <-time.After(time.Second): + t.Error("context not done after job should have completed") + } + + // Verify that ctx2 is also done. + select { + case <-ctx2.Done(): + // expected + case <-time.After(time.Millisecond): + t.Error("context2 not done even though context1 is") + } + + // Verify that a new context retrieved from stop is immediately done. + ctx3 := cron.Stop() + select { + case <-ctx3.Done(): + // expected + case <-time.After(time.Millisecond): + t.Error("context not done even when cron Stop is completed") + } + }) +} + +func TestMultiThreadedStartAndStop(t *testing.T) { + cron := New() + go cron.Run() + time.Sleep(2 * time.Millisecond) + cron.Stop() +} + +func wait(wg *sync.WaitGroup) chan bool { + ch := make(chan bool) + go func() { + wg.Wait() + ch <- true + }() + return ch +} + +func stop(cron *Cron) chan bool { + ch := make(chan bool) + go func() { + cron.Stop() + ch <- true + }() + return ch +} + +// newWithSeconds returns a Cron with the seconds field enabled. +func newWithSeconds() *Cron { + return New(WithParser(secondParser), WithChain()) +} diff --git a/plugin/cron/logger.go b/plugin/cron/logger.go new file mode 100644 index 0000000..2a36117 --- /dev/null +++ b/plugin/cron/logger.go @@ -0,0 +1,86 @@ +package cron + +import ( + "io" + "log" + "os" + "strings" + "time" +) + +// DefaultLogger is used by Cron if none is specified. +var DefaultLogger = PrintfLogger(log.New(os.Stdout, "cron: ", log.LstdFlags)) + +// DiscardLogger can be used by callers to discard all log messages. +var DiscardLogger = PrintfLogger(log.New(io.Discard, "", 0)) + +// Logger is the interface used in this package for logging, so that any backend +// can be plugged in. It is a subset of the github.com/go-logr/logr interface. +type Logger interface { + // Info logs routine messages about cron's operation. + Info(msg string, keysAndValues ...interface{}) + // Error logs an error condition. + Error(err error, msg string, keysAndValues ...interface{}) +} + +// PrintfLogger wraps a Printf-based logger (such as the standard library "log") +// into an implementation of the Logger interface which logs errors only. +func PrintfLogger(l interface{ Printf(string, ...interface{}) }) Logger { + return printfLogger{l, false} +} + +// VerbosePrintfLogger wraps a Printf-based logger (such as the standard library +// "log") into an implementation of the Logger interface which logs everything. +func VerbosePrintfLogger(l interface{ Printf(string, ...interface{}) }) Logger { + return printfLogger{l, true} +} + +type printfLogger struct { + logger interface{ Printf(string, ...interface{}) } + logInfo bool +} + +func (pl printfLogger) Info(msg string, keysAndValues ...interface{}) { + if pl.logInfo { + keysAndValues = formatTimes(keysAndValues) + pl.logger.Printf( + formatString(len(keysAndValues)), + append([]interface{}{msg}, keysAndValues...)...) + } +} + +func (pl printfLogger) Error(err error, msg string, keysAndValues ...interface{}) { + keysAndValues = formatTimes(keysAndValues) + pl.logger.Printf( + formatString(len(keysAndValues)+2), + append([]interface{}{msg, "error", err}, keysAndValues...)...) +} + +// formatString returns a logfmt-like format string for the number of +// key/values. +func formatString(numKeysAndValues int) string { + var sb strings.Builder + sb.WriteString("%s") + if numKeysAndValues > 0 { + sb.WriteString(", ") + } + for i := 0; i < numKeysAndValues/2; i++ { + if i > 0 { + sb.WriteString(", ") + } + sb.WriteString("%v=%v") + } + return sb.String() +} + +// formatTimes formats any time.Time values as RFC3339. +func formatTimes(keysAndValues []interface{}) []interface{} { + var formattedArgs []interface{} + for _, arg := range keysAndValues { + if t, ok := arg.(time.Time); ok { + arg = t.Format(time.RFC3339) + } + formattedArgs = append(formattedArgs, arg) + } + return formattedArgs +} diff --git a/plugin/cron/option.go b/plugin/cron/option.go new file mode 100644 index 0000000..09e4278 --- /dev/null +++ b/plugin/cron/option.go @@ -0,0 +1,45 @@ +package cron + +import ( + "time" +) + +// Option represents a modification to the default behavior of a Cron. +type Option func(*Cron) + +// WithLocation overrides the timezone of the cron instance. +func WithLocation(loc *time.Location) Option { + return func(c *Cron) { + c.location = loc + } +} + +// WithSeconds overrides the parser used for interpreting job schedules to +// include a seconds field as the first one. +func WithSeconds() Option { + return WithParser(NewParser( + Second | Minute | Hour | Dom | Month | Dow | Descriptor, + )) +} + +// WithParser overrides the parser used for interpreting job schedules. +func WithParser(p ScheduleParser) Option { + return func(c *Cron) { + c.parser = p + } +} + +// WithChain specifies Job wrappers to apply to all jobs added to this cron. +// Refer to the Chain* functions in this package for provided wrappers. +func WithChain(wrappers ...JobWrapper) Option { + return func(c *Cron) { + c.chain = NewChain(wrappers...) + } +} + +// WithLogger uses the provided logger. +func WithLogger(logger Logger) Option { + return func(c *Cron) { + c.logger = logger + } +} diff --git a/plugin/cron/option_test.go b/plugin/cron/option_test.go new file mode 100644 index 0000000..5322e1e --- /dev/null +++ b/plugin/cron/option_test.go @@ -0,0 +1,43 @@ +//nolint:all +package cron + +import ( + "log" + "strings" + "testing" + "time" +) + +func TestWithLocation(t *testing.T) { + c := New(WithLocation(time.UTC)) + if c.location != time.UTC { + t.Errorf("expected UTC, got %v", c.location) + } +} + +func TestWithParser(t *testing.T) { + var parser = NewParser(Dow) + c := New(WithParser(parser)) + if c.parser != parser { + t.Error("expected provided parser") + } +} + +func TestWithVerboseLogger(t *testing.T) { + var buf syncWriter + var logger = log.New(&buf, "", log.LstdFlags) + c := New(WithLogger(VerbosePrintfLogger(logger))) + if c.logger.(printfLogger).logger != logger { + t.Error("expected provided logger") + } + + c.AddFunc("@every 1s", func() {}) + c.Start() + time.Sleep(OneSecond) + c.Stop() + out := buf.String() + if !strings.Contains(out, "schedule,") || + !strings.Contains(out, "run,") { + t.Error("expected to see some actions, got:", out) + } +} diff --git a/plugin/cron/parser.go b/plugin/cron/parser.go new file mode 100644 index 0000000..e1607a1 --- /dev/null +++ b/plugin/cron/parser.go @@ -0,0 +1,435 @@ +package cron + +import ( + "math" + "strconv" + "strings" + "time" + + "github.com/pkg/errors" +) + +// Configuration options for creating a parser. Most options specify which +// fields should be included, while others enable features. If a field is not +// included the parser will assume a default value. These options do not change +// the order fields are parse in. +type ParseOption int + +const ( + Second ParseOption = 1 << iota // Seconds field, default 0 + SecondOptional // Optional seconds field, default 0 + Minute // Minutes field, default 0 + Hour // Hours field, default 0 + Dom // Day of month field, default * + Month // Month field, default * + Dow // Day of week field, default * + DowOptional // Optional day of week field, default * + Descriptor // Allow descriptors such as @monthly, @weekly, etc. +) + +var places = []ParseOption{ + Second, + Minute, + Hour, + Dom, + Month, + Dow, +} + +var defaults = []string{ + "0", + "0", + "0", + "*", + "*", + "*", +} + +// A custom Parser that can be configured. +type Parser struct { + options ParseOption +} + +// NewParser creates a Parser with custom options. +// +// It panics if more than one Optional is given, since it would be impossible to +// correctly infer which optional is provided or missing in general. +// +// Examples +// +// // Standard parser without descriptors +// specParser := NewParser(Minute | Hour | Dom | Month | Dow) +// sched, err := specParser.Parse("0 0 15 */3 *") +// +// // Same as above, just excludes time fields +// specParser := NewParser(Dom | Month | Dow) +// sched, err := specParser.Parse("15 */3 *") +// +// // Same as above, just makes Dow optional +// specParser := NewParser(Dom | Month | DowOptional) +// sched, err := specParser.Parse("15 */3") +func NewParser(options ParseOption) Parser { + optionals := 0 + if options&DowOptional > 0 { + optionals++ + } + if options&SecondOptional > 0 { + optionals++ + } + if optionals > 1 { + panic("multiple optionals may not be configured") + } + return Parser{options} +} + +// Parse returns a new crontab schedule representing the given spec. +// It returns a descriptive error if the spec is not valid. +// It accepts crontab specs and features configured by NewParser. +func (p Parser) Parse(spec string) (Schedule, error) { + if len(spec) == 0 { + return nil, errors.New("empty spec string") + } + + // Extract timezone if present + var loc = time.Local + if strings.HasPrefix(spec, "TZ=") || strings.HasPrefix(spec, "CRON_TZ=") { + var err error + i := strings.Index(spec, " ") + eq := strings.Index(spec, "=") + if loc, err = time.LoadLocation(spec[eq+1 : i]); err != nil { + return nil, errors.Wrap(err, "provided bad location") + } + spec = strings.TrimSpace(spec[i:]) + } + + // Handle named schedules (descriptors), if configured + if strings.HasPrefix(spec, "@") { + if p.options&Descriptor == 0 { + return nil, errors.New("descriptors not enabled") + } + return parseDescriptor(spec, loc) + } + + // Split on whitespace. + fields := strings.Fields(spec) + + // Validate & fill in any omitted or optional fields + var err error + fields, err = normalizeFields(fields, p.options) + if err != nil { + return nil, err + } + + field := func(field string, r bounds) uint64 { + if err != nil { + return 0 + } + var bits uint64 + bits, err = getField(field, r) + return bits + } + + var ( + second = field(fields[0], seconds) + minute = field(fields[1], minutes) + hour = field(fields[2], hours) + dayofmonth = field(fields[3], dom) + month = field(fields[4], months) + dayofweek = field(fields[5], dow) + ) + if err != nil { + return nil, err + } + + return &SpecSchedule{ + Second: second, + Minute: minute, + Hour: hour, + Dom: dayofmonth, + Month: month, + Dow: dayofweek, + Location: loc, + }, nil +} + +// normalizeFields takes a subset set of the time fields and returns the full set +// with defaults (zeroes) populated for unset fields. +// +// As part of performing this function, it also validates that the provided +// fields are compatible with the configured options. +func normalizeFields(fields []string, options ParseOption) ([]string, error) { + // Validate optionals & add their field to options + optionals := 0 + if options&SecondOptional > 0 { + options |= Second + optionals++ + } + if options&DowOptional > 0 { + options |= Dow + optionals++ + } + if optionals > 1 { + return nil, errors.New("multiple optionals may not be configured") + } + + // Figure out how many fields we need + max := 0 + for _, place := range places { + if options&place > 0 { + max++ + } + } + min := max - optionals + + // Validate number of fields + if count := len(fields); count < min || count > max { + if min == max { + return nil, errors.New("incorrect number of fields") + } + return nil, errors.New("incorrect number of fields, expected " + strconv.Itoa(min) + "-" + strconv.Itoa(max)) + } + + // Populate the optional field if not provided + if min < max && len(fields) == min { + switch { + case options&DowOptional > 0: + fields = append(fields, defaults[5]) // TODO: improve access to default + case options&SecondOptional > 0: + fields = append([]string{defaults[0]}, fields...) + default: + return nil, errors.New("unexpected optional field") + } + } + + // Populate all fields not part of options with their defaults + n := 0 + expandedFields := make([]string, len(places)) + copy(expandedFields, defaults) + for i, place := range places { + if options&place > 0 { + expandedFields[i] = fields[n] + n++ + } + } + return expandedFields, nil +} + +var standardParser = NewParser( + Minute | Hour | Dom | Month | Dow | Descriptor, +) + +// ParseStandard returns a new crontab schedule representing the given +// standardSpec (https://en.wikipedia.org/wiki/Cron). It requires 5 entries +// representing: minute, hour, day of month, month and day of week, in that +// order. It returns a descriptive error if the spec is not valid. +// +// It accepts +// - Standard crontab specs, e.g. "* * * * ?" +// - Descriptors, e.g. "@midnight", "@every 1h30m" +func ParseStandard(standardSpec string) (Schedule, error) { + return standardParser.Parse(standardSpec) +} + +// getField returns an Int with the bits set representing all of the times that +// the field represents or error parsing field value. A "field" is a comma-separated +// list of "ranges". +func getField(field string, r bounds) (uint64, error) { + var bits uint64 + ranges := strings.FieldsFunc(field, func(r rune) bool { return r == ',' }) + for _, expr := range ranges { + bit, err := getRange(expr, r) + if err != nil { + return bits, err + } + bits |= bit + } + return bits, nil +} + +// getRange returns the bits indicated by the given expression: +// +// number | number "-" number [ "/" number ] +// +// or error parsing range. +func getRange(expr string, r bounds) (uint64, error) { + var ( + start, end, step uint + rangeAndStep = strings.Split(expr, "/") + lowAndHigh = strings.Split(rangeAndStep[0], "-") + singleDigit = len(lowAndHigh) == 1 + err error + ) + + var extra uint64 + if lowAndHigh[0] == "*" || lowAndHigh[0] == "?" { + start = r.min + end = r.max + extra = starBit + } else { + start, err = parseIntOrName(lowAndHigh[0], r.names) + if err != nil { + return 0, err + } + switch len(lowAndHigh) { + case 1: + end = start + case 2: + end, err = parseIntOrName(lowAndHigh[1], r.names) + if err != nil { + return 0, err + } + default: + return 0, errors.New("too many hyphens: " + expr) + } + } + + switch len(rangeAndStep) { + case 1: + step = 1 + case 2: + step, err = mustParseInt(rangeAndStep[1]) + if err != nil { + return 0, err + } + + // Special handling: "N/step" means "N-max/step". + if singleDigit { + end = r.max + } + if step > 1 { + extra = 0 + } + default: + return 0, errors.New("too many slashes: " + expr) + } + + if start < r.min { + return 0, errors.New("beginning of range below minimum: " + expr) + } + if end > r.max { + return 0, errors.New("end of range above maximum: " + expr) + } + if start > end { + return 0, errors.New("beginning of range after end: " + expr) + } + if step == 0 { + return 0, errors.New("step cannot be zero: " + expr) + } + + return getBits(start, end, step) | extra, nil +} + +// parseIntOrName returns the (possibly-named) integer contained in expr. +func parseIntOrName(expr string, names map[string]uint) (uint, error) { + if names != nil { + if namedInt, ok := names[strings.ToLower(expr)]; ok { + return namedInt, nil + } + } + return mustParseInt(expr) +} + +// mustParseInt parses the given expression as an int or returns an error. +func mustParseInt(expr string) (uint, error) { + num, err := strconv.Atoi(expr) + if err != nil { + return 0, errors.Wrap(err, "failed to parse number") + } + if num < 0 { + return 0, errors.New("number must be positive") + } + + return uint(num), nil +} + +// getBits sets all bits in the range [min, max], modulo the given step size. +func getBits(min, max, step uint) uint64 { + var bits uint64 + + // If step is 1, use shifts. + if step == 1 { + return ^(math.MaxUint64 << (max + 1)) & (math.MaxUint64 << min) + } + + // Else, use a simple loop. + for i := min; i <= max; i += step { + bits |= 1 << i + } + return bits +} + +// all returns all bits within the given bounds. +func all(r bounds) uint64 { + return getBits(r.min, r.max, 1) | starBit +} + +// parseDescriptor returns a predefined schedule for the expression, or error if none matches. +func parseDescriptor(descriptor string, loc *time.Location) (Schedule, error) { + switch descriptor { + case "@yearly", "@annually": + return &SpecSchedule{ + Second: 1 << seconds.min, + Minute: 1 << minutes.min, + Hour: 1 << hours.min, + Dom: 1 << dom.min, + Month: 1 << months.min, + Dow: all(dow), + Location: loc, + }, nil + + case "@monthly": + return &SpecSchedule{ + Second: 1 << seconds.min, + Minute: 1 << minutes.min, + Hour: 1 << hours.min, + Dom: 1 << dom.min, + Month: all(months), + Dow: all(dow), + Location: loc, + }, nil + + case "@weekly": + return &SpecSchedule{ + Second: 1 << seconds.min, + Minute: 1 << minutes.min, + Hour: 1 << hours.min, + Dom: all(dom), + Month: all(months), + Dow: 1 << dow.min, + Location: loc, + }, nil + + case "@daily", "@midnight": + return &SpecSchedule{ + Second: 1 << seconds.min, + Minute: 1 << minutes.min, + Hour: 1 << hours.min, + Dom: all(dom), + Month: all(months), + Dow: all(dow), + Location: loc, + }, nil + + case "@hourly": + return &SpecSchedule{ + Second: 1 << seconds.min, + Minute: 1 << minutes.min, + Hour: all(hours), + Dom: all(dom), + Month: all(months), + Dow: all(dow), + Location: loc, + }, nil + } + + const every = "@every " + if strings.HasPrefix(descriptor, every) { + duration, err := time.ParseDuration(descriptor[len(every):]) + if err != nil { + return nil, errors.Wrap(err, "failed to parse duration") + } + return Every(duration), nil + } + + return nil, errors.New("unrecognized descriptor: " + descriptor) +} diff --git a/plugin/cron/parser_test.go b/plugin/cron/parser_test.go new file mode 100644 index 0000000..fe24703 --- /dev/null +++ b/plugin/cron/parser_test.go @@ -0,0 +1,384 @@ +//nolint:all +package cron + +import ( + "reflect" + "strings" + "testing" + "time" +) + +var secondParser = NewParser(Second | Minute | Hour | Dom | Month | DowOptional | Descriptor) + +func TestRange(t *testing.T) { + zero := uint64(0) + ranges := []struct { + expr string + min, max uint + expected uint64 + err string + }{ + {"5", 0, 7, 1 << 5, ""}, + {"0", 0, 7, 1 << 0, ""}, + {"7", 0, 7, 1 << 7, ""}, + + {"5-5", 0, 7, 1 << 5, ""}, + {"5-6", 0, 7, 1<<5 | 1<<6, ""}, + {"5-7", 0, 7, 1<<5 | 1<<6 | 1<<7, ""}, + + {"5-6/2", 0, 7, 1 << 5, ""}, + {"5-7/2", 0, 7, 1<<5 | 1<<7, ""}, + {"5-7/1", 0, 7, 1<<5 | 1<<6 | 1<<7, ""}, + + {"*", 1, 3, 1<<1 | 1<<2 | 1<<3 | starBit, ""}, + {"*/2", 1, 3, 1<<1 | 1<<3, ""}, + + {"5--5", 0, 0, zero, "too many hyphens"}, + {"jan-x", 0, 0, zero, `failed to parse number: strconv.Atoi: parsing "jan": invalid syntax`}, + {"2-x", 1, 5, zero, `failed to parse number: strconv.Atoi: parsing "x": invalid syntax`}, + {"*/-12", 0, 0, zero, "number must be positive"}, + {"*//2", 0, 0, zero, "too many slashes"}, + {"1", 3, 5, zero, "below minimum"}, + {"6", 3, 5, zero, "above maximum"}, + {"5-3", 3, 5, zero, "beginning of range after end: 5-3"}, + {"*/0", 0, 0, zero, "step cannot be zero: */0"}, + } + + for _, c := range ranges { + actual, err := getRange(c.expr, bounds{c.min, c.max, nil}) + if len(c.err) != 0 && (err == nil || !strings.Contains(err.Error(), c.err)) { + t.Errorf("%s => expected %v, got %v", c.expr, c.err, err) + } + if len(c.err) == 0 && err != nil { + t.Errorf("%s => unexpected error %v", c.expr, err) + } + if actual != c.expected { + t.Errorf("%s => expected %d, got %d", c.expr, c.expected, actual) + } + } +} + +func TestField(t *testing.T) { + fields := []struct { + expr string + min, max uint + expected uint64 + }{ + {"5", 1, 7, 1 << 5}, + {"5,6", 1, 7, 1<<5 | 1<<6}, + {"5,6,7", 1, 7, 1<<5 | 1<<6 | 1<<7}, + {"1,5-7/2,3", 1, 7, 1<<1 | 1<<5 | 1<<7 | 1<<3}, + } + + for _, c := range fields { + actual, _ := getField(c.expr, bounds{c.min, c.max, nil}) + if actual != c.expected { + t.Errorf("%s => expected %d, got %d", c.expr, c.expected, actual) + } + } +} + +func TestAll(t *testing.T) { + allBits := []struct { + r bounds + expected uint64 + }{ + {minutes, 0xfffffffffffffff}, // 0-59: 60 ones + {hours, 0xffffff}, // 0-23: 24 ones + {dom, 0xfffffffe}, // 1-31: 31 ones, 1 zero + {months, 0x1ffe}, // 1-12: 12 ones, 1 zero + {dow, 0x7f}, // 0-6: 7 ones + } + + for _, c := range allBits { + actual := all(c.r) // all() adds the starBit, so compensate for that.. + if c.expected|starBit != actual { + t.Errorf("%d-%d/%d => expected %b, got %b", + c.r.min, c.r.max, 1, c.expected|starBit, actual) + } + } +} + +func TestBits(t *testing.T) { + bits := []struct { + min, max, step uint + expected uint64 + }{ + {0, 0, 1, 0x1}, + {1, 1, 1, 0x2}, + {1, 5, 2, 0x2a}, // 101010 + {1, 4, 2, 0xa}, // 1010 + } + + for _, c := range bits { + actual := getBits(c.min, c.max, c.step) + if c.expected != actual { + t.Errorf("%d-%d/%d => expected %b, got %b", + c.min, c.max, c.step, c.expected, actual) + } + } +} + +func TestParseScheduleErrors(t *testing.T) { + var tests = []struct{ expr, err string }{ + {"* 5 j * * *", `failed to parse number: strconv.Atoi: parsing "j": invalid syntax`}, + {"@every Xm", "failed to parse duration"}, + {"@unrecognized", "unrecognized descriptor"}, + {"* * * *", "incorrect number of fields, expected 5-6"}, + {"", "empty spec string"}, + } + for _, c := range tests { + actual, err := secondParser.Parse(c.expr) + if err == nil || !strings.Contains(err.Error(), c.err) { + t.Errorf("%s => expected %v, got %v", c.expr, c.err, err) + } + if actual != nil { + t.Errorf("expected nil schedule on error, got %v", actual) + } + } +} + +func TestParseSchedule(t *testing.T) { + tokyo, _ := time.LoadLocation("Asia/Tokyo") + entries := []struct { + parser Parser + expr string + expected Schedule + }{ + {secondParser, "0 5 * * * *", every5min(time.Local)}, + {standardParser, "5 * * * *", every5min(time.Local)}, + {secondParser, "CRON_TZ=UTC 0 5 * * * *", every5min(time.UTC)}, + {standardParser, "CRON_TZ=UTC 5 * * * *", every5min(time.UTC)}, + {secondParser, "CRON_TZ=Asia/Tokyo 0 5 * * * *", every5min(tokyo)}, + {secondParser, "@every 5m", ConstantDelaySchedule{5 * time.Minute}}, + {secondParser, "@midnight", midnight(time.Local)}, + {secondParser, "TZ=UTC @midnight", midnight(time.UTC)}, + {secondParser, "TZ=Asia/Tokyo @midnight", midnight(tokyo)}, + {secondParser, "@yearly", annual(time.Local)}, + {secondParser, "@annually", annual(time.Local)}, + { + parser: secondParser, + expr: "* 5 * * * *", + expected: &SpecSchedule{ + Second: all(seconds), + Minute: 1 << 5, + Hour: all(hours), + Dom: all(dom), + Month: all(months), + Dow: all(dow), + Location: time.Local, + }, + }, + } + + for _, c := range entries { + actual, err := c.parser.Parse(c.expr) + if err != nil { + t.Errorf("%s => unexpected error %v", c.expr, err) + } + if !reflect.DeepEqual(actual, c.expected) { + t.Errorf("%s => expected %b, got %b", c.expr, c.expected, actual) + } + } +} + +func TestOptionalSecondSchedule(t *testing.T) { + parser := NewParser(SecondOptional | Minute | Hour | Dom | Month | Dow | Descriptor) + entries := []struct { + expr string + expected Schedule + }{ + {"0 5 * * * *", every5min(time.Local)}, + {"5 5 * * * *", every5min5s(time.Local)}, + {"5 * * * *", every5min(time.Local)}, + } + + for _, c := range entries { + actual, err := parser.Parse(c.expr) + if err != nil { + t.Errorf("%s => unexpected error %v", c.expr, err) + } + if !reflect.DeepEqual(actual, c.expected) { + t.Errorf("%s => expected %b, got %b", c.expr, c.expected, actual) + } + } +} + +func TestNormalizeFields(t *testing.T) { + tests := []struct { + name string + input []string + options ParseOption + expected []string + }{ + { + "AllFields_NoOptional", + []string{"0", "5", "*", "*", "*", "*"}, + Second | Minute | Hour | Dom | Month | Dow | Descriptor, + []string{"0", "5", "*", "*", "*", "*"}, + }, + { + "AllFields_SecondOptional_Provided", + []string{"0", "5", "*", "*", "*", "*"}, + SecondOptional | Minute | Hour | Dom | Month | Dow | Descriptor, + []string{"0", "5", "*", "*", "*", "*"}, + }, + { + "AllFields_SecondOptional_NotProvided", + []string{"5", "*", "*", "*", "*"}, + SecondOptional | Minute | Hour | Dom | Month | Dow | Descriptor, + []string{"0", "5", "*", "*", "*", "*"}, + }, + { + "SubsetFields_NoOptional", + []string{"5", "15", "*"}, + Hour | Dom | Month, + []string{"0", "0", "5", "15", "*", "*"}, + }, + { + "SubsetFields_DowOptional_Provided", + []string{"5", "15", "*", "4"}, + Hour | Dom | Month | DowOptional, + []string{"0", "0", "5", "15", "*", "4"}, + }, + { + "SubsetFields_DowOptional_NotProvided", + []string{"5", "15", "*"}, + Hour | Dom | Month | DowOptional, + []string{"0", "0", "5", "15", "*", "*"}, + }, + { + "SubsetFields_SecondOptional_NotProvided", + []string{"5", "15", "*"}, + SecondOptional | Hour | Dom | Month, + []string{"0", "0", "5", "15", "*", "*"}, + }, + } + + for _, test := range tests { + t.Run(test.name, func(*testing.T) { + actual, err := normalizeFields(test.input, test.options) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if !reflect.DeepEqual(actual, test.expected) { + t.Errorf("expected %v, got %v", test.expected, actual) + } + }) + } +} + +func TestNormalizeFields_Errors(t *testing.T) { + tests := []struct { + name string + input []string + options ParseOption + err string + }{ + { + "TwoOptionals", + []string{"0", "5", "*", "*", "*", "*"}, + SecondOptional | Minute | Hour | Dom | Month | DowOptional, + "", + }, + { + "TooManyFields", + []string{"0", "5", "*", "*"}, + SecondOptional | Minute | Hour, + "", + }, + { + "NoFields", + []string{}, + SecondOptional | Minute | Hour, + "", + }, + { + "TooFewFields", + []string{"*"}, + SecondOptional | Minute | Hour, + "", + }, + } + for _, test := range tests { + t.Run(test.name, func(*testing.T) { + actual, err := normalizeFields(test.input, test.options) + if err == nil { + t.Errorf("expected an error, got none. results: %v", actual) + } + if !strings.Contains(err.Error(), test.err) { + t.Errorf("expected error %q, got %q", test.err, err.Error()) + } + }) + } +} + +func TestStandardSpecSchedule(t *testing.T) { + entries := []struct { + expr string + expected Schedule + err string + }{ + { + expr: "5 * * * *", + expected: &SpecSchedule{1 << seconds.min, 1 << 5, all(hours), all(dom), all(months), all(dow), time.Local}, + }, + { + expr: "@every 5m", + expected: ConstantDelaySchedule{time.Duration(5) * time.Minute}, + }, + { + expr: "5 j * * *", + err: `failed to parse number: strconv.Atoi: parsing "j": invalid syntax`, + }, + { + expr: "* * * *", + err: "incorrect number of fields", + }, + } + + for _, c := range entries { + actual, err := ParseStandard(c.expr) + if len(c.err) != 0 && (err == nil || !strings.Contains(err.Error(), c.err)) { + t.Errorf("%s => expected %v, got %v", c.expr, c.err, err) + } + if len(c.err) == 0 && err != nil { + t.Errorf("%s => unexpected error %v", c.expr, err) + } + if !reflect.DeepEqual(actual, c.expected) { + t.Errorf("%s => expected %b, got %b", c.expr, c.expected, actual) + } + } +} + +func TestNoDescriptorParser(t *testing.T) { + parser := NewParser(Minute | Hour) + _, err := parser.Parse("@every 1m") + if err == nil { + t.Error("expected an error, got none") + } +} + +func every5min(loc *time.Location) *SpecSchedule { + return &SpecSchedule{1 << 0, 1 << 5, all(hours), all(dom), all(months), all(dow), loc} +} + +func every5min5s(loc *time.Location) *SpecSchedule { + return &SpecSchedule{1 << 5, 1 << 5, all(hours), all(dom), all(months), all(dow), loc} +} + +func midnight(loc *time.Location) *SpecSchedule { + return &SpecSchedule{1, 1, 1, all(dom), all(months), all(dow), loc} +} + +func annual(loc *time.Location) *SpecSchedule { + return &SpecSchedule{ + Second: 1 << seconds.min, + Minute: 1 << minutes.min, + Hour: 1 << hours.min, + Dom: 1 << dom.min, + Month: 1 << months.min, + Dow: all(dow), + Location: loc, + } +} diff --git a/plugin/cron/spec.go b/plugin/cron/spec.go new file mode 100644 index 0000000..9821a6a --- /dev/null +++ b/plugin/cron/spec.go @@ -0,0 +1,188 @@ +package cron + +import "time" + +// SpecSchedule specifies a duty cycle (to the second granularity), based on a +// traditional crontab specification. It is computed initially and stored as bit sets. +type SpecSchedule struct { + Second, Minute, Hour, Dom, Month, Dow uint64 + + // Override location for this schedule. + Location *time.Location +} + +// bounds provides a range of acceptable values (plus a map of name to value). +type bounds struct { + min, max uint + names map[string]uint +} + +// The bounds for each field. +var ( + seconds = bounds{0, 59, nil} + minutes = bounds{0, 59, nil} + hours = bounds{0, 23, nil} + dom = bounds{1, 31, nil} + months = bounds{1, 12, map[string]uint{ + "jan": 1, + "feb": 2, + "mar": 3, + "apr": 4, + "may": 5, + "jun": 6, + "jul": 7, + "aug": 8, + "sep": 9, + "oct": 10, + "nov": 11, + "dec": 12, + }} + dow = bounds{0, 6, map[string]uint{ + "sun": 0, + "mon": 1, + "tue": 2, + "wed": 3, + "thu": 4, + "fri": 5, + "sat": 6, + }} +) + +const ( + // Set the top bit if a star was included in the expression. + starBit = 1 << 63 +) + +// Next returns the next time this schedule is activated, greater than the given +// time. If no time can be found to satisfy the schedule, return the zero time. +func (s *SpecSchedule) Next(t time.Time) time.Time { + // General approach + // + // For Month, Day, Hour, Minute, Second: + // Check if the time value matches. If yes, continue to the next field. + // If the field doesn't match the schedule, then increment the field until it matches. + // While incrementing the field, a wrap-around brings it back to the beginning + // of the field list (since it is necessary to re-verify previous field + // values) + + // Convert the given time into the schedule's timezone, if one is specified. + // Save the original timezone so we can convert back after we find a time. + // Note that schedules without a time zone specified (time.Local) are treated + // as local to the time provided. + origLocation := t.Location() + loc := s.Location + if loc == time.Local { + loc = t.Location() + } + if s.Location != time.Local { + t = t.In(s.Location) + } + + // Start at the earliest possible time (the upcoming second). + t = t.Add(1*time.Second - time.Duration(t.Nanosecond())*time.Nanosecond) + + // This flag indicates whether a field has been incremented. + added := false + + // If no time is found within five years, return zero. + yearLimit := t.Year() + 5 + +WRAP: + if t.Year() > yearLimit { + return time.Time{} + } + + // Find the first applicable month. + // If it's this month, then do nothing. + for 1< 12 { + t = t.Add(time.Duration(24-t.Hour()) * time.Hour) + } else { + t = t.Add(time.Duration(-t.Hour()) * time.Hour) + } + } + + if t.Day() == 1 { + goto WRAP + } + } + + for 1< 0 + dowMatch = 1< 0 + ) + if s.Dom&starBit > 0 || s.Dow&starBit > 0 { + return domMatch && dowMatch + } + return domMatch || dowMatch +} diff --git a/plugin/cron/spec_test.go b/plugin/cron/spec_test.go new file mode 100644 index 0000000..f5109ea --- /dev/null +++ b/plugin/cron/spec_test.go @@ -0,0 +1,301 @@ +//nolint:all +package cron + +import ( + "strings" + "testing" + "time" +) + +func TestActivation(t *testing.T) { + tests := []struct { + time, spec string + expected bool + }{ + // Every fifteen minutes. + {"Mon Jul 9 15:00 2012", "0/15 * * * *", true}, + {"Mon Jul 9 15:45 2012", "0/15 * * * *", true}, + {"Mon Jul 9 15:40 2012", "0/15 * * * *", false}, + + // Every fifteen minutes, starting at 5 minutes. + {"Mon Jul 9 15:05 2012", "5/15 * * * *", true}, + {"Mon Jul 9 15:20 2012", "5/15 * * * *", true}, + {"Mon Jul 9 15:50 2012", "5/15 * * * *", true}, + + // Named months + {"Sun Jul 15 15:00 2012", "0/15 * * Jul *", true}, + {"Sun Jul 15 15:00 2012", "0/15 * * Jun *", false}, + + // Everything set. + {"Sun Jul 15 08:30 2012", "30 08 ? Jul Sun", true}, + {"Sun Jul 15 08:30 2012", "30 08 15 Jul ?", true}, + {"Mon Jul 16 08:30 2012", "30 08 ? Jul Sun", false}, + {"Mon Jul 16 08:30 2012", "30 08 15 Jul ?", false}, + + // Predefined schedules + {"Mon Jul 9 15:00 2012", "@hourly", true}, + {"Mon Jul 9 15:04 2012", "@hourly", false}, + {"Mon Jul 9 15:00 2012", "@daily", false}, + {"Mon Jul 9 00:00 2012", "@daily", true}, + {"Mon Jul 9 00:00 2012", "@weekly", false}, + {"Sun Jul 8 00:00 2012", "@weekly", true}, + {"Sun Jul 8 01:00 2012", "@weekly", false}, + {"Sun Jul 8 00:00 2012", "@monthly", false}, + {"Sun Jul 1 00:00 2012", "@monthly", true}, + + // Test interaction of DOW and DOM. + // If both are restricted, then only one needs to match. + {"Sun Jul 15 00:00 2012", "* * 1,15 * Sun", true}, + {"Fri Jun 15 00:00 2012", "* * 1,15 * Sun", true}, + {"Wed Aug 1 00:00 2012", "* * 1,15 * Sun", true}, + {"Sun Jul 15 00:00 2012", "* * */10 * Sun", true}, // verifies #70 + + // However, if one has a star, then both need to match. + {"Sun Jul 15 00:00 2012", "* * * * Mon", false}, + {"Mon Jul 9 00:00 2012", "* * 1,15 * *", false}, + {"Sun Jul 15 00:00 2012", "* * 1,15 * *", true}, + {"Sun Jul 15 00:00 2012", "* * */2 * Sun", true}, + } + + for _, test := range tests { + sched, err := ParseStandard(test.spec) + if err != nil { + t.Error(err) + continue + } + actual := sched.Next(getTime(test.time).Add(-1 * time.Second)) + expected := getTime(test.time) + if test.expected && expected != actual || !test.expected && expected == actual { + t.Errorf("Fail evaluating %s on %s: (expected) %s != %s (actual)", + test.spec, test.time, expected, actual) + } + } +} + +func TestNext(t *testing.T) { + runs := []struct { + time, spec string + expected string + }{ + // Simple cases + {"Mon Jul 9 14:45 2012", "0 0/15 * * * *", "Mon Jul 9 15:00 2012"}, + {"Mon Jul 9 14:59 2012", "0 0/15 * * * *", "Mon Jul 9 15:00 2012"}, + {"Mon Jul 9 14:59:59 2012", "0 0/15 * * * *", "Mon Jul 9 15:00 2012"}, + + // Wrap around hours + {"Mon Jul 9 15:45 2012", "0 20-35/15 * * * *", "Mon Jul 9 16:20 2012"}, + + // Wrap around days + {"Mon Jul 9 23:46 2012", "0 */15 * * * *", "Tue Jul 10 00:00 2012"}, + {"Mon Jul 9 23:45 2012", "0 20-35/15 * * * *", "Tue Jul 10 00:20 2012"}, + {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * * * *", "Tue Jul 10 00:20:15 2012"}, + {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 1/2 * * *", "Tue Jul 10 01:20:15 2012"}, + {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 10-12 * * *", "Tue Jul 10 10:20:15 2012"}, + + {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 1/2 */2 * *", "Thu Jul 11 01:20:15 2012"}, + {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * 9-20 * *", "Wed Jul 10 00:20:15 2012"}, + {"Mon Jul 9 23:35:51 2012", "15/35 20-35/15 * 9-20 Jul *", "Wed Jul 10 00:20:15 2012"}, + + // Wrap around months + {"Mon Jul 9 23:35 2012", "0 0 0 9 Apr-Oct ?", "Thu Aug 9 00:00 2012"}, + {"Mon Jul 9 23:35 2012", "0 0 0 */5 Apr,Aug,Oct Mon", "Tue Aug 1 00:00 2012"}, + {"Mon Jul 9 23:35 2012", "0 0 0 */5 Oct Mon", "Mon Oct 1 00:00 2012"}, + + // Wrap around years + {"Mon Jul 9 23:35 2012", "0 0 0 * Feb Mon", "Mon Feb 4 00:00 2013"}, + {"Mon Jul 9 23:35 2012", "0 0 0 * Feb Mon/2", "Fri Feb 1 00:00 2013"}, + + // Wrap around minute, hour, day, month, and year + {"Mon Dec 31 23:59:45 2012", "0 * * * * *", "Tue Jan 1 00:00:00 2013"}, + + // Leap year + {"Mon Jul 9 23:35 2012", "0 0 0 29 Feb ?", "Mon Feb 29 00:00 2016"}, + + // Daylight savings time 2am EST (-5) -> 3am EDT (-4) + {"2012-03-11T00:00:00-0500", "TZ=America/New_York 0 30 2 11 Mar ?", "2013-03-11T02:30:00-0400"}, + + // hourly job + {"2012-03-11T00:00:00-0500", "TZ=America/New_York 0 0 * * * ?", "2012-03-11T01:00:00-0500"}, + {"2012-03-11T01:00:00-0500", "TZ=America/New_York 0 0 * * * ?", "2012-03-11T03:00:00-0400"}, + {"2012-03-11T03:00:00-0400", "TZ=America/New_York 0 0 * * * ?", "2012-03-11T04:00:00-0400"}, + {"2012-03-11T04:00:00-0400", "TZ=America/New_York 0 0 * * * ?", "2012-03-11T05:00:00-0400"}, + + // hourly job using CRON_TZ + {"2012-03-11T00:00:00-0500", "CRON_TZ=America/New_York 0 0 * * * ?", "2012-03-11T01:00:00-0500"}, + {"2012-03-11T01:00:00-0500", "CRON_TZ=America/New_York 0 0 * * * ?", "2012-03-11T03:00:00-0400"}, + {"2012-03-11T03:00:00-0400", "CRON_TZ=America/New_York 0 0 * * * ?", "2012-03-11T04:00:00-0400"}, + {"2012-03-11T04:00:00-0400", "CRON_TZ=America/New_York 0 0 * * * ?", "2012-03-11T05:00:00-0400"}, + + // 1am nightly job + {"2012-03-11T00:00:00-0500", "TZ=America/New_York 0 0 1 * * ?", "2012-03-11T01:00:00-0500"}, + {"2012-03-11T01:00:00-0500", "TZ=America/New_York 0 0 1 * * ?", "2012-03-12T01:00:00-0400"}, + + // 2am nightly job (skipped) + {"2012-03-11T00:00:00-0500", "TZ=America/New_York 0 0 2 * * ?", "2012-03-12T02:00:00-0400"}, + + // Daylight savings time 2am EDT (-4) => 1am EST (-5) + {"2012-11-04T00:00:00-0400", "TZ=America/New_York 0 30 2 04 Nov ?", "2012-11-04T02:30:00-0500"}, + {"2012-11-04T01:45:00-0400", "TZ=America/New_York 0 30 1 04 Nov ?", "2012-11-04T01:30:00-0500"}, + + // hourly job + {"2012-11-04T00:00:00-0400", "TZ=America/New_York 0 0 * * * ?", "2012-11-04T01:00:00-0400"}, + {"2012-11-04T01:00:00-0400", "TZ=America/New_York 0 0 * * * ?", "2012-11-04T01:00:00-0500"}, + {"2012-11-04T01:00:00-0500", "TZ=America/New_York 0 0 * * * ?", "2012-11-04T02:00:00-0500"}, + + // 1am nightly job (runs twice) + {"2012-11-04T00:00:00-0400", "TZ=America/New_York 0 0 1 * * ?", "2012-11-04T01:00:00-0400"}, + {"2012-11-04T01:00:00-0400", "TZ=America/New_York 0 0 1 * * ?", "2012-11-04T01:00:00-0500"}, + {"2012-11-04T01:00:00-0500", "TZ=America/New_York 0 0 1 * * ?", "2012-11-05T01:00:00-0500"}, + + // 2am nightly job + {"2012-11-04T00:00:00-0400", "TZ=America/New_York 0 0 2 * * ?", "2012-11-04T02:00:00-0500"}, + {"2012-11-04T02:00:00-0500", "TZ=America/New_York 0 0 2 * * ?", "2012-11-05T02:00:00-0500"}, + + // 3am nightly job + {"2012-11-04T00:00:00-0400", "TZ=America/New_York 0 0 3 * * ?", "2012-11-04T03:00:00-0500"}, + {"2012-11-04T03:00:00-0500", "TZ=America/New_York 0 0 3 * * ?", "2012-11-05T03:00:00-0500"}, + + // hourly job + {"TZ=America/New_York 2012-11-04T00:00:00-0400", "0 0 * * * ?", "2012-11-04T01:00:00-0400"}, + {"TZ=America/New_York 2012-11-04T01:00:00-0400", "0 0 * * * ?", "2012-11-04T01:00:00-0500"}, + {"TZ=America/New_York 2012-11-04T01:00:00-0500", "0 0 * * * ?", "2012-11-04T02:00:00-0500"}, + + // 1am nightly job (runs twice) + {"TZ=America/New_York 2012-11-04T00:00:00-0400", "0 0 1 * * ?", "2012-11-04T01:00:00-0400"}, + {"TZ=America/New_York 2012-11-04T01:00:00-0400", "0 0 1 * * ?", "2012-11-04T01:00:00-0500"}, + {"TZ=America/New_York 2012-11-04T01:00:00-0500", "0 0 1 * * ?", "2012-11-05T01:00:00-0500"}, + + // 2am nightly job + {"TZ=America/New_York 2012-11-04T00:00:00-0400", "0 0 2 * * ?", "2012-11-04T02:00:00-0500"}, + {"TZ=America/New_York 2012-11-04T02:00:00-0500", "0 0 2 * * ?", "2012-11-05T02:00:00-0500"}, + + // 3am nightly job + {"TZ=America/New_York 2012-11-04T00:00:00-0400", "0 0 3 * * ?", "2012-11-04T03:00:00-0500"}, + {"TZ=America/New_York 2012-11-04T03:00:00-0500", "0 0 3 * * ?", "2012-11-05T03:00:00-0500"}, + + // Unsatisfiable + {"Mon Jul 9 23:35 2012", "0 0 0 30 Feb ?", ""}, + {"Mon Jul 9 23:35 2012", "0 0 0 31 Apr ?", ""}, + + // Monthly job + {"TZ=America/New_York 2012-11-04T00:00:00-0400", "0 0 3 3 * ?", "2012-12-03T03:00:00-0500"}, + + // Test the scenario of DST resulting in midnight not being a valid time. + // https://github.com/robfig/cron/issues/157 + {"2018-10-17T05:00:00-0400", "TZ=America/Sao_Paulo 0 0 9 10 * ?", "2018-11-10T06:00:00-0500"}, + {"2018-02-14T05:00:00-0500", "TZ=America/Sao_Paulo 0 0 9 22 * ?", "2018-02-22T07:00:00-0500"}, + } + + for _, c := range runs { + sched, err := secondParser.Parse(c.spec) + if err != nil { + t.Error(err) + continue + } + actual := sched.Next(getTime(c.time)) + expected := getTime(c.expected) + if !actual.Equal(expected) { + t.Errorf("%s, \"%s\": (expected) %v != %v (actual)", c.time, c.spec, expected, actual) + } + } +} + +func TestErrors(t *testing.T) { + invalidSpecs := []string{ + "xyz", + "60 0 * * *", + "0 60 * * *", + "0 0 * * XYZ", + } + for _, spec := range invalidSpecs { + _, err := ParseStandard(spec) + if err == nil { + t.Error("expected an error parsing: ", spec) + } + } +} + +func getTime(value string) time.Time { + if value == "" { + return time.Time{} + } + + var location = time.Local + if strings.HasPrefix(value, "TZ=") { + parts := strings.Fields(value) + loc, err := time.LoadLocation(parts[0][len("TZ="):]) + if err != nil { + panic("could not parse location:" + err.Error()) + } + location = loc + value = parts[1] + } + + var layouts = []string{ + "Mon Jan 2 15:04 2006", + "Mon Jan 2 15:04:05 2006", + } + for _, layout := range layouts { + if t, err := time.ParseInLocation(layout, value, location); err == nil { + return t + } + } + if t, err := time.ParseInLocation("2006-01-02T15:04:05-0700", value, location); err == nil { + return t + } + panic("could not parse time value " + value) +} + +func TestNextWithTz(t *testing.T) { + runs := []struct { + time, spec string + expected string + }{ + // Failing tests + {"2016-01-03T13:09:03+0530", "14 14 * * *", "2016-01-03T14:14:00+0530"}, + {"2016-01-03T04:09:03+0530", "14 14 * * ?", "2016-01-03T14:14:00+0530"}, + + // Passing tests + {"2016-01-03T14:09:03+0530", "14 14 * * *", "2016-01-03T14:14:00+0530"}, + {"2016-01-03T14:00:00+0530", "14 14 * * ?", "2016-01-03T14:14:00+0530"}, + } + for _, c := range runs { + sched, err := ParseStandard(c.spec) + if err != nil { + t.Error(err) + continue + } + actual := sched.Next(getTimeTZ(c.time)) + expected := getTimeTZ(c.expected) + if !actual.Equal(expected) { + t.Errorf("%s, \"%s\": (expected) %v != %v (actual)", c.time, c.spec, expected, actual) + } + } +} + +func getTimeTZ(value string) time.Time { + if value == "" { + return time.Time{} + } + t, err := time.Parse("Mon Jan 2 15:04 2006", value) + if err != nil { + t, err = time.Parse("Mon Jan 2 15:04:05 2006", value) + if err != nil { + t, err = time.Parse("2006-01-02T15:04:05-0700", value) + if err != nil { + panic(err) + } + } + } + + return t +} + +// https://github.com/robfig/cron/issues/144 +func TestSlash0NoHang(t *testing.T) { + schedule := "TZ=America/New_York 15/0 * * * *" + _, err := ParseStandard(schedule) + if err == nil { + t.Error("expected an error on 0 increment") + } +} diff --git a/plugin/filter/common_converter.go b/plugin/filter/common_converter.go new file mode 100644 index 0000000..407e4d9 --- /dev/null +++ b/plugin/filter/common_converter.go @@ -0,0 +1,448 @@ +package filter + +import ( + "fmt" + "slices" + "strings" + + "github.com/pkg/errors" + exprv1 "google.golang.org/genproto/googleapis/api/expr/v1alpha1" +) + +// CommonSQLConverter handles the common CEL to SQL conversion logic. +type CommonSQLConverter struct { + dialect SQLDialect + paramIndex int +} + +// NewCommonSQLConverter creates a new converter with the specified dialect. +func NewCommonSQLConverter(dialect SQLDialect) *CommonSQLConverter { + return &CommonSQLConverter{ + dialect: dialect, + paramIndex: 1, + } +} + +// ConvertExprToSQL converts a CEL expression to SQL using the configured dialect. +func (c *CommonSQLConverter) ConvertExprToSQL(ctx *ConvertContext, expr *exprv1.Expr) error { + if v, ok := expr.ExprKind.(*exprv1.Expr_CallExpr); ok { + switch v.CallExpr.Function { + case "_||_", "_&&_": + return c.handleLogicalOperator(ctx, v.CallExpr) + case "!_": + return c.handleNotOperator(ctx, v.CallExpr) + case "_==_", "_!=_", "_<_", "_>_", "_<=_", "_>=_": + return c.handleComparisonOperator(ctx, v.CallExpr) + case "@in": + return c.handleInOperator(ctx, v.CallExpr) + case "contains": + return c.handleContainsOperator(ctx, v.CallExpr) + } + } else if v, ok := expr.ExprKind.(*exprv1.Expr_IdentExpr); ok { + return c.handleIdentifier(ctx, v.IdentExpr) + } + return nil +} + +func (c *CommonSQLConverter) handleLogicalOperator(ctx *ConvertContext, callExpr *exprv1.Expr_Call) error { + if len(callExpr.Args) != 2 { + return errors.Errorf("invalid number of arguments for %s", callExpr.Function) + } + + if _, err := ctx.Buffer.WriteString("("); err != nil { + return err + } + + if err := c.ConvertExprToSQL(ctx, callExpr.Args[0]); err != nil { + return err + } + + operator := "AND" + if callExpr.Function == "_||_" { + operator = "OR" + } + + if _, err := ctx.Buffer.WriteString(fmt.Sprintf(" %s ", operator)); err != nil { + return err + } + + if err := c.ConvertExprToSQL(ctx, callExpr.Args[1]); err != nil { + return err + } + + if _, err := ctx.Buffer.WriteString(")"); err != nil { + return err + } + + return nil +} + +func (c *CommonSQLConverter) handleNotOperator(ctx *ConvertContext, callExpr *exprv1.Expr_Call) error { + if len(callExpr.Args) != 1 { + return errors.Errorf("invalid number of arguments for %s", callExpr.Function) + } + + if _, err := ctx.Buffer.WriteString("NOT ("); err != nil { + return err + } + + if err := c.ConvertExprToSQL(ctx, callExpr.Args[0]); err != nil { + return err + } + + if _, err := ctx.Buffer.WriteString(")"); err != nil { + return err + } + + return nil +} + +func (c *CommonSQLConverter) handleComparisonOperator(ctx *ConvertContext, callExpr *exprv1.Expr_Call) error { + if len(callExpr.Args) != 2 { + return errors.Errorf("invalid number of arguments for %s", callExpr.Function) + } + + // Check if the left side is a function call like size(tags) + if leftCallExpr, ok := callExpr.Args[0].ExprKind.(*exprv1.Expr_CallExpr); ok { + if leftCallExpr.CallExpr.Function == "size" { + return c.handleSizeComparison(ctx, callExpr, leftCallExpr.CallExpr) + } + } + + identifier, err := GetIdentExprName(callExpr.Args[0]) + if err != nil { + return err + } + + if !slices.Contains([]string{"creator_id", "created_ts", "updated_ts", "visibility", "content", "has_task_list"}, identifier) { + return errors.Errorf("invalid identifier for %s", callExpr.Function) + } + + value, err := GetExprValue(callExpr.Args[1]) + if err != nil { + return err + } + + operator := c.getComparisonOperator(callExpr.Function) + + switch identifier { + case "created_ts", "updated_ts": + return c.handleTimestampComparison(ctx, identifier, operator, value) + case "visibility", "content": + return c.handleStringComparison(ctx, identifier, operator, value) + case "creator_id": + return c.handleIntComparison(ctx, identifier, operator, value) + case "has_task_list": + return c.handleBooleanComparison(ctx, identifier, operator, value) + } + + return nil +} + +func (c *CommonSQLConverter) handleSizeComparison(ctx *ConvertContext, callExpr *exprv1.Expr_Call, sizeCall *exprv1.Expr_Call) error { + if len(sizeCall.Args) != 1 { + return errors.New("size function requires exactly one argument") + } + + identifier, err := GetIdentExprName(sizeCall.Args[0]) + if err != nil { + return err + } + + if identifier != "tags" { + return errors.Errorf("size function only supports 'tags' identifier, got: %s", identifier) + } + + value, err := GetExprValue(callExpr.Args[1]) + if err != nil { + return err + } + + valueInt, ok := value.(int64) + if !ok { + return errors.New("size comparison value must be an integer") + } + + operator := c.getComparisonOperator(callExpr.Function) + + if _, err := ctx.Buffer.WriteString(fmt.Sprintf("%s %s %s", + c.dialect.GetJSONArrayLength("$.tags"), + operator, + c.dialect.GetParameterPlaceholder(c.paramIndex))); err != nil { + return err + } + + ctx.Args = append(ctx.Args, valueInt) + c.paramIndex++ + + return nil +} + +func (c *CommonSQLConverter) handleInOperator(ctx *ConvertContext, callExpr *exprv1.Expr_Call) error { + if len(callExpr.Args) != 2 { + return errors.Errorf("invalid number of arguments for %s", callExpr.Function) + } + + // Check if this is "element in collection" syntax + if identifier, err := GetIdentExprName(callExpr.Args[1]); err == nil { + if identifier == "tags" { + return c.handleElementInTags(ctx, callExpr.Args[0]) + } + return errors.Errorf("invalid collection identifier for %s: %s", callExpr.Function, identifier) + } + + // Original logic for "identifier in [list]" syntax + identifier, err := GetIdentExprName(callExpr.Args[0]) + if err != nil { + return err + } + + if !slices.Contains([]string{"tag", "visibility"}, identifier) { + return errors.Errorf("invalid identifier for %s", callExpr.Function) + } + + values := []any{} + for _, element := range callExpr.Args[1].GetListExpr().Elements { + value, err := GetConstValue(element) + if err != nil { + return err + } + values = append(values, value) + } + + if identifier == "tag" { + return c.handleTagInList(ctx, values) + } else if identifier == "visibility" { + return c.handleVisibilityInList(ctx, values) + } + + return nil +} + +func (c *CommonSQLConverter) handleElementInTags(ctx *ConvertContext, elementExpr *exprv1.Expr) error { + element, err := GetConstValue(elementExpr) + if err != nil { + return errors.Errorf("first argument must be a constant value for 'element in tags': %v", err) + } + + // Use dialect-specific JSON contains logic + sqlExpr := c.dialect.GetJSONContains("$.tags", "element") + if _, err := ctx.Buffer.WriteString(sqlExpr); err != nil { + return err + } + + // For SQLite, we need a different approach since it uses LIKE + if _, ok := c.dialect.(*SQLiteDialect); ok { + ctx.Args = append(ctx.Args, fmt.Sprintf(`%%"%s"%%`, element)) + } else { + ctx.Args = append(ctx.Args, element) + } + c.paramIndex++ + + return nil +} + +func (c *CommonSQLConverter) handleTagInList(ctx *ConvertContext, values []any) error { + subconditions := []string{} + args := []any{} + + for _, v := range values { + if _, ok := c.dialect.(*SQLiteDialect); ok { + subconditions = append(subconditions, c.dialect.GetJSONLike("$.tags", "pattern")) + args = append(args, fmt.Sprintf(`%%"%s"%%`, v)) + } else { + subconditions = append(subconditions, c.dialect.GetJSONContains("$.tags", "element")) + args = append(args, v) + } + c.paramIndex++ + } + + if len(subconditions) == 1 { + if _, err := ctx.Buffer.WriteString(subconditions[0]); err != nil { + return err + } + } else { + if _, err := ctx.Buffer.WriteString(fmt.Sprintf("(%s)", strings.Join(subconditions, " OR "))); err != nil { + return err + } + } + + ctx.Args = append(ctx.Args, args...) + return nil +} + +func (c *CommonSQLConverter) handleVisibilityInList(ctx *ConvertContext, values []any) error { + placeholders := []string{} + for range values { + placeholders = append(placeholders, c.dialect.GetParameterPlaceholder(c.paramIndex)) + c.paramIndex++ + } + + tablePrefix := c.dialect.GetTablePrefix() + if _, err := ctx.Buffer.WriteString(fmt.Sprintf("%s.`visibility` IN (%s)", tablePrefix, strings.Join(placeholders, ","))); err != nil { + return err + } + + ctx.Args = append(ctx.Args, values...) + return nil +} + +func (c *CommonSQLConverter) handleContainsOperator(ctx *ConvertContext, callExpr *exprv1.Expr_Call) error { + if len(callExpr.Args) != 1 { + return errors.Errorf("invalid number of arguments for %s", callExpr.Function) + } + + identifier, err := GetIdentExprName(callExpr.Target) + if err != nil { + return err + } + + if identifier != "content" { + return errors.Errorf("invalid identifier for %s", callExpr.Function) + } + + arg, err := GetConstValue(callExpr.Args[0]) + if err != nil { + return err + } + + tablePrefix := c.dialect.GetTablePrefix() + if _, err := ctx.Buffer.WriteString(fmt.Sprintf("%s.`content` LIKE %s", tablePrefix, c.dialect.GetParameterPlaceholder(c.paramIndex))); err != nil { + return err + } + + ctx.Args = append(ctx.Args, fmt.Sprintf("%%%s%%", arg)) + c.paramIndex++ + + return nil +} + +func (c *CommonSQLConverter) handleIdentifier(ctx *ConvertContext, identExpr *exprv1.Expr_Ident) error { + identifier := identExpr.GetName() + + if !slices.Contains([]string{"pinned", "has_task_list"}, identifier) { + return errors.Errorf("invalid identifier %s", identifier) + } + + if identifier == "pinned" { + tablePrefix := c.dialect.GetTablePrefix() + if _, err := ctx.Buffer.WriteString(fmt.Sprintf("%s.`pinned` IS TRUE", tablePrefix)); err != nil { + return err + } + } else if identifier == "has_task_list" { + if _, err := ctx.Buffer.WriteString(c.dialect.GetBooleanCheck("$.property.hasTaskList")); err != nil { + return err + } + } + + return nil +} + +func (c *CommonSQLConverter) handleTimestampComparison(ctx *ConvertContext, field, operator string, value interface{}) error { + valueInt, ok := value.(int64) + if !ok { + return errors.New("invalid integer timestamp value") + } + + timestampField := c.dialect.GetTimestampComparison(field) + if _, err := ctx.Buffer.WriteString(fmt.Sprintf("%s %s %s", timestampField, operator, c.dialect.GetParameterPlaceholder(c.paramIndex))); err != nil { + return err + } + + ctx.Args = append(ctx.Args, valueInt) + c.paramIndex++ + + return nil +} + +func (c *CommonSQLConverter) handleStringComparison(ctx *ConvertContext, field, operator string, value interface{}) error { + if operator != "=" && operator != "!=" { + return errors.Errorf("invalid operator for %s", field) + } + + valueStr, ok := value.(string) + if !ok { + return errors.New("invalid string value") + } + + tablePrefix := c.dialect.GetTablePrefix() + fieldName := field + if field == "visibility" { + fieldName = "`visibility`" + } else if field == "content" { + fieldName = "`content`" + } + + if _, err := ctx.Buffer.WriteString(fmt.Sprintf("%s.%s %s %s", tablePrefix, fieldName, operator, c.dialect.GetParameterPlaceholder(c.paramIndex))); err != nil { + return err + } + + ctx.Args = append(ctx.Args, valueStr) + c.paramIndex++ + + return nil +} + +func (c *CommonSQLConverter) handleIntComparison(ctx *ConvertContext, field, operator string, value interface{}) error { + if operator != "=" && operator != "!=" { + return errors.Errorf("invalid operator for %s", field) + } + + valueInt, ok := value.(int64) + if !ok { + return errors.New("invalid int value") + } + + tablePrefix := c.dialect.GetTablePrefix() + if _, err := ctx.Buffer.WriteString(fmt.Sprintf("%s.`%s` %s %s", tablePrefix, field, operator, c.dialect.GetParameterPlaceholder(c.paramIndex))); err != nil { + return err + } + + ctx.Args = append(ctx.Args, valueInt) + c.paramIndex++ + + return nil +} + +func (c *CommonSQLConverter) handleBooleanComparison(ctx *ConvertContext, field, operator string, value interface{}) error { + if operator != "=" && operator != "!=" { + return errors.Errorf("invalid operator for %s", field) + } + + valueBool, ok := value.(bool) + if !ok { + return errors.New("invalid boolean value for has_task_list") + } + + sqlExpr := c.dialect.GetBooleanComparison("$.property.hasTaskList", valueBool) + if _, err := ctx.Buffer.WriteString(sqlExpr); err != nil { + return err + } + + // For dialects that need parameters (PostgreSQL) + if _, ok := c.dialect.(*PostgreSQLDialect); ok { + ctx.Args = append(ctx.Args, valueBool) + c.paramIndex++ + } + + return nil +} + +func (*CommonSQLConverter) getComparisonOperator(function string) string { + switch function { + case "_==_": + return "=" + case "_!=_": + return "!=" + case "_<_": + return "<" + case "_>_": + return ">" + case "_<=_": + return "<=" + case "_>=_": + return ">=" + default: + return "=" + } +} diff --git a/plugin/filter/converter.go b/plugin/filter/converter.go new file mode 100644 index 0000000..c55a395 --- /dev/null +++ b/plugin/filter/converter.go @@ -0,0 +1,20 @@ +package filter + +import ( + "strings" +) + +type ConvertContext struct { + Buffer strings.Builder + Args []any + // The offset of the next argument in the condition string. + // Mainly using for PostgreSQL. + ArgsOffset int +} + +func NewConvertContext() *ConvertContext { + return &ConvertContext{ + Buffer: strings.Builder{}, + Args: []any{}, + } +} diff --git a/plugin/filter/dialect.go b/plugin/filter/dialect.go new file mode 100644 index 0000000..6c6ab4c --- /dev/null +++ b/plugin/filter/dialect.go @@ -0,0 +1,212 @@ +package filter + +import ( + "fmt" + "strings" +) + +// SQLDialect defines database-specific SQL generation methods. +type SQLDialect interface { + // Basic field access + GetTablePrefix() string + GetParameterPlaceholder(index int) string + + // JSON operations + GetJSONExtract(path string) string + GetJSONArrayLength(path string) string + GetJSONContains(path, element string) string + GetJSONLike(path, pattern string) string + + // Boolean operations + GetBooleanValue(value bool) interface{} + GetBooleanComparison(path string, value bool) string + GetBooleanCheck(path string) string + + // Timestamp operations + GetTimestampComparison(field string) string + GetCurrentTimestamp() string +} + +// DatabaseType represents the type of database. +type DatabaseType string + +const ( + SQLite DatabaseType = "sqlite" + MySQL DatabaseType = "mysql" + PostgreSQL DatabaseType = "postgres" +) + +// GetDialect returns the appropriate dialect for the database type. +func GetDialect(dbType DatabaseType) SQLDialect { + switch dbType { + case SQLite: + return &SQLiteDialect{} + case MySQL: + return &MySQLDialect{} + case PostgreSQL: + return &PostgreSQLDialect{} + default: + return &SQLiteDialect{} // default fallback + } +} + +// SQLiteDialect implements SQLDialect for SQLite. +type SQLiteDialect struct{} + +func (*SQLiteDialect) GetTablePrefix() string { + return "`memo`" +} + +func (*SQLiteDialect) GetParameterPlaceholder(_ int) string { + return "?" +} + +func (d *SQLiteDialect) GetJSONExtract(path string) string { + return fmt.Sprintf("JSON_EXTRACT(%s.`payload`, '%s')", d.GetTablePrefix(), path) +} + +func (d *SQLiteDialect) GetJSONArrayLength(path string) string { + return fmt.Sprintf("JSON_ARRAY_LENGTH(COALESCE(%s, JSON_ARRAY()))", d.GetJSONExtract(path)) +} + +func (d *SQLiteDialect) GetJSONContains(path, _ string) string { + return fmt.Sprintf("%s LIKE ?", d.GetJSONExtract(path)) +} + +func (d *SQLiteDialect) GetJSONLike(path, _ string) string { + return fmt.Sprintf("%s LIKE ?", d.GetJSONExtract(path)) +} + +func (*SQLiteDialect) GetBooleanValue(value bool) interface{} { + if value { + return 1 + } + return 0 +} + +func (d *SQLiteDialect) GetBooleanComparison(path string, value bool) string { + return fmt.Sprintf("%s = %d", d.GetJSONExtract(path), d.GetBooleanValue(value)) +} + +func (d *SQLiteDialect) GetBooleanCheck(path string) string { + return fmt.Sprintf("%s IS TRUE", d.GetJSONExtract(path)) +} + +func (d *SQLiteDialect) GetTimestampComparison(field string) string { + return fmt.Sprintf("%s.`%s`", d.GetTablePrefix(), field) +} + +func (*SQLiteDialect) GetCurrentTimestamp() string { + return "strftime('%s', 'now')" +} + +// MySQLDialect implements SQLDialect for MySQL. +type MySQLDialect struct{} + +func (*MySQLDialect) GetTablePrefix() string { + return "`memo`" +} + +func (*MySQLDialect) GetParameterPlaceholder(_ int) string { + return "?" +} + +func (d *MySQLDialect) GetJSONExtract(path string) string { + return fmt.Sprintf("JSON_EXTRACT(%s.`payload`, '%s')", d.GetTablePrefix(), path) +} + +func (d *MySQLDialect) GetJSONArrayLength(path string) string { + return fmt.Sprintf("JSON_LENGTH(COALESCE(%s, JSON_ARRAY()))", d.GetJSONExtract(path)) +} + +func (d *MySQLDialect) GetJSONContains(path, _ string) string { + return fmt.Sprintf("JSON_CONTAINS(%s, ?)", d.GetJSONExtract(path)) +} + +func (d *MySQLDialect) GetJSONLike(path, _ string) string { + return fmt.Sprintf("%s LIKE ?", d.GetJSONExtract(path)) +} + +func (*MySQLDialect) GetBooleanValue(value bool) interface{} { + return value +} + +func (d *MySQLDialect) GetBooleanComparison(path string, value bool) string { + boolStr := "false" + if value { + boolStr = "true" + } + return fmt.Sprintf("%s = CAST('%s' AS JSON)", d.GetJSONExtract(path), boolStr) +} + +func (d *MySQLDialect) GetBooleanCheck(path string) string { + return fmt.Sprintf("%s = CAST('true' AS JSON)", d.GetJSONExtract(path)) +} + +func (d *MySQLDialect) GetTimestampComparison(field string) string { + return fmt.Sprintf("UNIX_TIMESTAMP(%s.`%s`)", d.GetTablePrefix(), field) +} + +func (*MySQLDialect) GetCurrentTimestamp() string { + return "UNIX_TIMESTAMP()" +} + +// PostgreSQLDialect implements SQLDialect for PostgreSQL. +type PostgreSQLDialect struct{} + +func (*PostgreSQLDialect) GetTablePrefix() string { + return "memo" +} + +func (*PostgreSQLDialect) GetParameterPlaceholder(index int) string { + return fmt.Sprintf("$%d", index) +} + +func (d *PostgreSQLDialect) GetJSONExtract(path string) string { + // Convert $.property.hasTaskList to payload->'property'->>'hasTaskList' + parts := strings.Split(strings.TrimPrefix(path, "$."), ".") + result := fmt.Sprintf("%s.payload", d.GetTablePrefix()) + for i, part := range parts { + if i == len(parts)-1 { + result += fmt.Sprintf("->>'%s'", part) + } else { + result += fmt.Sprintf("->'%s'", part) + } + } + return result +} + +func (d *PostgreSQLDialect) GetJSONArrayLength(path string) string { + jsonPath := strings.Replace(path, "$.tags", "payload->'tags'", 1) + return fmt.Sprintf("jsonb_array_length(COALESCE(%s.%s, '[]'::jsonb))", d.GetTablePrefix(), jsonPath) +} + +func (d *PostgreSQLDialect) GetJSONContains(path, _ string) string { + jsonPath := strings.Replace(path, "$.tags", "payload->'tags'", 1) + return fmt.Sprintf("%s.%s @> jsonb_build_array(?)", d.GetTablePrefix(), jsonPath) +} + +func (d *PostgreSQLDialect) GetJSONLike(path, _ string) string { + jsonPath := strings.Replace(path, "$.tags", "payload->'tags'", 1) + return fmt.Sprintf("%s.%s @> jsonb_build_array(?)", d.GetTablePrefix(), jsonPath) +} + +func (*PostgreSQLDialect) GetBooleanValue(value bool) interface{} { + return value +} + +func (d *PostgreSQLDialect) GetBooleanComparison(path string, _ bool) string { + return fmt.Sprintf("(%s)::boolean = ?", d.GetJSONExtract(path)) +} + +func (d *PostgreSQLDialect) GetBooleanCheck(path string) string { + return fmt.Sprintf("(%s)::boolean IS TRUE", d.GetJSONExtract(path)) +} + +func (d *PostgreSQLDialect) GetTimestampComparison(field string) string { + return fmt.Sprintf("EXTRACT(EPOCH FROM %s.%s)", d.GetTablePrefix(), field) +} + +func (*PostgreSQLDialect) GetCurrentTimestamp() string { + return "EXTRACT(EPOCH FROM NOW())" +} diff --git a/plugin/filter/expr.go b/plugin/filter/expr.go new file mode 100644 index 0000000..01ce539 --- /dev/null +++ b/plugin/filter/expr.go @@ -0,0 +1,127 @@ +package filter + +import ( + "errors" + "time" + + exprv1 "google.golang.org/genproto/googleapis/api/expr/v1alpha1" +) + +// GetConstValue returns the constant value of the expression. +func GetConstValue(expr *exprv1.Expr) (any, error) { + v, ok := expr.ExprKind.(*exprv1.Expr_ConstExpr) + if !ok { + return nil, errors.New("invalid constant expression") + } + + switch v.ConstExpr.ConstantKind.(type) { + case *exprv1.Constant_StringValue: + return v.ConstExpr.GetStringValue(), nil + case *exprv1.Constant_Int64Value: + return v.ConstExpr.GetInt64Value(), nil + case *exprv1.Constant_Uint64Value: + return v.ConstExpr.GetUint64Value(), nil + case *exprv1.Constant_DoubleValue: + return v.ConstExpr.GetDoubleValue(), nil + case *exprv1.Constant_BoolValue: + return v.ConstExpr.GetBoolValue(), nil + default: + return nil, errors.New("unexpected constant type") + } +} + +// GetIdentExprName returns the name of the identifier expression. +func GetIdentExprName(expr *exprv1.Expr) (string, error) { + _, ok := expr.ExprKind.(*exprv1.Expr_IdentExpr) + if !ok { + return "", errors.New("invalid identifier expression") + } + return expr.GetIdentExpr().GetName(), nil +} + +// GetFunctionValue evaluates CEL function calls and returns their value. +// This is specifically for time functions like now(). +func GetFunctionValue(expr *exprv1.Expr) (any, error) { + callExpr, ok := expr.ExprKind.(*exprv1.Expr_CallExpr) + if !ok { + return nil, errors.New("invalid function call expression") + } + + switch callExpr.CallExpr.Function { + case "now": + if len(callExpr.CallExpr.Args) != 0 { + return nil, errors.New("now() function takes no arguments") + } + return time.Now().Unix(), nil + case "_-_": + // Handle subtraction for expressions like "now() - 60 * 60 * 24" + if len(callExpr.CallExpr.Args) != 2 { + return nil, errors.New("subtraction requires exactly two arguments") + } + left, err := GetExprValue(callExpr.CallExpr.Args[0]) + if err != nil { + return nil, err + } + right, err := GetExprValue(callExpr.CallExpr.Args[1]) + if err != nil { + return nil, err + } + leftInt, ok1 := left.(int64) + rightInt, ok2 := right.(int64) + if !ok1 || !ok2 { + return nil, errors.New("subtraction operands must be integers") + } + return leftInt - rightInt, nil + case "_*_": + // Handle multiplication for expressions like "60 * 60 * 24" + if len(callExpr.CallExpr.Args) != 2 { + return nil, errors.New("multiplication requires exactly two arguments") + } + left, err := GetExprValue(callExpr.CallExpr.Args[0]) + if err != nil { + return nil, err + } + right, err := GetExprValue(callExpr.CallExpr.Args[1]) + if err != nil { + return nil, err + } + leftInt, ok1 := left.(int64) + rightInt, ok2 := right.(int64) + if !ok1 || !ok2 { + return nil, errors.New("multiplication operands must be integers") + } + return leftInt * rightInt, nil + case "_+_": + // Handle addition + if len(callExpr.CallExpr.Args) != 2 { + return nil, errors.New("addition requires exactly two arguments") + } + left, err := GetExprValue(callExpr.CallExpr.Args[0]) + if err != nil { + return nil, err + } + right, err := GetExprValue(callExpr.CallExpr.Args[1]) + if err != nil { + return nil, err + } + leftInt, ok1 := left.(int64) + rightInt, ok2 := right.(int64) + if !ok1 || !ok2 { + return nil, errors.New("addition operands must be integers") + } + return leftInt + rightInt, nil + default: + return nil, errors.New("unsupported function: " + callExpr.CallExpr.Function) + } +} + +// GetExprValue attempts to get a value from an expression, trying constants first, then functions. +func GetExprValue(expr *exprv1.Expr) (any, error) { + // Try to get constant value first + if constValue, err := GetConstValue(expr); err == nil { + return constValue, nil + } + + // If not a constant, try to evaluate as a function + return GetFunctionValue(expr) +} diff --git a/plugin/filter/filter.go b/plugin/filter/filter.go new file mode 100644 index 0000000..6ebaca2 --- /dev/null +++ b/plugin/filter/filter.go @@ -0,0 +1,48 @@ +package filter + +import ( + "time" + + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + "github.com/pkg/errors" + exprv1 "google.golang.org/genproto/googleapis/api/expr/v1alpha1" +) + +// MemoFilterCELAttributes are the CEL attributes for memo. +var MemoFilterCELAttributes = []cel.EnvOption{ + cel.Variable("content", cel.StringType), + cel.Variable("creator_id", cel.IntType), + cel.Variable("created_ts", cel.IntType), + cel.Variable("updated_ts", cel.IntType), + cel.Variable("pinned", cel.BoolType), + cel.Variable("tag", cel.StringType), + cel.Variable("tags", cel.ListType(cel.StringType)), + cel.Variable("visibility", cel.StringType), + cel.Variable("has_task_list", cel.BoolType), + // Current timestamp function. + cel.Function("now", + cel.Overload("now", + []*cel.Type{}, + cel.IntType, + cel.FunctionBinding(func(_ ...ref.Val) ref.Val { + return types.Int(time.Now().Unix()) + }), + ), + ), +} + +// Parse parses the filter string and returns the parsed expression. +// The filter string should be a CEL expression. +func Parse(filter string, opts ...cel.EnvOption) (expr *exprv1.ParsedExpr, err error) { + e, err := cel.NewEnv(opts...) + if err != nil { + return nil, errors.Wrap(err, "failed to create CEL environment") + } + ast, issues := e.Compile(filter) + if issues != nil { + return nil, errors.Errorf("failed to compile filter: %v", issues) + } + return cel.AstToParsedExpr(ast) +} diff --git a/plugin/filter/templates.go b/plugin/filter/templates.go new file mode 100644 index 0000000..73e1f1d --- /dev/null +++ b/plugin/filter/templates.go @@ -0,0 +1,146 @@ +package filter + +import ( + "fmt" +) + +// SQLTemplate holds database-specific SQL fragments. +type SQLTemplate struct { + SQLite string + MySQL string + PostgreSQL string +} + +// TemplateDBType represents the database type for templates. +type TemplateDBType string + +const ( + SQLiteTemplate TemplateDBType = "sqlite" + MySQLTemplate TemplateDBType = "mysql" + PostgreSQLTemplate TemplateDBType = "postgres" +) + +// SQLTemplates contains common SQL patterns for different databases. +var SQLTemplates = map[string]SQLTemplate{ + "json_extract": { + SQLite: "JSON_EXTRACT(`memo`.`payload`, '%s')", + MySQL: "JSON_EXTRACT(`memo`.`payload`, '%s')", + PostgreSQL: "memo.payload%s", + }, + "json_array_length": { + SQLite: "JSON_ARRAY_LENGTH(COALESCE(JSON_EXTRACT(`memo`.`payload`, '$.tags'), JSON_ARRAY()))", + MySQL: "JSON_LENGTH(COALESCE(JSON_EXTRACT(`memo`.`payload`, '$.tags'), JSON_ARRAY()))", + PostgreSQL: "jsonb_array_length(COALESCE(memo.payload->'tags', '[]'::jsonb))", + }, + "json_contains_element": { + SQLite: "JSON_EXTRACT(`memo`.`payload`, '$.tags') LIKE ?", + MySQL: "JSON_CONTAINS(JSON_EXTRACT(`memo`.`payload`, '$.tags'), ?)", + PostgreSQL: "memo.payload->'tags' @> jsonb_build_array(?)", + }, + "json_contains_tag": { + SQLite: "JSON_EXTRACT(`memo`.`payload`, '$.tags') LIKE ?", + MySQL: "JSON_CONTAINS(JSON_EXTRACT(`memo`.`payload`, '$.tags'), ?)", + PostgreSQL: "memo.payload->'tags' @> jsonb_build_array(?)", + }, + "boolean_true": { + SQLite: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = 1", + MySQL: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = CAST('true' AS JSON)", + PostgreSQL: "(memo.payload->'property'->>'hasTaskList')::boolean = true", + }, + "boolean_false": { + SQLite: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = 0", + MySQL: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = CAST('false' AS JSON)", + PostgreSQL: "(memo.payload->'property'->>'hasTaskList')::boolean = false", + }, + "boolean_not_true": { + SQLite: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') != 1", + MySQL: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') != CAST('true' AS JSON)", + PostgreSQL: "(memo.payload->'property'->>'hasTaskList')::boolean != true", + }, + "boolean_not_false": { + SQLite: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') != 0", + MySQL: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') != CAST('false' AS JSON)", + PostgreSQL: "(memo.payload->'property'->>'hasTaskList')::boolean != false", + }, + "boolean_compare": { + SQLite: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') %s ?", + MySQL: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') %s CAST(? AS JSON)", + PostgreSQL: "(memo.payload->'property'->>'hasTaskList')::boolean %s ?", + }, + "boolean_check": { + SQLite: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') IS TRUE", + MySQL: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = CAST('true' AS JSON)", + PostgreSQL: "(memo.payload->'property'->>'hasTaskList')::boolean IS TRUE", + }, + "table_prefix": { + SQLite: "`memo`", + MySQL: "`memo`", + PostgreSQL: "memo", + }, + "timestamp_field": { + SQLite: "`memo`.`%s`", + MySQL: "UNIX_TIMESTAMP(`memo`.`%s`)", + PostgreSQL: "EXTRACT(EPOCH FROM memo.%s)", + }, + "content_like": { + SQLite: "`memo`.`content` LIKE ?", + MySQL: "`memo`.`content` LIKE ?", + PostgreSQL: "memo.content ILIKE ?", + }, + "visibility_in": { + SQLite: "`memo`.`visibility` IN (%s)", + MySQL: "`memo`.`visibility` IN (%s)", + PostgreSQL: "memo.visibility IN (%s)", + }, +} + +// GetSQL returns the appropriate SQL for the given template and database type. +func GetSQL(templateName string, dbType TemplateDBType) string { + template, exists := SQLTemplates[templateName] + if !exists { + return "" + } + + switch dbType { + case SQLiteTemplate: + return template.SQLite + case MySQLTemplate: + return template.MySQL + case PostgreSQLTemplate: + return template.PostgreSQL + default: + return template.SQLite + } +} + +// GetParameterPlaceholder returns the appropriate parameter placeholder for the database. +func GetParameterPlaceholder(dbType TemplateDBType, index int) string { + switch dbType { + case PostgreSQLTemplate: + return fmt.Sprintf("$%d", index) + default: + return "?" + } +} + +// GetParameterValue returns the appropriate parameter value for the database. +func GetParameterValue(dbType TemplateDBType, templateName string, value interface{}) interface{} { + switch templateName { + case "json_contains_element", "json_contains_tag": + if dbType == SQLiteTemplate { + return fmt.Sprintf(`%%"%s"%%`, value) + } + return value + default: + return value + } +} + +// FormatPlaceholders formats a list of placeholders for the given database type. +func FormatPlaceholders(dbType TemplateDBType, count int, startIndex int) []string { + placeholders := make([]string, count) + for i := 0; i < count; i++ { + placeholders[i] = GetParameterPlaceholder(dbType, startIndex+i) + } + return placeholders +} diff --git a/plugin/httpgetter/html_meta.go b/plugin/httpgetter/html_meta.go new file mode 100644 index 0000000..3ac7194 --- /dev/null +++ b/plugin/httpgetter/html_meta.go @@ -0,0 +1,166 @@ +package httpgetter + +import ( + "fmt" + "io" + "net" + "net/http" + "net/url" + + "github.com/pkg/errors" + "golang.org/x/net/html" + "golang.org/x/net/html/atom" +) + +var ErrInternalIP = errors.New("internal IP addresses are not allowed") + +var httpClient = &http.Client{ + CheckRedirect: func(req *http.Request, via []*http.Request) error { + if err := validateURL(req.URL.String()); err != nil { + return errors.Wrap(err, "redirect to internal IP") + } + if len(via) >= 10 { + return errors.New("too many redirects") + } + return nil + }, +} + +type HTMLMeta struct { + Title string `json:"title"` + Description string `json:"description"` + Image string `json:"image"` +} + +func GetHTMLMeta(urlStr string) (*HTMLMeta, error) { + if err := validateURL(urlStr); err != nil { + return nil, err + } + + response, err := httpClient.Get(urlStr) + if err != nil { + return nil, err + } + defer response.Body.Close() + + mediatype, err := getMediatype(response) + if err != nil { + return nil, err + } + if mediatype != "text/html" { + return nil, errors.New("not a HTML page") + } + + // TODO: limit the size of the response body + + htmlMeta := extractHTMLMeta(response.Body) + enrichSiteMeta(response.Request.URL, htmlMeta) + return htmlMeta, nil +} + +func extractHTMLMeta(resp io.Reader) *HTMLMeta { + tokenizer := html.NewTokenizer(resp) + htmlMeta := new(HTMLMeta) + + for { + tokenType := tokenizer.Next() + if tokenType == html.ErrorToken { + break + } else if tokenType == html.StartTagToken || tokenType == html.SelfClosingTagToken { + token := tokenizer.Token() + if token.DataAtom == atom.Body { + break + } + + if token.DataAtom == atom.Title { + tokenizer.Next() + token := tokenizer.Token() + htmlMeta.Title = token.Data + } else if token.DataAtom == atom.Meta { + description, ok := extractMetaProperty(token, "description") + if ok { + htmlMeta.Description = description + } + + ogTitle, ok := extractMetaProperty(token, "og:title") + if ok { + htmlMeta.Title = ogTitle + } + + ogDescription, ok := extractMetaProperty(token, "og:description") + if ok { + htmlMeta.Description = ogDescription + } + + ogImage, ok := extractMetaProperty(token, "og:image") + if ok { + htmlMeta.Image = ogImage + } + } + } + } + + return htmlMeta +} + +func extractMetaProperty(token html.Token, prop string) (content string, ok bool) { + content, ok = "", false + for _, attr := range token.Attr { + if attr.Key == "property" && attr.Val == prop { + ok = true + } + if attr.Key == "content" { + content = attr.Val + } + } + return content, ok +} + +func validateURL(urlStr string) error { + u, err := url.Parse(urlStr) + if err != nil { + return errors.New("invalid URL format") + } + + if u.Scheme != "http" && u.Scheme != "https" { + return errors.New("only http/https protocols are allowed") + } + + host := u.Hostname() + if host == "" { + return errors.New("empty hostname") + } + + // check if the hostname is an IP + if ip := net.ParseIP(host); ip != nil { + if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() { + return errors.Wrap(ErrInternalIP, ip.String()) + } + return nil + } + + // check if it's a hostname, resolve it and check all returned IPs + ips, err := net.LookupIP(host) + if err != nil { + return errors.Errorf("failed to resolve hostname: %v", err) + } + + for _, ip := range ips { + if ip.IsLoopback() || ip.IsPrivate() || ip.IsLinkLocalUnicast() { + return errors.Wrapf(ErrInternalIP, "host=%s, ip=%s", host, ip.String()) + } + } + + return nil +} + +func enrichSiteMeta(url *url.URL, meta *HTMLMeta) { + if url.Hostname() == "www.youtube.com" { + if url.Path == "/watch" { + vid := url.Query().Get("v") + if vid != "" { + meta.Image = fmt.Sprintf("https://img.youtube.com/vi/%s/mqdefault.jpg", vid) + } + } + } +} diff --git a/plugin/httpgetter/html_meta_test.go b/plugin/httpgetter/html_meta_test.go new file mode 100644 index 0000000..d1668db --- /dev/null +++ b/plugin/httpgetter/html_meta_test.go @@ -0,0 +1,32 @@ +package httpgetter + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetHTMLMeta(t *testing.T) { + tests := []struct { + urlStr string + htmlMeta HTMLMeta + }{} + for _, test := range tests { + metadata, err := GetHTMLMeta(test.urlStr) + require.NoError(t, err) + require.Equal(t, test.htmlMeta, *metadata) + } +} + +func TestGetHTMLMetaForInternal(t *testing.T) { + // test for internal IP + if _, err := GetHTMLMeta("http://192.168.0.1"); !errors.Is(err, ErrInternalIP) { + t.Errorf("Expected error for internal IP, got %v", err) + } + + // test for resolved internal IP + if _, err := GetHTMLMeta("http://localhost"); !errors.Is(err, ErrInternalIP) { + t.Errorf("Expected error for resolved internal IP, got %v", err) + } +} diff --git a/plugin/httpgetter/http_getter.go b/plugin/httpgetter/http_getter.go new file mode 100644 index 0000000..581acb7 --- /dev/null +++ b/plugin/httpgetter/http_getter.go @@ -0,0 +1 @@ +package httpgetter diff --git a/plugin/httpgetter/image.go b/plugin/httpgetter/image.go new file mode 100644 index 0000000..536afea --- /dev/null +++ b/plugin/httpgetter/image.go @@ -0,0 +1,45 @@ +package httpgetter + +import ( + "errors" + "io" + "net/http" + "net/url" + "strings" +) + +type Image struct { + Blob []byte + Mediatype string +} + +func GetImage(urlStr string) (*Image, error) { + if _, err := url.Parse(urlStr); err != nil { + return nil, err + } + + response, err := http.Get(urlStr) + if err != nil { + return nil, err + } + defer response.Body.Close() + + mediatype, err := getMediatype(response) + if err != nil { + return nil, err + } + if !strings.HasPrefix(mediatype, "image/") { + return nil, errors.New("wrong image mediatype") + } + + bodyBytes, err := io.ReadAll(response.Body) + if err != nil { + return nil, err + } + + image := &Image{ + Blob: bodyBytes, + Mediatype: mediatype, + } + return image, nil +} diff --git a/plugin/httpgetter/util.go b/plugin/httpgetter/util.go new file mode 100644 index 0000000..d83f5ef --- /dev/null +++ b/plugin/httpgetter/util.go @@ -0,0 +1,15 @@ +package httpgetter + +import ( + "mime" + "net/http" +) + +func getMediatype(response *http.Response) (string, error) { + contentType := response.Header.Get("content-type") + mediatype, _, err := mime.ParseMediaType(contentType) + if err != nil { + return "", err + } + return mediatype, nil +} diff --git a/plugin/idp/idp.go b/plugin/idp/idp.go new file mode 100644 index 0000000..1360d6a --- /dev/null +++ b/plugin/idp/idp.go @@ -0,0 +1,8 @@ +package idp + +type IdentityProviderUserInfo struct { + Identifier string + DisplayName string + Email string + AvatarURL string +} diff --git a/plugin/idp/oauth2/oauth2.go b/plugin/idp/oauth2/oauth2.go new file mode 100644 index 0000000..6d10075 --- /dev/null +++ b/plugin/idp/oauth2/oauth2.go @@ -0,0 +1,123 @@ +// Package oauth2 is the plugin for OAuth2 Identity Provider. +package oauth2 + +import ( + "context" + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + + "github.com/pkg/errors" + "golang.org/x/oauth2" + + "github.com/usememos/memos/plugin/idp" + storepb "github.com/usememos/memos/proto/gen/store" +) + +// IdentityProvider represents an OAuth2 Identity Provider. +type IdentityProvider struct { + config *storepb.OAuth2Config +} + +// NewIdentityProvider initializes a new OAuth2 Identity Provider with the given configuration. +func NewIdentityProvider(config *storepb.OAuth2Config) (*IdentityProvider, error) { + for v, field := range map[string]string{ + config.ClientId: "clientId", + config.ClientSecret: "clientSecret", + config.TokenUrl: "tokenUrl", + config.UserInfoUrl: "userInfoUrl", + config.FieldMapping.Identifier: "fieldMapping.identifier", + } { + if v == "" { + return nil, errors.Errorf(`the field "%s" is empty but required`, field) + } + } + + return &IdentityProvider{ + config: config, + }, nil +} + +// ExchangeToken returns the exchanged OAuth2 token using the given authorization code. +func (p *IdentityProvider) ExchangeToken(ctx context.Context, redirectURL, code string) (string, error) { + conf := &oauth2.Config{ + ClientID: p.config.ClientId, + ClientSecret: p.config.ClientSecret, + RedirectURL: redirectURL, + Scopes: p.config.Scopes, + Endpoint: oauth2.Endpoint{ + AuthURL: p.config.AuthUrl, + TokenURL: p.config.TokenUrl, + AuthStyle: oauth2.AuthStyleInParams, + }, + } + + token, err := conf.Exchange(ctx, code) + if err != nil { + return "", errors.Wrap(err, "failed to exchange access token") + } + + accessToken, ok := token.Extra("access_token").(string) + if !ok { + return "", errors.New(`missing "access_token" from authorization response`) + } + + return accessToken, nil +} + +// UserInfo returns the parsed user information using the given OAuth2 token. +func (p *IdentityProvider) UserInfo(token string) (*idp.IdentityProviderUserInfo, error) { + client := &http.Client{} + req, err := http.NewRequest(http.MethodGet, p.config.UserInfoUrl, nil) + if err != nil { + return nil, errors.Wrap(err, "failed to new http request") + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) + resp, err := client.Do(req) + if err != nil { + return nil, errors.Wrap(err, "failed to get user information") + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrap(err, "failed to read response body") + } + defer resp.Body.Close() + + var claims map[string]any + if err := json.Unmarshal(body, &claims); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal response body") + } + slog.Info("user info claims", "claims", claims) + userInfo := &idp.IdentityProviderUserInfo{} + if v, ok := claims[p.config.FieldMapping.Identifier].(string); ok { + userInfo.Identifier = v + } + if userInfo.Identifier == "" { + return nil, errors.Errorf("the field %q is not found in claims or has empty value", p.config.FieldMapping.Identifier) + } + + // Best effort to map optional fields + if p.config.FieldMapping.DisplayName != "" { + if v, ok := claims[p.config.FieldMapping.DisplayName].(string); ok { + userInfo.DisplayName = v + } + } + if userInfo.DisplayName == "" { + userInfo.DisplayName = userInfo.Identifier + } + if p.config.FieldMapping.Email != "" { + if v, ok := claims[p.config.FieldMapping.Email].(string); ok { + userInfo.Email = v + } + } + if p.config.FieldMapping.AvatarUrl != "" { + if v, ok := claims[p.config.FieldMapping.AvatarUrl].(string); ok { + userInfo.AvatarURL = v + } + } + slog.Info("user info", "userInfo", userInfo) + return userInfo, nil +} diff --git a/plugin/idp/oauth2/oauth2_test.go b/plugin/idp/oauth2/oauth2_test.go new file mode 100644 index 0000000..b91f037 --- /dev/null +++ b/plugin/idp/oauth2/oauth2_test.go @@ -0,0 +1,163 @@ +package oauth2 + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/usememos/memos/plugin/idp" + storepb "github.com/usememos/memos/proto/gen/store" +) + +func TestNewIdentityProvider(t *testing.T) { + tests := []struct { + name string + config *storepb.OAuth2Config + containsErr string + }{ + { + name: "no tokenUrl", + config: &storepb.OAuth2Config{ + ClientId: "test-client-id", + ClientSecret: "test-client-secret", + AuthUrl: "", + TokenUrl: "", + UserInfoUrl: "https://example.com/api/user", + FieldMapping: &storepb.FieldMapping{ + Identifier: "login", + }, + }, + containsErr: `the field "tokenUrl" is empty but required`, + }, + { + name: "no userInfoUrl", + config: &storepb.OAuth2Config{ + ClientId: "test-client-id", + ClientSecret: "test-client-secret", + AuthUrl: "", + TokenUrl: "https://example.com/token", + UserInfoUrl: "", + FieldMapping: &storepb.FieldMapping{ + Identifier: "login", + }, + }, + containsErr: `the field "userInfoUrl" is empty but required`, + }, + { + name: "no field mapping identifier", + config: &storepb.OAuth2Config{ + ClientId: "test-client-id", + ClientSecret: "test-client-secret", + AuthUrl: "", + TokenUrl: "https://example.com/token", + UserInfoUrl: "https://example.com/api/user", + FieldMapping: &storepb.FieldMapping{ + Identifier: "", + }, + }, + containsErr: `the field "fieldMapping.identifier" is empty but required`, + }, + } + for _, test := range tests { + t.Run(test.name, func(*testing.T) { + _, err := NewIdentityProvider(test.config) + assert.ErrorContains(t, err, test.containsErr) + }) + } +} + +func newMockServer(t *testing.T, code, accessToken string, userinfo []byte) *httptest.Server { + mux := http.NewServeMux() + + var rawIDToken string + mux.HandleFunc("/oauth2/token", func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + + body, err := io.ReadAll(r.Body) + require.NoError(t, err) + vals, err := url.ParseQuery(string(body)) + require.NoError(t, err) + + require.Equal(t, code, vals.Get("code")) + require.Equal(t, "authorization_code", vals.Get("grant_type")) + + w.Header().Set("Content-Type", "application/json") + err = json.NewEncoder(w).Encode(map[string]any{ + "access_token": accessToken, + "token_type": "Bearer", + "expires_in": 3600, + "id_token": rawIDToken, + }) + require.NoError(t, err) + }) + mux.HandleFunc("/oauth2/userinfo", func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, err := w.Write(userinfo) + require.NoError(t, err) + }) + + s := httptest.NewServer(mux) + + return s +} + +func TestIdentityProvider(t *testing.T) { + ctx := context.Background() + + const ( + testClientID = "test-client-id" + testCode = "test-code" + testAccessToken = "test-access-token" + testSubject = "123456789" + testName = "John Doe" + testEmail = "john.doe@example.com" + ) + userInfo, err := json.Marshal( + map[string]any{ + "sub": testSubject, + "name": testName, + "email": testEmail, + }, + ) + require.NoError(t, err) + + s := newMockServer(t, testCode, testAccessToken, userInfo) + + oauth2, err := NewIdentityProvider( + &storepb.OAuth2Config{ + ClientId: testClientID, + ClientSecret: "test-client-secret", + TokenUrl: fmt.Sprintf("%s/oauth2/token", s.URL), + UserInfoUrl: fmt.Sprintf("%s/oauth2/userinfo", s.URL), + FieldMapping: &storepb.FieldMapping{ + Identifier: "sub", + DisplayName: "name", + Email: "email", + }, + }, + ) + require.NoError(t, err) + + redirectURL := "https://example.com/oauth/callback" + oauthToken, err := oauth2.ExchangeToken(ctx, redirectURL, testCode) + require.NoError(t, err) + require.Equal(t, testAccessToken, oauthToken) + + userInfoResult, err := oauth2.UserInfo(oauthToken) + require.NoError(t, err) + + wantUserInfo := &idp.IdentityProviderUserInfo{ + Identifier: testSubject, + DisplayName: testName, + Email: testEmail, + } + assert.Equal(t, wantUserInfo, userInfoResult) +} diff --git a/plugin/storage/s3/s3.go b/plugin/storage/s3/s3.go new file mode 100644 index 0000000..34b55c1 --- /dev/null +++ b/plugin/storage/s3/s3.go @@ -0,0 +1,92 @@ +package s3 + +import ( + "context" + "io" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/feature/s3/manager" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/pkg/errors" + + storepb "github.com/usememos/memos/proto/gen/store" +) + +type Client struct { + Client *s3.Client + Bucket *string +} + +func NewClient(ctx context.Context, s3Config *storepb.StorageS3Config) (*Client, error) { + cfg, err := config.LoadDefaultConfig(ctx, + config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(s3Config.AccessKeyId, s3Config.AccessKeySecret, "")), + config.WithRegion(s3Config.Region), + ) + if err != nil { + return nil, errors.Wrap(err, "failed to load s3 config") + } + + client := s3.NewFromConfig(cfg, func(o *s3.Options) { + o.BaseEndpoint = aws.String(s3Config.Endpoint) + o.UsePathStyle = s3Config.UsePathStyle + o.RequestChecksumCalculation = aws.RequestChecksumCalculationWhenRequired + o.ResponseChecksumValidation = aws.ResponseChecksumValidationWhenRequired + }) + return &Client{ + Client: client, + Bucket: aws.String(s3Config.Bucket), + }, nil +} + +// UploadObject uploads an object to S3. +func (c *Client) UploadObject(ctx context.Context, key string, fileType string, content io.Reader) (string, error) { + uploader := manager.NewUploader(c.Client) + putInput := s3.PutObjectInput{ + Bucket: c.Bucket, + Key: aws.String(key), + ContentType: aws.String(fileType), + Body: content, + } + result, err := uploader.Upload(ctx, &putInput) + if err != nil { + return "", err + } + + resultKey := result.Key + if resultKey == nil || *resultKey == "" { + return "", errors.New("failed to get file key") + } + return *resultKey, nil +} + +// PresignGetObject presigns an object in S3. +func (c *Client) PresignGetObject(ctx context.Context, key string) (string, error) { + presignClient := s3.NewPresignClient(c.Client) + presignResult, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{ + Bucket: aws.String(*c.Bucket), + Key: aws.String(key), + }, func(opts *s3.PresignOptions) { + // Set the expiration time of the presigned URL to 5 days. + // Reference: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html + opts.Expires = time.Duration(5 * 24 * time.Hour) + }) + if err != nil { + return "", errors.Wrap(err, "failed to presign put object") + } + return presignResult.URL, nil +} + +// DeleteObject deletes an object in S3. +func (c *Client) DeleteObject(ctx context.Context, key string) error { + _, err := c.Client.DeleteObject(ctx, &s3.DeleteObjectInput{ + Bucket: c.Bucket, + Key: aws.String(key), + }) + if err != nil { + return errors.Wrap(err, "failed to delete object") + } + return nil +} diff --git a/plugin/webhook/webhook.go b/plugin/webhook/webhook.go new file mode 100644 index 0000000..fe59d4b --- /dev/null +++ b/plugin/webhook/webhook.go @@ -0,0 +1,90 @@ +package webhook + +import ( + "bytes" + "encoding/json" + "io" + "log/slog" + "net/http" + "time" + + "github.com/pkg/errors" + + v1pb "github.com/usememos/memos/proto/gen/api/v1" +) + +var ( + // timeout is the timeout for webhook request. Default to 30 seconds. + timeout = 30 * time.Second +) + +type WebhookRequestPayload struct { + // The target URL for the webhook request. + URL string `json:"url"` + // The type of activity that triggered this webhook. + ActivityType string `json:"activityType"` + // The resource name of the creator. Format: users/{user} + Creator string `json:"creator"` + // The memo that triggered this webhook (if applicable). + Memo *v1pb.Memo `json:"memo"` +} + +// Post posts the message to webhook endpoint. +func Post(requestPayload *WebhookRequestPayload) error { + body, err := json.Marshal(requestPayload) + if err != nil { + return errors.Wrapf(err, "failed to marshal webhook request to %s", requestPayload.URL) + } + + req, err := http.NewRequest("POST", requestPayload.URL, bytes.NewBuffer(body)) + if err != nil { + return errors.Wrapf(err, "failed to construct webhook request to %s", requestPayload.URL) + } + + req.Header.Set("Content-Type", "application/json") + client := &http.Client{ + Timeout: timeout, + } + resp, err := client.Do(req) + if err != nil { + return errors.Wrapf(err, "failed to post webhook to %s", requestPayload.URL) + } + + b, err := io.ReadAll(resp.Body) + if err != nil { + return errors.Wrapf(err, "failed to read webhook response from %s", requestPayload.URL) + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode > 299 { + return errors.Errorf("failed to post webhook %s, status code: %d, response body: %s", requestPayload.URL, resp.StatusCode, b) + } + + response := &struct { + Code int `json:"code"` + Message string `json:"message"` + }{} + if err := json.Unmarshal(b, response); err != nil { + return errors.Wrapf(err, "failed to unmarshal webhook response from %s", requestPayload.URL) + } + + if response.Code != 0 { + return errors.Errorf("receive error code sent by webhook server, code %d, msg: %s", response.Code, response.Message) + } + + return nil +} + +// PostAsync posts the message to webhook endpoint asynchronously. +// It spawns a new goroutine to handle the request and does not wait for the response. +func PostAsync(requestPayload *WebhookRequestPayload) { + go func() { + if err := Post(requestPayload); err != nil { + // Since we're in a goroutine, we can only log the error + slog.Warn("Failed to dispatch webhook asynchronously", + slog.String("url", requestPayload.URL), + slog.String("activityType", requestPayload.ActivityType), + slog.Any("err", err)) + } + }() +} diff --git a/plugin/webhook/webhook_test.go b/plugin/webhook/webhook_test.go new file mode 100644 index 0000000..d770c2c --- /dev/null +++ b/plugin/webhook/webhook_test.go @@ -0,0 +1 @@ +package webhook diff --git a/proto/README.md b/proto/README.md new file mode 100644 index 0000000..6360cad --- /dev/null +++ b/proto/README.md @@ -0,0 +1,17 @@ +# Guide + +## Prerequisites + +- [buf](https://docs.buf.build/installation) + +## Generate + +```sh +buf generate +``` + +## Format + +```sh +buf format -w +``` diff --git a/proto/api/v1/README.md b/proto/api/v1/README.md new file mode 100644 index 0000000..09b7b3d --- /dev/null +++ b/proto/api/v1/README.md @@ -0,0 +1,3 @@ +# Memos API Design + +This API design should follow the guidelines and best practices outlined in the [Google API Improvement Proposals (AIPs)](https://google.aip.dev/). diff --git a/proto/api/v1/activity_service.proto b/proto/api/v1/activity_service.proto new file mode 100644 index 0000000..25ded5c --- /dev/null +++ b/proto/api/v1/activity_service.proto @@ -0,0 +1,127 @@ +syntax = "proto3"; + +package memos.api.v1; + +import "google/api/annotations.proto"; +import "google/api/client.proto"; +import "google/api/field_behavior.proto"; +import "google/api/resource.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = "gen/api/v1"; + +service ActivityService { + // ListActivities returns a list of activities. + rpc ListActivities(ListActivitiesRequest) returns (ListActivitiesResponse) { + option (google.api.http) = {get: "/api/v1/activities"}; + } + + // GetActivity returns the activity with the given id. + rpc GetActivity(GetActivityRequest) returns (Activity) { + option (google.api.http) = {get: "/api/v1/{name=activities/*}"}; + option (google.api.method_signature) = "name"; + } +} + +message Activity { + option (google.api.resource) = { + type: "memos.api.v1/Activity" + pattern: "activities/{activity}" + name_field: "name" + singular: "activity" + plural: "activities" + }; + + // The name of the activity. + // Format: activities/{id} + string name = 1 [ + (google.api.field_behavior) = OUTPUT_ONLY, + (google.api.field_behavior) = IDENTIFIER + ]; + + // The name of the creator. + // Format: users/{user} + string creator = 2 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // The type of the activity. + Type type = 3 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // The level of the activity. + Level level = 4 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // The create time of the activity. + google.protobuf.Timestamp create_time = 5 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // The payload of the activity. + ActivityPayload payload = 6 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // Activity types. + enum Type { + // Unspecified type. + TYPE_UNSPECIFIED = 0; + // Memo comment activity. + MEMO_COMMENT = 1; + // Version update activity. + VERSION_UPDATE = 2; + } + + // Activity levels. + enum Level { + // Unspecified level. + LEVEL_UNSPECIFIED = 0; + // Info level. + INFO = 1; + // Warn level. + WARN = 2; + // Error level. + ERROR = 3; + } +} + +message ActivityPayload { + oneof payload { + // Memo comment activity payload. + ActivityMemoCommentPayload memo_comment = 1; + } +} + +// ActivityMemoCommentPayload represents the payload of a memo comment activity. +message ActivityMemoCommentPayload { + // The memo name of comment. + // Format: memos/{memo} + string memo = 1; + // The name of related memo. + // Format: memos/{memo} + string related_memo = 2; +} + +message ListActivitiesRequest { + // The maximum number of activities to return. + // The service may return fewer than this value. + // If unspecified, at most 100 activities will be returned. + // The maximum value is 1000; values above 1000 will be coerced to 1000. + int32 page_size = 1; + + // A page token, received from a previous `ListActivities` call. + // Provide this to retrieve the subsequent page. + string page_token = 2; +} + +message ListActivitiesResponse { + // The activities. + repeated Activity activities = 1; + + // A token to retrieve the next page of results. + // Pass this value in the page_token field in the subsequent call to `ListActivities` + // method to retrieve the next page of results. + string next_page_token = 2; +} + +message GetActivityRequest { + // The name of the activity. + // Format: activities/{id}, id is the system generated auto-incremented id. + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/Activity"} + ]; +} diff --git a/proto/api/v1/attachment_service.proto b/proto/api/v1/attachment_service.proto new file mode 100644 index 0000000..12fc9c1 --- /dev/null +++ b/proto/api/v1/attachment_service.proto @@ -0,0 +1,171 @@ +syntax = "proto3"; + +package memos.api.v1; + +import "google/api/annotations.proto"; +import "google/api/client.proto"; +import "google/api/field_behavior.proto"; +import "google/api/httpbody.proto"; +import "google/api/resource.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/field_mask.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = "gen/api/v1"; + +service AttachmentService { + // CreateAttachment creates a new attachment. + rpc CreateAttachment(CreateAttachmentRequest) returns (Attachment) { + option (google.api.http) = { + post: "/api/v1/attachments" + body: "attachment" + }; + option (google.api.method_signature) = "attachment"; + } + // ListAttachments lists all attachments. + rpc ListAttachments(ListAttachmentsRequest) returns (ListAttachmentsResponse) { + option (google.api.http) = {get: "/api/v1/attachments"}; + } + // GetAttachment returns a attachment by name. + rpc GetAttachment(GetAttachmentRequest) returns (Attachment) { + option (google.api.http) = {get: "/api/v1/{name=attachments/*}"}; + option (google.api.method_signature) = "name"; + } + // GetAttachmentBinary returns a attachment binary by name. + rpc GetAttachmentBinary(GetAttachmentBinaryRequest) returns (google.api.HttpBody) { + option (google.api.http) = {get: "/file/{name=attachments/*}/{filename}"}; + option (google.api.method_signature) = "name,filename,thumbnail"; + } + // UpdateAttachment updates a attachment. + rpc UpdateAttachment(UpdateAttachmentRequest) returns (Attachment) { + option (google.api.http) = { + patch: "/api/v1/{attachment.name=attachments/*}" + body: "attachment" + }; + option (google.api.method_signature) = "attachment,update_mask"; + } + // DeleteAttachment deletes a attachment by name. + rpc DeleteAttachment(DeleteAttachmentRequest) returns (google.protobuf.Empty) { + option (google.api.http) = {delete: "/api/v1/{name=attachments/*}"}; + option (google.api.method_signature) = "name"; + } +} + +message Attachment { + option (google.api.resource) = { + type: "memos.api.v1/Attachment" + pattern: "attachments/{attachment}" + singular: "attachment" + plural: "attachments" + }; + + // The name of the attachment. + // Format: attachments/{attachment} + string name = 1 [(google.api.field_behavior) = IDENTIFIER]; + + // Output only. The creation timestamp. + google.protobuf.Timestamp create_time = 2 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // The filename of the attachment. + string filename = 3 [(google.api.field_behavior) = REQUIRED]; + + // Input only. The content of the attachment. + bytes content = 4 [(google.api.field_behavior) = INPUT_ONLY]; + + // Optional. The external link of the attachment. + string external_link = 5 [(google.api.field_behavior) = OPTIONAL]; + + // The MIME type of the attachment. + string type = 6 [(google.api.field_behavior) = REQUIRED]; + + // Output only. The size of the attachment in bytes. + int64 size = 7 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // Optional. The related memo. Refer to `Memo.name`. + // Format: memos/{memo} + optional string memo = 8 [(google.api.field_behavior) = OPTIONAL]; +} + +message CreateAttachmentRequest { + // Required. The attachment to create. + Attachment attachment = 1 [(google.api.field_behavior) = REQUIRED]; + + // Optional. The attachment ID to use for this attachment. + // If empty, a unique ID will be generated. + string attachment_id = 2 [(google.api.field_behavior) = OPTIONAL]; +} + +message ListAttachmentsRequest { + // Optional. The maximum number of attachments to return. + // The service may return fewer than this value. + // If unspecified, at most 50 attachments will be returned. + // The maximum value is 1000; values above 1000 will be coerced to 1000. + int32 page_size = 1 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. A page token, received from a previous `ListAttachments` call. + // Provide this to retrieve the subsequent page. + string page_token = 2 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Filter to apply to the list results. + // Example: "type=image/png" or "filename:*.jpg" + // Supported operators: =, !=, <, <=, >, >=, : + // Supported fields: filename, type, size, create_time, memo + string filter = 3 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. The order to sort results by. + // Example: "create_time desc" or "filename asc" + string order_by = 4 [(google.api.field_behavior) = OPTIONAL]; +} + +message ListAttachmentsResponse { + // The list of attachments. + repeated Attachment attachments = 1; + + // A token that can be sent as `page_token` to retrieve the next page. + // If this field is omitted, there are no subsequent pages. + string next_page_token = 2; + + // The total count of attachments (may be approximate). + int32 total_size = 3; +} + +message GetAttachmentRequest { + // Required. The attachment name of the attachment to retrieve. + // Format: attachments/{attachment} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/Attachment"} + ]; +} + +message GetAttachmentBinaryRequest { + // Required. The attachment name of the attachment. + // Format: attachments/{attachment} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/Attachment"} + ]; + + // The filename of the attachment. Mainly used for downloading. + string filename = 2 [(google.api.field_behavior) = REQUIRED]; + + // Optional. A flag indicating if the thumbnail version of the attachment should be returned. + bool thumbnail = 3 [(google.api.field_behavior) = OPTIONAL]; +} + +message UpdateAttachmentRequest { + // Required. The attachment which replaces the attachment on the server. + Attachment attachment = 1 [(google.api.field_behavior) = REQUIRED]; + + // Required. The list of fields to update. + google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = REQUIRED]; +} + +message DeleteAttachmentRequest { + // Required. The attachment name of the attachment to delete. + // Format: attachments/{attachment} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/Attachment"} + ]; +} diff --git a/proto/api/v1/auth_service.proto b/proto/api/v1/auth_service.proto new file mode 100644 index 0000000..eb3477f --- /dev/null +++ b/proto/api/v1/auth_service.proto @@ -0,0 +1,93 @@ +syntax = "proto3"; + +package memos.api.v1; + +import "api/v1/user_service.proto"; +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = "gen/api/v1"; + +service AuthService { + // GetCurrentSession returns the current active session information. + // This method is idempotent and safe, suitable for checking current session state. + rpc GetCurrentSession(GetCurrentSessionRequest) returns (GetCurrentSessionResponse) { + option (google.api.http) = {get: "/api/v1/auth/sessions/current"}; + } + + // CreateSession authenticates a user and creates a new session. + // Returns the authenticated user information upon successful authentication. + rpc CreateSession(CreateSessionRequest) returns (CreateSessionResponse) { + option (google.api.http) = { + post: "/api/v1/auth/sessions" + body: "*" + }; + } + + // DeleteSession terminates the current user session. + // This is an idempotent operation that invalidates the user's authentication. + rpc DeleteSession(DeleteSessionRequest) returns (google.protobuf.Empty) { + option (google.api.http) = {delete: "/api/v1/auth/sessions/current"}; + } +} + +message GetCurrentSessionRequest {} + +message GetCurrentSessionResponse { + User user = 1; + + // Last time the session was accessed. + // Used for sliding expiration calculation (last_accessed_time + 2 weeks). + google.protobuf.Timestamp last_accessed_at = 2; +} + +message CreateSessionRequest { + // Nested message for password-based authentication credentials. + message PasswordCredentials { + // The username to sign in with. + // Required field for password-based authentication. + string username = 1 [(google.api.field_behavior) = REQUIRED]; + + // The password to sign in with. + // Required field for password-based authentication. + string password = 2 [(google.api.field_behavior) = REQUIRED]; + } + + // Nested message for SSO authentication credentials. + message SSOCredentials { + // The ID of the SSO provider. + // Required field to identify the SSO provider. + int32 idp_id = 1 [(google.api.field_behavior) = REQUIRED]; + + // The authorization code from the SSO provider. + // Required field for completing the SSO flow. + string code = 2 [(google.api.field_behavior) = REQUIRED]; + + // The redirect URI used in the SSO flow. + // Required field for security validation. + string redirect_uri = 3 [(google.api.field_behavior) = REQUIRED]; + } + + // Provide one authentication method (username/password or SSO). + // Required field to specify the authentication method. + oneof credentials { + // Username and password authentication method. + PasswordCredentials password_credentials = 1; + + // SSO provider authentication method. + SSOCredentials sso_credentials = 2; + } +} + +message CreateSessionResponse { + // The authenticated user information. + User user = 1; + + // Last time the session was accessed. + // Used for sliding expiration calculation (last_accessed_time + 2 weeks). + google.protobuf.Timestamp last_accessed_at = 2; +} + +message DeleteSessionRequest {} diff --git a/proto/api/v1/common.proto b/proto/api/v1/common.proto new file mode 100644 index 0000000..6c5fcf8 --- /dev/null +++ b/proto/api/v1/common.proto @@ -0,0 +1,23 @@ +syntax = "proto3"; + +package memos.api.v1; + +option go_package = "gen/api/v1"; + +enum State { + STATE_UNSPECIFIED = 0; + NORMAL = 1; + ARCHIVED = 2; +} + +// Used internally for obfuscating the page token. +message PageToken { + int32 limit = 1; + int32 offset = 2; +} + +enum Direction { + DIRECTION_UNSPECIFIED = 0; + ASC = 1; + DESC = 2; +} diff --git a/proto/api/v1/idp_service.proto b/proto/api/v1/idp_service.proto new file mode 100644 index 0000000..a35e672 --- /dev/null +++ b/proto/api/v1/idp_service.proto @@ -0,0 +1,147 @@ +syntax = "proto3"; + +package memos.api.v1; + +import "google/api/annotations.proto"; +import "google/api/client.proto"; +import "google/api/field_behavior.proto"; +import "google/api/resource.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/field_mask.proto"; + +option go_package = "gen/api/v1"; + +service IdentityProviderService { + // ListIdentityProviders lists identity providers. + rpc ListIdentityProviders(ListIdentityProvidersRequest) returns (ListIdentityProvidersResponse) { + option (google.api.http) = {get: "/api/v1/identityProviders"}; + } + + // GetIdentityProvider gets an identity provider. + rpc GetIdentityProvider(GetIdentityProviderRequest) returns (IdentityProvider) { + option (google.api.http) = {get: "/api/v1/{name=identityProviders/*}"}; + option (google.api.method_signature) = "name"; + } + + // CreateIdentityProvider creates an identity provider. + rpc CreateIdentityProvider(CreateIdentityProviderRequest) returns (IdentityProvider) { + option (google.api.http) = { + post: "/api/v1/identityProviders" + body: "identity_provider" + }; + option (google.api.method_signature) = "identity_provider"; + } + + // UpdateIdentityProvider updates an identity provider. + rpc UpdateIdentityProvider(UpdateIdentityProviderRequest) returns (IdentityProvider) { + option (google.api.http) = { + patch: "/api/v1/{identity_provider.name=identityProviders/*}" + body: "identity_provider" + }; + option (google.api.method_signature) = "identity_provider,update_mask"; + } + + // DeleteIdentityProvider deletes an identity provider. + rpc DeleteIdentityProvider(DeleteIdentityProviderRequest) returns (google.protobuf.Empty) { + option (google.api.http) = {delete: "/api/v1/{name=identityProviders/*}"}; + option (google.api.method_signature) = "name"; + } +} + +message IdentityProvider { + option (google.api.resource) = { + type: "memos.api.v1/IdentityProvider" + pattern: "identityProviders/{idp}" + name_field: "name" + singular: "identityProvider" + plural: "identityProviders" + }; + + // The resource name of the identity provider. + // Format: identityProviders/{idp} + string name = 1 [(google.api.field_behavior) = IDENTIFIER]; + + // Required. The type of the identity provider. + Type type = 2 [(google.api.field_behavior) = REQUIRED]; + + // Required. The display title of the identity provider. + string title = 3 [(google.api.field_behavior) = REQUIRED]; + + // Optional. Filter applied to user identifiers. + string identifier_filter = 4 [(google.api.field_behavior) = OPTIONAL]; + + // Required. Configuration for the identity provider. + IdentityProviderConfig config = 5 [(google.api.field_behavior) = REQUIRED]; + + enum Type { + TYPE_UNSPECIFIED = 0; + // OAuth2 identity provider. + OAUTH2 = 1; + } +} + +message IdentityProviderConfig { + oneof config { + OAuth2Config oauth2_config = 1; + } +} + +message FieldMapping { + string identifier = 1; + string display_name = 2; + string email = 3; + string avatar_url = 4; +} + +message OAuth2Config { + string client_id = 1; + string client_secret = 2; + string auth_url = 3; + string token_url = 4; + string user_info_url = 5; + repeated string scopes = 6; + FieldMapping field_mapping = 7; +} + +message ListIdentityProvidersRequest {} + +message ListIdentityProvidersResponse { + // The list of identity providers. + repeated IdentityProvider identity_providers = 1; +} + +message GetIdentityProviderRequest { + // Required. The resource name of the identity provider to get. + // Format: identityProviders/{idp} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/IdentityProvider"} + ]; +} + +message CreateIdentityProviderRequest { + // Required. The identity provider to create. + IdentityProvider identity_provider = 1 [(google.api.field_behavior) = REQUIRED]; + + // Optional. The ID to use for the identity provider, which will become the final component of the resource name. + // If not provided, the system will generate one. + string identity_provider_id = 2 [(google.api.field_behavior) = OPTIONAL]; +} + +message UpdateIdentityProviderRequest { + // Required. The identity provider to update. + IdentityProvider identity_provider = 1 [(google.api.field_behavior) = REQUIRED]; + + // Required. The update mask applies to the resource. Only the top level fields of + // IdentityProvider are supported. + google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = REQUIRED]; +} + +message DeleteIdentityProviderRequest { + // Required. The resource name of the identity provider to delete. + // Format: identityProviders/{idp} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/IdentityProvider"} + ]; +} diff --git a/proto/api/v1/inbox_service.proto b/proto/api/v1/inbox_service.proto new file mode 100644 index 0000000..4571a86 --- /dev/null +++ b/proto/api/v1/inbox_service.proto @@ -0,0 +1,149 @@ +syntax = "proto3"; + +package memos.api.v1; + +import "google/api/annotations.proto"; +import "google/api/client.proto"; +import "google/api/field_behavior.proto"; +import "google/api/resource.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/field_mask.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = "gen/api/v1"; + +service InboxService { + // ListInboxes lists inboxes for a user. + rpc ListInboxes(ListInboxesRequest) returns (ListInboxesResponse) { + option (google.api.http) = {get: "/api/v1/{parent=users/*}/inboxes"}; + option (google.api.method_signature) = "parent"; + } + // UpdateInbox updates an inbox. + rpc UpdateInbox(UpdateInboxRequest) returns (Inbox) { + option (google.api.http) = { + patch: "/api/v1/{inbox.name=inboxes/*}" + body: "inbox" + }; + option (google.api.method_signature) = "inbox,update_mask"; + } + // DeleteInbox deletes an inbox. + rpc DeleteInbox(DeleteInboxRequest) returns (google.protobuf.Empty) { + option (google.api.http) = {delete: "/api/v1/{name=inboxes/*}"}; + option (google.api.method_signature) = "name"; + } +} + +message Inbox { + option (google.api.resource) = { + type: "memos.api.v1/Inbox" + pattern: "inboxes/{inbox}" + name_field: "name" + singular: "inbox" + plural: "inboxes" + }; + + // The resource name of the inbox. + // Format: inboxes/{inbox} + string name = 1 [(google.api.field_behavior) = IDENTIFIER]; + + // The sender of the inbox notification. + // Format: users/{user} + string sender = 2 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // The receiver of the inbox notification. + // Format: users/{user} + string receiver = 3 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // The status of the inbox notification. + Status status = 4 [(google.api.field_behavior) = OPTIONAL]; + + // Output only. The creation timestamp. + google.protobuf.Timestamp create_time = 5 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // The type of the inbox notification. + Type type = 6 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // Optional. The activity ID associated with this inbox notification. + optional int32 activity_id = 7 [(google.api.field_behavior) = OPTIONAL]; + + // Status enumeration for inbox notifications. + enum Status { + // Unspecified status. + STATUS_UNSPECIFIED = 0; + // The notification is unread. + UNREAD = 1; + // The notification is archived. + ARCHIVED = 2; + } + + // Type enumeration for inbox notifications. + enum Type { + // Unspecified type. + TYPE_UNSPECIFIED = 0; + // Memo comment notification. + MEMO_COMMENT = 1; + // Version update notification. + VERSION_UPDATE = 2; + } +} + +message ListInboxesRequest { + // Required. The parent resource whose inboxes will be listed. + // Format: users/{user} + string parent = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/User"} + ]; + + // Optional. The maximum number of inboxes to return. + // The service may return fewer than this value. + // If unspecified, at most 50 inboxes will be returned. + // The maximum value is 1000; values above 1000 will be coerced to 1000. + int32 page_size = 2 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. A page token, received from a previous `ListInboxes` call. + // Provide this to retrieve the subsequent page. + string page_token = 3 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Filter to apply to the list results. + // Example: "status=UNREAD" or "type=MEMO_COMMENT" + // Supported operators: =, != + // Supported fields: status, type, sender, create_time + string filter = 4 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. The order to sort results by. + // Example: "create_time desc" or "status asc" + string order_by = 5 [(google.api.field_behavior) = OPTIONAL]; +} + +message ListInboxesResponse { + // The list of inboxes. + repeated Inbox inboxes = 1; + + // A token that can be sent as `page_token` to retrieve the next page. + // If this field is omitted, there are no subsequent pages. + string next_page_token = 2; + + // The total count of inboxes (may be approximate). + int32 total_size = 3; +} + +message UpdateInboxRequest { + // Required. The inbox to update. + Inbox inbox = 1 [(google.api.field_behavior) = REQUIRED]; + + // Required. The list of fields to update. + google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = REQUIRED]; + + // Optional. If set to true, allows updating missing fields. + bool allow_missing = 3 [(google.api.field_behavior) = OPTIONAL]; +} + +message DeleteInboxRequest { + // Required. The resource name of the inbox to delete. + // Format: inboxes/{inbox} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/Inbox"} + ]; +} diff --git a/proto/api/v1/markdown_service.proto b/proto/api/v1/markdown_service.proto new file mode 100644 index 0000000..8ac0e8e --- /dev/null +++ b/proto/api/v1/markdown_service.proto @@ -0,0 +1,329 @@ +syntax = "proto3"; + +package memos.api.v1; + +import "google/api/annotations.proto"; +import "google/api/field_behavior.proto"; + +option go_package = "gen/api/v1"; + +service MarkdownService { + // ParseMarkdown parses the given markdown content and returns a list of nodes. + // This is a utility method that transforms markdown text into structured nodes. + rpc ParseMarkdown(ParseMarkdownRequest) returns (ParseMarkdownResponse) { + option (google.api.http) = { + post: "/api/v1/markdown:parse" + body: "*" + }; + } + + // RestoreMarkdownNodes restores the given nodes to markdown content. + // This is the inverse operation of ParseMarkdown. + rpc RestoreMarkdownNodes(RestoreMarkdownNodesRequest) returns (RestoreMarkdownNodesResponse) { + option (google.api.http) = { + post: "/api/v1/markdown:restore" + body: "*" + }; + } + + // StringifyMarkdownNodes stringify the given nodes to plain text content. + // This removes all markdown formatting and returns plain text. + rpc StringifyMarkdownNodes(StringifyMarkdownNodesRequest) returns (StringifyMarkdownNodesResponse) { + option (google.api.http) = { + post: "/api/v1/markdown:stringify" + body: "*" + }; + } + + // GetLinkMetadata returns metadata for a given link. + // This is useful for generating link previews. + rpc GetLinkMetadata(GetLinkMetadataRequest) returns (LinkMetadata) { + option (google.api.http) = {get: "/api/v1/markdown/links:getMetadata"}; + } +} + +message ParseMarkdownRequest { + // The markdown content to parse. + string markdown = 1 [(google.api.field_behavior) = REQUIRED]; +} + +message ParseMarkdownResponse { + // The parsed markdown nodes. + repeated Node nodes = 1; +} + +message RestoreMarkdownNodesRequest { + // The nodes to restore to markdown content. + repeated Node nodes = 1 [(google.api.field_behavior) = REQUIRED]; +} + +message RestoreMarkdownNodesResponse { + // The restored markdown content. + string markdown = 1; +} + +message StringifyMarkdownNodesRequest { + // The nodes to stringify to plain text. + repeated Node nodes = 1 [(google.api.field_behavior) = REQUIRED]; +} + +message StringifyMarkdownNodesResponse { + // The plain text content. + string plain_text = 1; +} + +message GetLinkMetadataRequest { + // The link URL to get metadata for. + string link = 1 [(google.api.field_behavior) = REQUIRED]; +} + +message LinkMetadata { + // The title of the linked page. + string title = 1; + + // The description of the linked page. + string description = 2; + + // The URL of the preview image for the linked page. + string image = 3; +} + +enum NodeType { + NODE_UNSPECIFIED = 0; + + // Block nodes. + LINE_BREAK = 1; + PARAGRAPH = 2; + CODE_BLOCK = 3; + HEADING = 4; + HORIZONTAL_RULE = 5; + BLOCKQUOTE = 6; + LIST = 7; + ORDERED_LIST_ITEM = 8; + UNORDERED_LIST_ITEM = 9; + TASK_LIST_ITEM = 10; + MATH_BLOCK = 11; + TABLE = 12; + EMBEDDED_CONTENT = 13; + + // Inline nodes. + TEXT = 51; + BOLD = 52; + ITALIC = 53; + BOLD_ITALIC = 54; + CODE = 55; + IMAGE = 56; + LINK = 57; + AUTO_LINK = 58; + TAG = 59; + STRIKETHROUGH = 60; + ESCAPING_CHARACTER = 61; + MATH = 62; + HIGHLIGHT = 63; + SUBSCRIPT = 64; + SUPERSCRIPT = 65; + REFERENCED_CONTENT = 66; + SPOILER = 67; + HTML_ELEMENT = 68; +} + +message Node { + NodeType type = 1; + + oneof node { + // Block nodes. + LineBreakNode line_break_node = 11; + ParagraphNode paragraph_node = 12; + CodeBlockNode code_block_node = 13; + HeadingNode heading_node = 14; + HorizontalRuleNode horizontal_rule_node = 15; + BlockquoteNode blockquote_node = 16; + ListNode list_node = 17; + OrderedListItemNode ordered_list_item_node = 18; + UnorderedListItemNode unordered_list_item_node = 19; + TaskListItemNode task_list_item_node = 20; + MathBlockNode math_block_node = 21; + TableNode table_node = 22; + EmbeddedContentNode embedded_content_node = 23; + + // Inline nodes. + TextNode text_node = 51; + BoldNode bold_node = 52; + ItalicNode italic_node = 53; + BoldItalicNode bold_italic_node = 54; + CodeNode code_node = 55; + ImageNode image_node = 56; + LinkNode link_node = 57; + AutoLinkNode auto_link_node = 58; + TagNode tag_node = 59; + StrikethroughNode strikethrough_node = 60; + EscapingCharacterNode escaping_character_node = 61; + MathNode math_node = 62; + HighlightNode highlight_node = 63; + SubscriptNode subscript_node = 64; + SuperscriptNode superscript_node = 65; + ReferencedContentNode referenced_content_node = 66; + SpoilerNode spoiler_node = 67; + HTMLElementNode html_element_node = 68; + } +} + +message LineBreakNode {} + +message ParagraphNode { + repeated Node children = 1; +} + +message CodeBlockNode { + string language = 1; + string content = 2; +} + +message HeadingNode { + int32 level = 1; + repeated Node children = 2; +} + +message HorizontalRuleNode { + string symbol = 1; +} + +message BlockquoteNode { + repeated Node children = 1; +} + +message ListNode { + enum Kind { + KIND_UNSPECIFIED = 0; + ORDERED = 1; + UNORDERED = 2; + DESCRIPTION = 3; + } + Kind kind = 1; + int32 indent = 2; + repeated Node children = 3; +} + +message OrderedListItemNode { + string number = 1; + int32 indent = 2; + repeated Node children = 3; +} + +message UnorderedListItemNode { + string symbol = 1; + int32 indent = 2; + repeated Node children = 3; +} + +message TaskListItemNode { + string symbol = 1; + int32 indent = 2; + bool complete = 3; + repeated Node children = 4; +} + +message MathBlockNode { + string content = 1; +} + +message TableNode { + repeated Node header = 1; + repeated string delimiter = 2; + + message Row { + repeated Node cells = 1; + } + repeated Row rows = 3; +} + +message EmbeddedContentNode { + // The resource name of the embedded content. + string resource_name = 1; + + // Additional parameters for the embedded content. + string params = 2; +} + +message TextNode { + string content = 1; +} + +message BoldNode { + string symbol = 1; + repeated Node children = 2; +} + +message ItalicNode { + string symbol = 1; + repeated Node children = 2; +} + +message BoldItalicNode { + string symbol = 1; + string content = 2; +} + +message CodeNode { + string content = 1; +} + +message ImageNode { + string alt_text = 1; + string url = 2; +} + +message LinkNode { + repeated Node content = 1; + string url = 2; +} + +message AutoLinkNode { + string url = 1; + bool is_raw_text = 2; +} + +message TagNode { + string content = 1; +} + +message StrikethroughNode { + string content = 1; +} + +message EscapingCharacterNode { + string symbol = 1; +} + +message MathNode { + string content = 1; +} + +message HighlightNode { + string content = 1; +} + +message SubscriptNode { + string content = 1; +} + +message SuperscriptNode { + string content = 1; +} + +message ReferencedContentNode { + // The resource name of the referenced content. + string resource_name = 1; + + // Additional parameters for the referenced content. + string params = 2; +} + +message SpoilerNode { + string content = 1; +} + +message HTMLElementNode { + string tag_name = 1; + map attributes = 2; +} diff --git a/proto/api/v1/memo_service.proto b/proto/api/v1/memo_service.proto new file mode 100644 index 0000000..883acbe --- /dev/null +++ b/proto/api/v1/memo_service.proto @@ -0,0 +1,714 @@ +syntax = "proto3"; + +package memos.api.v1; + +import "api/v1/attachment_service.proto"; +import "api/v1/common.proto"; +import "api/v1/markdown_service.proto"; +import "google/api/annotations.proto"; +import "google/api/client.proto"; +import "google/api/field_behavior.proto"; +import "google/api/resource.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/field_mask.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = "gen/api/v1"; + +service MemoService { + // CreateMemo creates a memo. + rpc CreateMemo(CreateMemoRequest) returns (Memo) { + option (google.api.http) = { + post: "/api/v1/memos" + body: "memo" + }; + option (google.api.method_signature) = "memo"; + } + // ListMemos lists memos with pagination and filter. + rpc ListMemos(ListMemosRequest) returns (ListMemosResponse) { + option (google.api.http) = { + get: "/api/v1/memos" + additional_bindings: {get: "/api/v1/{parent=users/*}/memos"} + }; + option (google.api.method_signature) = ""; + option (google.api.method_signature) = "parent"; + } + // GetMemo gets a memo. + rpc GetMemo(GetMemoRequest) returns (Memo) { + option (google.api.http) = {get: "/api/v1/{name=memos/*}"}; + option (google.api.method_signature) = "name"; + } + // UpdateMemo updates a memo. + rpc UpdateMemo(UpdateMemoRequest) returns (Memo) { + option (google.api.http) = { + patch: "/api/v1/{memo.name=memos/*}" + body: "memo" + }; + option (google.api.method_signature) = "memo,update_mask"; + } + // DeleteMemo deletes a memo. + rpc DeleteMemo(DeleteMemoRequest) returns (google.protobuf.Empty) { + option (google.api.http) = {delete: "/api/v1/{name=memos/*}"}; + option (google.api.method_signature) = "name"; + } + // RenameMemoTag renames a tag for a memo. + rpc RenameMemoTag(RenameMemoTagRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { + patch: "/api/v1/{parent=memos/*}/tags:rename" + body: "*" + }; + option (google.api.method_signature) = "parent,old_tag,new_tag"; + } + // DeleteMemoTag deletes a tag for a memo. + rpc DeleteMemoTag(DeleteMemoTagRequest) returns (google.protobuf.Empty) { + option (google.api.http) = {delete: "/api/v1/{parent=memos/*}/tags/{tag}"}; + option (google.api.method_signature) = "parent,tag"; + } + // SetMemoAttachments sets attachments for a memo. + rpc SetMemoAttachments(SetMemoAttachmentsRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { + patch: "/api/v1/{name=memos/*}/attachments" + body: "*" + }; + option (google.api.method_signature) = "name"; + } + // ListMemoAttachments lists attachments for a memo. + rpc ListMemoAttachments(ListMemoAttachmentsRequest) returns (ListMemoAttachmentsResponse) { + option (google.api.http) = {get: "/api/v1/{name=memos/*}/attachments"}; + option (google.api.method_signature) = "name"; + } + // SetMemoRelations sets relations for a memo. + rpc SetMemoRelations(SetMemoRelationsRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { + patch: "/api/v1/{name=memos/*}/relations" + body: "*" + }; + option (google.api.method_signature) = "name"; + } + // ListMemoRelations lists relations for a memo. + rpc ListMemoRelations(ListMemoRelationsRequest) returns (ListMemoRelationsResponse) { + option (google.api.http) = {get: "/api/v1/{name=memos/*}/relations"}; + option (google.api.method_signature) = "name"; + } + // CreateMemoComment creates a comment for a memo. + rpc CreateMemoComment(CreateMemoCommentRequest) returns (Memo) { + option (google.api.http) = { + post: "/api/v1/{name=memos/*}/comments" + body: "comment" + }; + option (google.api.method_signature) = "name,comment"; + } + // ListMemoComments lists comments for a memo. + rpc ListMemoComments(ListMemoCommentsRequest) returns (ListMemoCommentsResponse) { + option (google.api.http) = {get: "/api/v1/{name=memos/*}/comments"}; + option (google.api.method_signature) = "name"; + } + // ListMemoReactions lists reactions for a memo. + rpc ListMemoReactions(ListMemoReactionsRequest) returns (ListMemoReactionsResponse) { + option (google.api.http) = {get: "/api/v1/{name=memos/*}/reactions"}; + option (google.api.method_signature) = "name"; + } + // UpsertMemoReaction upserts a reaction for a memo. + rpc UpsertMemoReaction(UpsertMemoReactionRequest) returns (Reaction) { + option (google.api.http) = { + post: "/api/v1/{name=memos/*}/reactions" + body: "*" + }; + option (google.api.method_signature) = "name"; + } + // DeleteMemoReaction deletes a reaction for a memo. + rpc DeleteMemoReaction(DeleteMemoReactionRequest) returns (google.protobuf.Empty) { + option (google.api.http) = {delete: "/api/v1/{name=reactions/*}"}; + option (google.api.method_signature) = "name"; + } + // ExportMemos exports memos for the current user + rpc ExportMemos(ExportMemosRequest) returns (ExportMemosResponse) { + option (google.api.http) = { + post: "/api/v1/memos:export" + body: "*" + }; + } + // ImportMemos imports memos from provided data + rpc ImportMemos(ImportMemosRequest) returns (ImportMemosResponse) { + option (google.api.http) = { + post: "/api/v1/memos:import" + body: "*" + }; + } +} + +enum Visibility { + VISIBILITY_UNSPECIFIED = 0; + PRIVATE = 1; + PROTECTED = 2; + PUBLIC = 3; +} + +message Reaction { + option (google.api.resource) = { + type: "memos.api.v1/Reaction" + pattern: "reactions/{reaction}" + name_field: "name" + singular: "reaction" + plural: "reactions" + }; + + // The resource name of the reaction. + // Format: reactions/{reaction} + string name = 1 [ + (google.api.field_behavior) = OUTPUT_ONLY, + (google.api.field_behavior) = IDENTIFIER + ]; + + // The resource name of the creator. + // Format: users/{user} + string creator = 2 [ + (google.api.field_behavior) = OUTPUT_ONLY, + (google.api.resource_reference) = {type: "memos.api.v1/User"} + ]; + + // The resource name of the content. + // For memo reactions, this should be the memo's resource name. + // Format: memos/{memo} + string content_id = 3 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/Memo"} + ]; + + // Required. The type of reaction (e.g., "👍", "❤️", "😄"). + string reaction_type = 4 [(google.api.field_behavior) = REQUIRED]; + + // Output only. The creation timestamp. + google.protobuf.Timestamp create_time = 5 [(google.api.field_behavior) = OUTPUT_ONLY]; +} + +message Memo { + option (google.api.resource) = { + type: "memos.api.v1/Memo" + pattern: "memos/{memo}" + name_field: "name" + singular: "memo" + plural: "memos" + }; + + // The resource name of the memo. + // Format: memos/{memo}, memo is the user defined id or uuid. + string name = 1 [(google.api.field_behavior) = IDENTIFIER]; + + // The state of the memo. + State state = 2 [(google.api.field_behavior) = REQUIRED]; + + // The name of the creator. + // Format: users/{user} + string creator = 3 [ + (google.api.field_behavior) = OUTPUT_ONLY, + (google.api.resource_reference) = {type: "memos.api.v1/User"} + ]; + + // Output only. The creation timestamp. + google.protobuf.Timestamp create_time = 4 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // Output only. The last update timestamp. + google.protobuf.Timestamp update_time = 5 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // The display timestamp of the memo. + google.protobuf.Timestamp display_time = 6 [(google.api.field_behavior) = OPTIONAL]; + + // Required. The content of the memo in Markdown format. + string content = 7 [(google.api.field_behavior) = REQUIRED]; + + // Output only. The parsed nodes from the content. + repeated Node nodes = 8 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // The visibility of the memo. + Visibility visibility = 9 [(google.api.field_behavior) = REQUIRED]; + + // Output only. The tags extracted from the content. + repeated string tags = 10 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // Whether the memo is pinned. + bool pinned = 11 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. The attachments of the memo. + repeated Attachment attachments = 12 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. The relations of the memo. + repeated MemoRelation relations = 13 [(google.api.field_behavior) = OPTIONAL]; + + // Output only. The reactions to the memo. + repeated Reaction reactions = 14 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // Output only. The computed properties of the memo. + Property property = 15 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // Output only. The name of the parent memo. + // Format: memos/{memo} + optional string parent = 16 [ + (google.api.field_behavior) = OUTPUT_ONLY, + (google.api.resource_reference) = {type: "memos.api.v1/Memo"} + ]; + + // Output only. The snippet of the memo content. Plain text only. + string snippet = 17 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // Optional. The location of the memo. + optional Location location = 18 [(google.api.field_behavior) = OPTIONAL]; + + // Computed properties of a memo. + message Property { + bool has_link = 1; + bool has_task_list = 2; + bool has_code = 3; + bool has_incomplete_tasks = 4; + } +} + +message Location { + // A placeholder text for the location. + string placeholder = 1 [(google.api.field_behavior) = OPTIONAL]; + + // The latitude of the location. + double latitude = 2 [(google.api.field_behavior) = OPTIONAL]; + + // The longitude of the location. + double longitude = 3 [(google.api.field_behavior) = OPTIONAL]; +} + +message CreateMemoRequest { + // Required. The memo to create. + Memo memo = 1 [(google.api.field_behavior) = REQUIRED]; + + // Optional. The memo ID to use for this memo. + // If empty, a unique ID will be generated. + string memo_id = 2 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. If set, validate the request but don't actually create the memo. + bool validate_only = 3 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. An idempotency token. + string request_id = 4 [(google.api.field_behavior) = OPTIONAL]; +} + +message ListMemosRequest { + // Optional. The parent is the owner of the memos. + // If not specified or `users/-`, it will list all memos. + // Format: users/{user} + string parent = 1 [ + (google.api.field_behavior) = OPTIONAL, + (google.api.resource_reference) = {type: "memos.api.v1/User"} + ]; + + // Optional. The maximum number of memos to return. + // The service may return fewer than this value. + // If unspecified, at most 50 memos will be returned. + // The maximum value is 1000; values above 1000 will be coerced to 1000. + int32 page_size = 2 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. A page token, received from a previous `ListMemos` call. + // Provide this to retrieve the subsequent page. + string page_token = 3 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. The state of the memos to list. + // Default to `NORMAL`. Set to `ARCHIVED` to list archived memos. + State state = 4 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. The order to sort results by. + // Default to "display_time desc". + // Example: "display_time desc" or "create_time asc" + string order_by = 5 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Filter to apply to the list results. + // Filter is a CEL expression to filter memos. + // Refer to `Shortcut.filter`. + string filter = 6 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. If true, show deleted memos in the response. + bool show_deleted = 7 [(google.api.field_behavior) = OPTIONAL]; + + // [Deprecated] Old filter contains some specific conditions to filter memos. + // Format: "creator == 'users/{user}' && visibilities == ['PUBLIC', 'PROTECTED']" + string old_filter = 8; +} + +message ListMemosResponse { + // The list of memos. + repeated Memo memos = 1; + + // A token that can be sent as `page_token` to retrieve the next page. + // If this field is omitted, there are no subsequent pages. + string next_page_token = 2; + + // The total count of memos (may be approximate). + int32 total_size = 3; +} + +message GetMemoRequest { + // Required. The resource name of the memo. + // Format: memos/{memo} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/Memo"} + ]; + + // Optional. The fields to return in the response. + // If not specified, all fields are returned. + google.protobuf.FieldMask read_mask = 2 [(google.api.field_behavior) = OPTIONAL]; +} + +message UpdateMemoRequest { + // Required. The memo to update. + // The `name` field is required. + Memo memo = 1 [(google.api.field_behavior) = REQUIRED]; + + // Required. The list of fields to update. + google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = REQUIRED]; + + // Optional. If set to true, allows updating sensitive fields. + bool allow_missing = 3 [(google.api.field_behavior) = OPTIONAL]; +} + +message DeleteMemoRequest { + // Required. The resource name of the memo to delete. + // Format: memos/{memo} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/Memo"} + ]; + + // Optional. If set to true, the memo will be deleted even if it has associated data. + bool force = 2 [(google.api.field_behavior) = OPTIONAL]; +} + +message RenameMemoTagRequest { + // Required. The parent, who owns the tags. + // Format: memos/{memo}. Use "memos/-" to rename all tags. + string parent = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/Memo"} + ]; + + // Required. The old tag name to rename. + string old_tag = 2 [(google.api.field_behavior) = REQUIRED]; + + // Required. The new tag name. + string new_tag = 3 [(google.api.field_behavior) = REQUIRED]; +} + +message DeleteMemoTagRequest { + // Required. The parent, who owns the tags. + // Format: memos/{memo}. Use "memos/-" to delete all tags. + string parent = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/Memo"} + ]; + + // Required. The tag name to delete. + string tag = 2 [(google.api.field_behavior) = REQUIRED]; + + // Optional. Whether to delete related memos. + bool delete_related_memos = 3 [(google.api.field_behavior) = OPTIONAL]; +} + +message SetMemoAttachmentsRequest { + // Required. The resource name of the memo. + // Format: memos/{memo} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/Memo"} + ]; + + // Required. The attachments to set for the memo. + repeated Attachment attachments = 2 [(google.api.field_behavior) = REQUIRED]; +} + +message ListMemoAttachmentsRequest { + // Required. The resource name of the memo. + // Format: memos/{memo} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/Memo"} + ]; + + // Optional. The maximum number of attachments to return. + int32 page_size = 2 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. A page token for pagination. + string page_token = 3 [(google.api.field_behavior) = OPTIONAL]; +} + +message ListMemoAttachmentsResponse { + // The list of attachments. + repeated Attachment attachments = 1; + + // A token for the next page of results. + string next_page_token = 2; + + // The total count of attachments. + int32 total_size = 3; +} + +message MemoRelation { + // The memo in the relation. + Memo memo = 1 [(google.api.field_behavior) = REQUIRED]; + + // The related memo. + Memo related_memo = 2 [(google.api.field_behavior) = REQUIRED]; + + // The type of the relation. + enum Type { + TYPE_UNSPECIFIED = 0; + REFERENCE = 1; + COMMENT = 2; + } + Type type = 3 [(google.api.field_behavior) = REQUIRED]; + + // Memo reference in relations. + message Memo { + // The resource name of the memo. + // Format: memos/{memo} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/Memo"} + ]; + + // Output only. The snippet of the memo content. Plain text only. + string snippet = 2 [(google.api.field_behavior) = OUTPUT_ONLY]; + } +} + +message SetMemoRelationsRequest { + // Required. The resource name of the memo. + // Format: memos/{memo} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/Memo"} + ]; + + // Required. The relations to set for the memo. + repeated MemoRelation relations = 2 [(google.api.field_behavior) = REQUIRED]; +} + +message ListMemoRelationsRequest { + // Required. The resource name of the memo. + // Format: memos/{memo} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/Memo"} + ]; + + // Optional. The maximum number of relations to return. + int32 page_size = 2 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. A page token for pagination. + string page_token = 3 [(google.api.field_behavior) = OPTIONAL]; +} + +message ListMemoRelationsResponse { + // The list of relations. + repeated MemoRelation relations = 1; + + // A token for the next page of results. + string next_page_token = 2; + + // The total count of relations. + int32 total_size = 3; +} + +message CreateMemoCommentRequest { + // Required. The resource name of the memo. + // Format: memos/{memo} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/Memo"} + ]; + + // Required. The comment to create. + Memo comment = 2 [(google.api.field_behavior) = REQUIRED]; + + // Optional. The comment ID to use. + string comment_id = 3 [(google.api.field_behavior) = OPTIONAL]; +} + +message ListMemoCommentsRequest { + // Required. The resource name of the memo. + // Format: memos/{memo} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/Memo"} + ]; + + // Optional. The maximum number of comments to return. + int32 page_size = 2 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. A page token for pagination. + string page_token = 3 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. The order to sort results by. + string order_by = 4 [(google.api.field_behavior) = OPTIONAL]; +} + +message ListMemoCommentsResponse { + // The list of comment memos. + repeated Memo memos = 1; + + // A token for the next page of results. + string next_page_token = 2; + + // The total count of comments. + int32 total_size = 3; +} + +message ListMemoReactionsRequest { + // Required. The resource name of the memo. + // Format: memos/{memo} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/Memo"} + ]; + + // Optional. The maximum number of reactions to return. + int32 page_size = 2 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. A page token for pagination. + string page_token = 3 [(google.api.field_behavior) = OPTIONAL]; +} + +message ListMemoReactionsResponse { + // The list of reactions. + repeated Reaction reactions = 1; + + // A token for the next page of results. + string next_page_token = 2; + + // The total count of reactions. + int32 total_size = 3; +} + +message UpsertMemoReactionRequest { + // Required. The resource name of the memo. + // Format: memos/{memo} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/Memo"} + ]; + + // Required. The reaction to upsert. + Reaction reaction = 2 [(google.api.field_behavior) = REQUIRED]; +} + +message DeleteMemoReactionRequest { + // Required. The resource name of the reaction to delete. + // Format: reactions/{reaction} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/Reaction"} + ]; +} + +// Export/Import Messages + +message ExportMemosRequest { + // Optional. Format for the export (currently only "json" is supported) + string format = 1 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Filter to apply to memos for export + // Uses the same filter format as ListMemosRequest + string filter = 2 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Whether to exclude archived memos from export + // Default: false (include archived memos) + bool exclude_archived = 3 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Whether to include attachments in the export + // Default: true + bool include_attachments = 4 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Whether to include memo relations in the export + // Default: true + bool include_relations = 5 [(google.api.field_behavior) = OPTIONAL]; +} + +message ExportMemosResponse { + // The exported data as bytes + bytes data = 1; + + // The format of the exported data + string format = 2; + + // Suggested filename for the export + string filename = 3; + + // Number of memos exported + int32 memo_count = 4; + + // Size of the export data in bytes + int64 size_bytes = 5; +} + +message ImportMemosRequest { + // Required. The data to import (JSON format) + bytes data = 1 [(google.api.field_behavior) = REQUIRED]; + + // Optional. Format of the import data (currently only "json" is supported) + string format = 2 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Whether to overwrite existing memos with the same UID + // Default: false (skip existing memos) + bool overwrite_existing = 3 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Whether to validate only (dry run mode) + // If true, the import will be validated but no data will be created + bool validate_only = 4 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Whether to preserve original timestamps + // Default: true + bool preserve_timestamps = 5 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Whether to skip importing attachments + // Default: false (import attachments if present) + bool skip_attachments = 6 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Whether to skip importing memo relations + // Default: false (import relations if present) + bool skip_relations = 7 [(google.api.field_behavior) = OPTIONAL]; +} + +message ImportMemosResponse { + // Number of memos successfully imported + int32 imported_count = 1; + + // Number of memos skipped (due to errors or existing UIDs) + int32 skipped_count = 2; + + // Number of memos that failed validation (in validate_only mode) + int32 validation_errors = 3; + + // List of error messages for failed imports + repeated string errors = 4; + + // List of warning messages for potential issues + repeated string warnings = 5; + + // Summary of the import operation + ImportSummary summary = 6; +} + +message ImportSummary { + // Total number of memos in the import data + int32 total_memos = 1; + + // Number of new memos created + int32 created_count = 2; + + // Number of existing memos updated + int32 updated_count = 3; + + // Number of attachments imported + int32 attachments_imported = 4; + + // Number of relations imported + int32 relations_imported = 5; + + // Import duration in milliseconds + int64 duration_ms = 6; +} diff --git a/proto/api/v1/shortcut_service.proto b/proto/api/v1/shortcut_service.proto new file mode 100644 index 0000000..7ebb878 --- /dev/null +++ b/proto/api/v1/shortcut_service.proto @@ -0,0 +1,124 @@ +syntax = "proto3"; + +package memos.api.v1; + +import "google/api/annotations.proto"; +import "google/api/client.proto"; +import "google/api/field_behavior.proto"; +import "google/api/resource.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/field_mask.proto"; + +option go_package = "gen/api/v1"; + +service ShortcutService { + // ListShortcuts returns a list of shortcuts for a user. + rpc ListShortcuts(ListShortcutsRequest) returns (ListShortcutsResponse) { + option (google.api.http) = {get: "/api/v1/{parent=users/*}/shortcuts"}; + option (google.api.method_signature) = "parent"; + } + + // GetShortcut gets a shortcut by name. + rpc GetShortcut(GetShortcutRequest) returns (Shortcut) { + option (google.api.http) = {get: "/api/v1/{name=users/*/shortcuts/*}"}; + option (google.api.method_signature) = "name"; + } + + // CreateShortcut creates a new shortcut for a user. + rpc CreateShortcut(CreateShortcutRequest) returns (Shortcut) { + option (google.api.http) = { + post: "/api/v1/{parent=users/*}/shortcuts" + body: "shortcut" + }; + option (google.api.method_signature) = "parent,shortcut"; + } + + // UpdateShortcut updates a shortcut for a user. + rpc UpdateShortcut(UpdateShortcutRequest) returns (Shortcut) { + option (google.api.http) = { + patch: "/api/v1/{shortcut.name=users/*/shortcuts/*}" + body: "shortcut" + }; + option (google.api.method_signature) = "shortcut,update_mask"; + } + + // DeleteShortcut deletes a shortcut for a user. + rpc DeleteShortcut(DeleteShortcutRequest) returns (google.protobuf.Empty) { + option (google.api.http) = {delete: "/api/v1/{name=users/*/shortcuts/*}"}; + option (google.api.method_signature) = "name"; + } +} + +message Shortcut { + option (google.api.resource) = { + type: "memos.api.v1/Shortcut" + pattern: "users/{user}/shortcuts/{shortcut}" + singular: "shortcut" + plural: "shortcuts" + }; + + // The resource name of the shortcut. + // Format: users/{user}/shortcuts/{shortcut} + string name = 1 [(google.api.field_behavior) = IDENTIFIER]; + + // The title of the shortcut. + string title = 2 [(google.api.field_behavior) = REQUIRED]; + + // The filter expression for the shortcut. + string filter = 3 [(google.api.field_behavior) = OPTIONAL]; +} + +message ListShortcutsRequest { + // Required. The parent resource where shortcuts are listed. + // Format: users/{user} + string parent = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {child_type: "memos.api.v1/Shortcut"} + ]; +} + +message ListShortcutsResponse { + // The list of shortcuts. + repeated Shortcut shortcuts = 1; +} + +message GetShortcutRequest { + // Required. The resource name of the shortcut to retrieve. + // Format: users/{user}/shortcuts/{shortcut} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/Shortcut"} + ]; +} + +message CreateShortcutRequest { + // Required. The parent resource where this shortcut will be created. + // Format: users/{user} + string parent = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {child_type: "memos.api.v1/Shortcut"} + ]; + + // Required. The shortcut to create. + Shortcut shortcut = 2 [(google.api.field_behavior) = REQUIRED]; + + // Optional. If set, validate the request, but do not actually create the shortcut. + bool validate_only = 3 [(google.api.field_behavior) = OPTIONAL]; +} + +message UpdateShortcutRequest { + // Required. The shortcut resource which replaces the resource on the server. + Shortcut shortcut = 1 [(google.api.field_behavior) = REQUIRED]; + + // Optional. The list of fields to update. + google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = OPTIONAL]; +} + +message DeleteShortcutRequest { + // Required. The resource name of the shortcut to delete. + // Format: users/{user}/shortcuts/{shortcut} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/Shortcut"} + ]; +} diff --git a/proto/api/v1/user_service.proto b/proto/api/v1/user_service.proto new file mode 100644 index 0000000..b0b0ab1 --- /dev/null +++ b/proto/api/v1/user_service.proto @@ -0,0 +1,554 @@ +syntax = "proto3"; + +package memos.api.v1; + +import "api/v1/common.proto"; +import "google/api/annotations.proto"; +import "google/api/client.proto"; +import "google/api/field_behavior.proto"; +import "google/api/httpbody.proto"; +import "google/api/resource.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/field_mask.proto"; +import "google/protobuf/timestamp.proto"; + +option go_package = "gen/api/v1"; + +service UserService { + // ListUsers returns a list of users. + rpc ListUsers(ListUsersRequest) returns (ListUsersResponse) { + option (google.api.http) = {get: "/api/v1/users"}; + } + + // GetUser gets a user by name. + rpc GetUser(GetUserRequest) returns (User) { + option (google.api.http) = {get: "/api/v1/{name=users/*}"}; + option (google.api.method_signature) = "name"; + } + + // CreateUser creates a new user. + rpc CreateUser(CreateUserRequest) returns (User) { + option (google.api.http) = { + post: "/api/v1/users" + body: "user" + }; + option (google.api.method_signature) = "user"; + } + + // UpdateUser updates a user. + rpc UpdateUser(UpdateUserRequest) returns (User) { + option (google.api.http) = { + patch: "/api/v1/{user.name=users/*}" + body: "user" + }; + option (google.api.method_signature) = "user,update_mask"; + } + + // DeleteUser deletes a user. + rpc DeleteUser(DeleteUserRequest) returns (google.protobuf.Empty) { + option (google.api.http) = {delete: "/api/v1/{name=users/*}"}; + option (google.api.method_signature) = "name"; + } + + // SearchUsers searches for users based on query. + rpc SearchUsers(SearchUsersRequest) returns (SearchUsersResponse) { + option (google.api.http) = {get: "/api/v1/users:search"}; + option (google.api.method_signature) = "query"; + } + + // GetUserAvatar gets the avatar of a user. + rpc GetUserAvatar(GetUserAvatarRequest) returns (google.api.HttpBody) { + option (google.api.http) = {get: "/api/v1/{name=users/*}/avatar"}; + option (google.api.method_signature) = "name"; + } + + // ListAllUserStats returns statistics for all users. + rpc ListAllUserStats(ListAllUserStatsRequest) returns (ListAllUserStatsResponse) { + option (google.api.http) = {get: "/api/v1/users:stats"}; + } + + // GetUserStats returns statistics for a specific user. + rpc GetUserStats(GetUserStatsRequest) returns (UserStats) { + option (google.api.http) = {get: "/api/v1/{name=users/*}:getStats"}; + option (google.api.method_signature) = "name"; + } + + // GetUserSetting returns the user setting. + rpc GetUserSetting(GetUserSettingRequest) returns (UserSetting) { + option (google.api.http) = {get: "/api/v1/{name=users/*}:getSetting"}; + option (google.api.method_signature) = "name"; + } + + // UpdateUserSetting updates the user setting. + rpc UpdateUserSetting(UpdateUserSettingRequest) returns (UserSetting) { + option (google.api.http) = { + patch: "/api/v1/{setting.name=users/*}:updateSetting" + body: "setting" + }; + option (google.api.method_signature) = "setting,update_mask"; + } + + // ListUserAccessTokens returns a list of access tokens for a user. + rpc ListUserAccessTokens(ListUserAccessTokensRequest) returns (ListUserAccessTokensResponse) { + option (google.api.http) = {get: "/api/v1/{parent=users/*}/accessTokens"}; + option (google.api.method_signature) = "parent"; + } + + // CreateUserAccessToken creates a new access token for a user. + rpc CreateUserAccessToken(CreateUserAccessTokenRequest) returns (UserAccessToken) { + option (google.api.http) = { + post: "/api/v1/{parent=users/*}/accessTokens" + body: "access_token" + }; + option (google.api.method_signature) = "parent,access_token"; + } + + // DeleteUserAccessToken deletes an access token. + rpc DeleteUserAccessToken(DeleteUserAccessTokenRequest) returns (google.protobuf.Empty) { + option (google.api.http) = {delete: "/api/v1/{name=users/*/accessTokens/*}"}; + option (google.api.method_signature) = "name"; + } + + // ListUserSessions returns a list of active sessions for a user. + rpc ListUserSessions(ListUserSessionsRequest) returns (ListUserSessionsResponse) { + option (google.api.http) = {get: "/api/v1/{parent=users/*}/sessions"}; + option (google.api.method_signature) = "parent"; + } + + // RevokeUserSession revokes a specific session for a user. + rpc RevokeUserSession(RevokeUserSessionRequest) returns (google.protobuf.Empty) { + option (google.api.http) = {delete: "/api/v1/{name=users/*/sessions/*}"}; + option (google.api.method_signature) = "name"; + } +} + +message User { + option (google.api.resource) = { + type: "memos.api.v1/User" + pattern: "users/{user}" + name_field: "name" + singular: "user" + plural: "users" + }; + + // The resource name of the user. + // Format: users/{user} + string name = 1 [(google.api.field_behavior) = IDENTIFIER]; + + // The role of the user. + Role role = 2 [(google.api.field_behavior) = REQUIRED]; + + // Required. The unique username for login. + string username = 3 [(google.api.field_behavior) = REQUIRED]; + + // Optional. The email address of the user. + string email = 4 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. The display name of the user. + string display_name = 5 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. The avatar URL of the user. + string avatar_url = 6 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. The description of the user. + string description = 7 [(google.api.field_behavior) = OPTIONAL]; + + // Input only. The password for the user. + string password = 8 [(google.api.field_behavior) = INPUT_ONLY]; + + // The state of the user. + State state = 9 [(google.api.field_behavior) = REQUIRED]; + + // Output only. The creation timestamp. + google.protobuf.Timestamp create_time = 10 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // Output only. The last update timestamp. + google.protobuf.Timestamp update_time = 11 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // User role enumeration. + enum Role { + // Unspecified role. + ROLE_UNSPECIFIED = 0; + // Host role with full system access. + HOST = 1; + // Admin role with administrative privileges. + ADMIN = 2; + // Regular user role. + USER = 3; + } +} + +message ListUsersRequest { + // Optional. The maximum number of users to return. + // The service may return fewer than this value. + // If unspecified, at most 50 users will be returned. + // The maximum value is 1000; values above 1000 will be coerced to 1000. + int32 page_size = 1 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. A page token, received from a previous `ListUsers` call. + // Provide this to retrieve the subsequent page. + string page_token = 2 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Filter to apply to the list results. + // Example: "state=ACTIVE" or "role=USER" or "email:@example.com" + // Supported operators: =, !=, <, <=, >, >=, : + // Supported fields: username, email, role, state, create_time, update_time + string filter = 3 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. The order to sort results by. + // Example: "create_time desc" or "username asc" + string order_by = 4 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. If true, show deleted users in the response. + bool show_deleted = 5 [(google.api.field_behavior) = OPTIONAL]; +} + +message ListUsersResponse { + // The list of users. + repeated User users = 1; + + // A token that can be sent as `page_token` to retrieve the next page. + // If this field is omitted, there are no subsequent pages. + string next_page_token = 2; + + // The total count of users (may be approximate). + int32 total_size = 3; +} + +message GetUserRequest { + // Required. The resource name of the user. + // Format: users/{user} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/User"} + ]; + + // Optional. The fields to return in the response. + // If not specified, all fields are returned. + google.protobuf.FieldMask read_mask = 2 [(google.api.field_behavior) = OPTIONAL]; +} + +message CreateUserRequest { + // Required. The user to create. + User user = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.field_behavior) = INPUT_ONLY + ]; + + // Optional. The user ID to use for this user. + // If empty, a unique ID will be generated. + // Must match the pattern [a-z0-9-]+ + string user_id = 2 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. If set, validate the request but don't actually create the user. + bool validate_only = 3 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. An idempotency token that can be used to ensure that multiple + // requests to create a user have the same result. + string request_id = 4 [(google.api.field_behavior) = OPTIONAL]; +} + +message UpdateUserRequest { + // Required. The user to update. + User user = 1 [(google.api.field_behavior) = REQUIRED]; + + // Required. The list of fields to update. + google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = REQUIRED]; + + // Optional. If set to true, allows updating sensitive fields. + bool allow_missing = 3 [(google.api.field_behavior) = OPTIONAL]; +} + +message DeleteUserRequest { + // Required. The resource name of the user to delete. + // Format: users/{user} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/User"} + ]; + + // Optional. If set to true, the user will be deleted even if they have associated data. + bool force = 2 [(google.api.field_behavior) = OPTIONAL]; +} + +message SearchUsersRequest { + // Required. The search query. + string query = 1 [(google.api.field_behavior) = REQUIRED]; + + // Optional. The maximum number of users to return. + int32 page_size = 2 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. A page token for pagination. + string page_token = 3 [(google.api.field_behavior) = OPTIONAL]; +} + +message SearchUsersResponse { + // The list of users matching the search query. + repeated User users = 1; + + // A token for the next page of results. + string next_page_token = 2; + + // The total count of matching users. + int32 total_size = 3; +} + +message GetUserAvatarRequest { + // Required. The resource name of the user. + // Format: users/{user} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/User"} + ]; +} + +// User statistics messages +message UserStats { + option (google.api.resource) = { + type: "memos.api.v1/UserStats" + pattern: "users/{user}" + singular: "userStats" + plural: "userStats" + }; + + // The resource name of the user whose stats these are. + // Format: users/{user} + string name = 1 [(google.api.field_behavior) = IDENTIFIER]; + + // The timestamps when the memos were displayed. + repeated google.protobuf.Timestamp memo_display_timestamps = 2; + + // The stats of memo types. + MemoTypeStats memo_type_stats = 3; + + // The count of tags. + map tag_count = 4; + + // The pinned memos of the user. + repeated string pinned_memos = 5; + + // Total memo count. + int32 total_memo_count = 6; + + // Memo type statistics. + message MemoTypeStats { + int32 link_count = 1; + int32 code_count = 2; + int32 todo_count = 3; + int32 undo_count = 4; + } +} + +message GetUserStatsRequest { + // Required. The resource name of the user. + // Format: users/{user} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/User"} + ]; +} + +// User settings message +message UserSetting { + option (google.api.resource) = { + type: "memos.api.v1/UserSetting" + pattern: "users/{user}" + singular: "userSetting" + plural: "userSettings" + }; + + // The resource name of the user whose setting this is. + // Format: users/{user} + string name = 1 [(google.api.field_behavior) = IDENTIFIER]; + + // The preferred locale of the user. + string locale = 2 [(google.api.field_behavior) = OPTIONAL]; + + // The preferred appearance of the user. + string appearance = 3 [(google.api.field_behavior) = OPTIONAL]; + + // The default visibility of the memo. + string memo_visibility = 4 [(google.api.field_behavior) = OPTIONAL]; + + // The preferred theme of the user. + // This references a CSS file in the web/public/themes/ directory. + // If not set, the default theme will be used. + string theme = 5 [(google.api.field_behavior) = OPTIONAL]; +} + +message GetUserSettingRequest { + // Required. The resource name of the user. + // Format: users/{user} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/User"} + ]; +} + +message UpdateUserSettingRequest { + // Required. The user setting to update. + UserSetting setting = 1 [(google.api.field_behavior) = REQUIRED]; + + // Required. The list of fields to update. + google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = REQUIRED]; +} + +// User access token message +message UserAccessToken { + option (google.api.resource) = { + type: "memos.api.v1/UserAccessToken" + pattern: "users/{user}/accessTokens/{access_token}" + singular: "userAccessToken" + plural: "userAccessTokens" + }; + + // The resource name of the access token. + // Format: users/{user}/accessTokens/{access_token} + string name = 1 [(google.api.field_behavior) = IDENTIFIER]; + + // Output only. The access token value. + string access_token = 2 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // The description of the access token. + string description = 3 [(google.api.field_behavior) = OPTIONAL]; + + // Output only. The issued timestamp. + google.protobuf.Timestamp issued_at = 4 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // Optional. The expiration timestamp. + google.protobuf.Timestamp expires_at = 5 [(google.api.field_behavior) = OPTIONAL]; +} + +message ListUserAccessTokensRequest { + // Required. The parent resource whose access tokens will be listed. + // Format: users/{user} + string parent = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/User"} + ]; + + // Optional. The maximum number of access tokens to return. + int32 page_size = 2 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. A page token for pagination. + string page_token = 3 [(google.api.field_behavior) = OPTIONAL]; +} + +message ListUserAccessTokensResponse { + // The list of access tokens. + repeated UserAccessToken access_tokens = 1; + + // A token for the next page of results. + string next_page_token = 2; + + // The total count of access tokens. + int32 total_size = 3; +} + +message CreateUserAccessTokenRequest { + // Required. The parent resource where this access token will be created. + // Format: users/{user} + string parent = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/User"} + ]; + + // Required. The access token to create. + UserAccessToken access_token = 2 [(google.api.field_behavior) = REQUIRED]; + + // Optional. The access token ID to use. + string access_token_id = 3 [(google.api.field_behavior) = OPTIONAL]; +} + +message DeleteUserAccessTokenRequest { + // Required. The resource name of the access token to delete. + // Format: users/{user}/accessTokens/{access_token} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/UserAccessToken"} + ]; +} + +message UserSession { + option (google.api.resource) = { + type: "memos.api.v1/UserSession" + pattern: "users/{user}/sessions/{session}" + name_field: "name" + }; + + // The resource name of the session. + // Format: users/{user}/sessions/{session} + string name = 1 [(google.api.field_behavior) = IDENTIFIER]; + + // The session ID. + string session_id = 2 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // The timestamp when the session was created. + google.protobuf.Timestamp create_time = 3 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // The timestamp when the session was last accessed. + // Used for sliding expiration calculation (last_accessed_time + 2 weeks). + google.protobuf.Timestamp last_accessed_time = 4 [(google.api.field_behavior) = OUTPUT_ONLY]; + + // Client information associated with this session. + ClientInfo client_info = 5 [(google.api.field_behavior) = OUTPUT_ONLY]; + + message ClientInfo { + // User agent string of the client. + string user_agent = 1; + + // IP address of the client. + string ip_address = 2; + + // Optional. Device type (e.g., "mobile", "desktop", "tablet"). + string device_type = 3 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Operating system (e.g., "iOS 17.0", "Windows 11"). + string os = 4 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Browser name and version (e.g., "Chrome 119.0"). + string browser = 5 [(google.api.field_behavior) = OPTIONAL]; + } +} + +message ListUserSessionsRequest { + // Required. The resource name of the parent. + // Format: users/{user} + string parent = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/User"} + ]; +} + +message ListUserSessionsResponse { + // The list of user sessions. + repeated UserSession sessions = 1; +} + +message RevokeUserSessionRequest { + // Required. The resource name of the session to revoke. + // Format: users/{user}/sessions/{session} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/UserSession"} + ]; +} + +message ListAllUserStatsRequest { + // Optional. The maximum number of user stats to return. + int32 page_size = 1 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. A page token for pagination. + string page_token = 2 [(google.api.field_behavior) = OPTIONAL]; +} + +message ListAllUserStatsResponse { + // The list of user statistics. + repeated UserStats user_stats = 1; + + // A token for the next page of results. + string next_page_token = 2; + + // The total count of user statistics. + int32 total_size = 3; +} diff --git a/proto/api/v1/webhook_service.proto b/proto/api/v1/webhook_service.proto new file mode 100644 index 0000000..acbee62 --- /dev/null +++ b/proto/api/v1/webhook_service.proto @@ -0,0 +1,124 @@ +syntax = "proto3"; + +package memos.api.v1; + +import "google/api/annotations.proto"; +import "google/api/client.proto"; +import "google/api/field_behavior.proto"; +import "google/api/resource.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/field_mask.proto"; + +option go_package = "gen/api/v1"; + +service WebhookService { + // ListWebhooks returns a list of webhooks for a user. + rpc ListWebhooks(ListWebhooksRequest) returns (ListWebhooksResponse) { + option (google.api.http) = {get: "/api/v1/{parent=users/*}/webhooks"}; + option (google.api.method_signature) = "parent"; + } + + // GetWebhook gets a webhook by name. + rpc GetWebhook(GetWebhookRequest) returns (Webhook) { + option (google.api.http) = {get: "/api/v1/{name=users/*/webhooks/*}"}; + option (google.api.method_signature) = "name"; + } + + // CreateWebhook creates a new webhook for a user. + rpc CreateWebhook(CreateWebhookRequest) returns (Webhook) { + option (google.api.http) = { + post: "/api/v1/{parent=users/*}/webhooks" + body: "webhook" + }; + option (google.api.method_signature) = "parent,webhook"; + } + + // UpdateWebhook updates a webhook for a user. + rpc UpdateWebhook(UpdateWebhookRequest) returns (Webhook) { + option (google.api.http) = { + patch: "/api/v1/{webhook.name=users/*/webhooks/*}" + body: "webhook" + }; + option (google.api.method_signature) = "webhook,update_mask"; + } + + // DeleteWebhook deletes a webhook for a user. + rpc DeleteWebhook(DeleteWebhookRequest) returns (google.protobuf.Empty) { + option (google.api.http) = {delete: "/api/v1/{name=users/*/webhooks/*}"}; + option (google.api.method_signature) = "name"; + } +} + +message Webhook { + option (google.api.resource) = { + type: "memos.api.v1/Webhook" + pattern: "users/{user}/webhooks/{webhook}" + singular: "webhook" + plural: "webhooks" + }; + + // The resource name of the webhook. + // Format: users/{user}/webhooks/{webhook} + string name = 1 [(google.api.field_behavior) = IDENTIFIER]; + + // The display name of the webhook. + string display_name = 2 [(google.api.field_behavior) = REQUIRED]; + + // The target URL for the webhook. + string url = 3 [(google.api.field_behavior) = REQUIRED]; +} + +message ListWebhooksRequest { + // Required. The parent resource where webhooks are listed. + // Format: users/{user} + string parent = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {child_type: "memos.api.v1/Webhook"} + ]; +} + +message ListWebhooksResponse { + // The list of webhooks. + repeated Webhook webhooks = 1; +} + +message GetWebhookRequest { + // Required. The resource name of the webhook to retrieve. + // Format: users/{user}/webhooks/{webhook} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/Webhook"} + ]; +} + +message CreateWebhookRequest { + // Required. The parent resource where this webhook will be created. + // Format: users/{user} + string parent = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {child_type: "memos.api.v1/Webhook"} + ]; + + // Required. The webhook to create. + Webhook webhook = 2 [(google.api.field_behavior) = REQUIRED]; + + // Optional. If set, validate the request, but do not actually create the webhook. + bool validate_only = 3 [(google.api.field_behavior) = OPTIONAL]; +} + +message UpdateWebhookRequest { + // Required. The webhook resource which replaces the resource on the server. + Webhook webhook = 1 [(google.api.field_behavior) = REQUIRED]; + + // Optional. The list of fields to update. + google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = OPTIONAL]; +} + +message DeleteWebhookRequest { + // Required. The resource name of the webhook to delete. + // Format: users/{user}/webhooks/{webhook} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "memos.api.v1/Webhook"} + ]; +} diff --git a/proto/api/v1/workspace_service.proto b/proto/api/v1/workspace_service.proto new file mode 100644 index 0000000..012bc50 --- /dev/null +++ b/proto/api/v1/workspace_service.proto @@ -0,0 +1,177 @@ +syntax = "proto3"; + +package memos.api.v1; + +import "google/api/annotations.proto"; +import "google/api/client.proto"; +import "google/api/field_behavior.proto"; +import "google/api/resource.proto"; +import "google/protobuf/field_mask.proto"; + +option go_package = "gen/api/v1"; + +service WorkspaceService { + // Gets the workspace profile. + rpc GetWorkspaceProfile(GetWorkspaceProfileRequest) returns (WorkspaceProfile) { + option (google.api.http) = {get: "/api/v1/workspace/profile"}; + } + + // Gets a workspace setting. + rpc GetWorkspaceSetting(GetWorkspaceSettingRequest) returns (WorkspaceSetting) { + option (google.api.http) = {get: "/api/v1/{name=workspace/settings/*}"}; + option (google.api.method_signature) = "name"; + } + + // Updates a workspace setting. + rpc UpdateWorkspaceSetting(UpdateWorkspaceSettingRequest) returns (WorkspaceSetting) { + option (google.api.http) = { + patch: "/api/v1/{setting.name=workspace/settings/*}" + body: "setting" + }; + option (google.api.method_signature) = "setting,update_mask"; + } +} + +// Workspace profile message containing basic workspace information. +message WorkspaceProfile { + // The name of instance owner. + // Format: users/{user} + string owner = 1; + + // Version is the current version of instance. + string version = 2; + + // Mode is the instance mode (e.g. "prod", "dev" or "demo"). + string mode = 3; + + // Instance URL is the URL of the instance. + string instance_url = 6; +} + +// Request for workspace profile. +message GetWorkspaceProfileRequest {} + +// A workspace setting resource. +message WorkspaceSetting { + option (google.api.resource) = { + type: "api.memos.dev/WorkspaceSetting" + pattern: "workspace/settings/{setting}" + singular: "workspaceSetting" + plural: "workspaceSettings" + }; + + // The name of the workspace setting. + // Format: workspace/settings/{setting} + string name = 1 [(google.api.field_behavior) = IDENTIFIER]; + + oneof value { + WorkspaceGeneralSetting general_setting = 2; + WorkspaceStorageSetting storage_setting = 3; + WorkspaceMemoRelatedSetting memo_related_setting = 4; + } +} + +message WorkspaceGeneralSetting { + // theme is the name of the selected theme. + // This references a CSS file in the web/public/themes/ directory. + string theme = 1; + // disallow_user_registration disallows user registration. + bool disallow_user_registration = 2; + // disallow_password_auth disallows password authentication. + bool disallow_password_auth = 3; + // additional_script is the additional script. + string additional_script = 4; + // additional_style is the additional style. + string additional_style = 5; + // custom_profile is the custom profile. + WorkspaceCustomProfile custom_profile = 6; + // week_start_day_offset is the week start day offset from Sunday. + // 0: Sunday, 1: Monday, 2: Tuesday, 3: Wednesday, 4: Thursday, 5: Friday, 6: Saturday + // Default is Sunday. + int32 week_start_day_offset = 7; + + // disallow_change_username disallows changing username. + bool disallow_change_username = 8; + // disallow_change_nickname disallows changing nickname. + bool disallow_change_nickname = 9; +} + +message WorkspaceCustomProfile { + string title = 1; + string description = 2; + string logo_url = 3; + string locale = 4; + string appearance = 5; +} + +message WorkspaceStorageSetting { + enum StorageType { + STORAGE_TYPE_UNSPECIFIED = 0; + // DATABASE is the database storage type. + DATABASE = 1; + // LOCAL is the local storage type. + LOCAL = 2; + // S3 is the S3 storage type. + S3 = 3; + } + // storage_type is the storage type. + StorageType storage_type = 1; + // The template of file path. + // e.g. assets/{timestamp}_{filename} + string filepath_template = 2; + // The max upload size in megabytes. + int64 upload_size_limit_mb = 3; + // Reference: https://developers.cloudflare.com/r2/examples/aws/aws-sdk-go/ + message S3Config { + string access_key_id = 1; + string access_key_secret = 2; + string endpoint = 3; + string region = 4; + string bucket = 5; + bool use_path_style = 6; + } + // The S3 config. + S3Config s3_config = 4; +} + +message WorkspaceMemoRelatedSetting { + // disallow_public_visibility disallows set memo as public visibility. + bool disallow_public_visibility = 1; + // display_with_update_time orders and displays memo with update time. + bool display_with_update_time = 2; + // content_length_limit is the limit of content length. Unit is byte. + int32 content_length_limit = 3; + // enable_double_click_edit enables editing on double click. + bool enable_double_click_edit = 4; + // enable_link_preview enables links preview. + bool enable_link_preview = 5; + // enable_comment enables comment. + bool enable_comment = 6; + // reactions is the list of reactions. + repeated string reactions = 7; + // disable_markdown_shortcuts disallow the registration of markdown shortcuts. + bool disable_markdown_shortcuts = 8; + // enable_blur_nsfw_content enables blurring of content marked as not safe for work (NSFW). + bool enable_blur_nsfw_content = 9; + // nsfw_tags is the list of tags that mark content as NSFW for blurring. + repeated string nsfw_tags = 10; +} + +// Request message for GetWorkspaceSetting method. +message GetWorkspaceSettingRequest { + // The resource name of the workspace setting. + // Format: workspace/settings/{setting} + string name = 1 [ + (google.api.field_behavior) = REQUIRED, + (google.api.resource_reference) = {type: "api.memos.dev/WorkspaceSetting"} + ]; +} + +// Request message for UpdateWorkspaceSetting method. +message UpdateWorkspaceSettingRequest { + // The workspace setting resource which replaces the resource on the server. + WorkspaceSetting setting = 1 [(google.api.field_behavior) = REQUIRED]; + + // The list of fields to update. + google.protobuf.FieldMask update_mask = 2 [(google.api.field_behavior) = OPTIONAL]; +} diff --git a/proto/buf.gen.yaml b/proto/buf.gen.yaml new file mode 100644 index 0000000..df60636 --- /dev/null +++ b/proto/buf.gen.yaml @@ -0,0 +1,32 @@ +version: v2 +managed: + enabled: true + disable: + - file_option: go_package + module: buf.build/googleapis/googleapis + override: + - file_option: go_package_prefix + value: github.com/usememos/memos/proto/gen +plugins: + - remote: buf.build/protocolbuffers/go + out: gen + opt: paths=source_relative + - remote: buf.build/grpc/go + out: gen + opt: paths=source_relative + - remote: buf.build/grpc-ecosystem/gateway + out: gen + opt: paths=source_relative + - remote: buf.build/grpc-ecosystem/openapiv2 + out: gen + opt: output_format=yaml,allow_merge=true + - remote: buf.build/community/stephenh-ts-proto + out: ../web/src/types/proto + opt: + - env=browser + - useOptionals=messages + - outputServices=generic-definitions + - outputJsonMethods=false + - useExactTypes=false + - esModuleInterop=true + - stringEnums=true diff --git a/proto/buf.lock b/proto/buf.lock new file mode 100644 index 0000000..9a4fa6c --- /dev/null +++ b/proto/buf.lock @@ -0,0 +1,6 @@ +# Generated by buf. DO NOT EDIT. +version: v2 +deps: + - name: buf.build/googleapis/googleapis + commit: 61b203b9a9164be9a834f58c37be6f62 + digest: b5:7811a98b35bd2e4ae5c3ac73c8b3d9ae429f3a790da15de188dc98fc2b77d6bb10e45711f14903af9553fa9821dff256054f2e4b7795789265bc476bec2f088c diff --git a/proto/buf.yaml b/proto/buf.yaml new file mode 100644 index 0000000..563f5f5 --- /dev/null +++ b/proto/buf.yaml @@ -0,0 +1,19 @@ +version: v2 +deps: + - buf.build/googleapis/googleapis +lint: + use: + - BASIC + except: + - ENUM_VALUE_PREFIX + - FIELD_NOT_REQUIRED + - PACKAGE_DIRECTORY_MATCH + - PACKAGE_NO_IMPORT_CYCLE + - PACKAGE_VERSION_SUFFIX + disallow_comment_ignores: true +breaking: + use: + - FILE + except: + - EXTENSION_NO_DELETE + - FIELD_SAME_DEFAULT diff --git a/proto/gen/api/v1/activity_service.pb.go b/proto/gen/api/v1/activity_service.pb.go new file mode 100644 index 0000000..5634f82 --- /dev/null +++ b/proto/gen/api/v1/activity_service.pb.go @@ -0,0 +1,628 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: api/v1/activity_service.proto + +package apiv1 + +import ( + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Activity types. +type Activity_Type int32 + +const ( + // Unspecified type. + Activity_TYPE_UNSPECIFIED Activity_Type = 0 + // Memo comment activity. + Activity_MEMO_COMMENT Activity_Type = 1 + // Version update activity. + Activity_VERSION_UPDATE Activity_Type = 2 +) + +// Enum value maps for Activity_Type. +var ( + Activity_Type_name = map[int32]string{ + 0: "TYPE_UNSPECIFIED", + 1: "MEMO_COMMENT", + 2: "VERSION_UPDATE", + } + Activity_Type_value = map[string]int32{ + "TYPE_UNSPECIFIED": 0, + "MEMO_COMMENT": 1, + "VERSION_UPDATE": 2, + } +) + +func (x Activity_Type) Enum() *Activity_Type { + p := new(Activity_Type) + *p = x + return p +} + +func (x Activity_Type) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Activity_Type) Descriptor() protoreflect.EnumDescriptor { + return file_api_v1_activity_service_proto_enumTypes[0].Descriptor() +} + +func (Activity_Type) Type() protoreflect.EnumType { + return &file_api_v1_activity_service_proto_enumTypes[0] +} + +func (x Activity_Type) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Activity_Type.Descriptor instead. +func (Activity_Type) EnumDescriptor() ([]byte, []int) { + return file_api_v1_activity_service_proto_rawDescGZIP(), []int{0, 0} +} + +// Activity levels. +type Activity_Level int32 + +const ( + // Unspecified level. + Activity_LEVEL_UNSPECIFIED Activity_Level = 0 + // Info level. + Activity_INFO Activity_Level = 1 + // Warn level. + Activity_WARN Activity_Level = 2 + // Error level. + Activity_ERROR Activity_Level = 3 +) + +// Enum value maps for Activity_Level. +var ( + Activity_Level_name = map[int32]string{ + 0: "LEVEL_UNSPECIFIED", + 1: "INFO", + 2: "WARN", + 3: "ERROR", + } + Activity_Level_value = map[string]int32{ + "LEVEL_UNSPECIFIED": 0, + "INFO": 1, + "WARN": 2, + "ERROR": 3, + } +) + +func (x Activity_Level) Enum() *Activity_Level { + p := new(Activity_Level) + *p = x + return p +} + +func (x Activity_Level) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Activity_Level) Descriptor() protoreflect.EnumDescriptor { + return file_api_v1_activity_service_proto_enumTypes[1].Descriptor() +} + +func (Activity_Level) Type() protoreflect.EnumType { + return &file_api_v1_activity_service_proto_enumTypes[1] +} + +func (x Activity_Level) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Activity_Level.Descriptor instead. +func (Activity_Level) EnumDescriptor() ([]byte, []int) { + return file_api_v1_activity_service_proto_rawDescGZIP(), []int{0, 1} +} + +type Activity struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The name of the activity. + // Format: activities/{id} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // The name of the creator. + // Format: users/{user} + Creator string `protobuf:"bytes,2,opt,name=creator,proto3" json:"creator,omitempty"` + // The type of the activity. + Type Activity_Type `protobuf:"varint,3,opt,name=type,proto3,enum=memos.api.v1.Activity_Type" json:"type,omitempty"` + // The level of the activity. + Level Activity_Level `protobuf:"varint,4,opt,name=level,proto3,enum=memos.api.v1.Activity_Level" json:"level,omitempty"` + // The create time of the activity. + CreateTime *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` + // The payload of the activity. + Payload *ActivityPayload `protobuf:"bytes,6,opt,name=payload,proto3" json:"payload,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Activity) Reset() { + *x = Activity{} + mi := &file_api_v1_activity_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Activity) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Activity) ProtoMessage() {} + +func (x *Activity) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_activity_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Activity.ProtoReflect.Descriptor instead. +func (*Activity) Descriptor() ([]byte, []int) { + return file_api_v1_activity_service_proto_rawDescGZIP(), []int{0} +} + +func (x *Activity) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Activity) GetCreator() string { + if x != nil { + return x.Creator + } + return "" +} + +func (x *Activity) GetType() Activity_Type { + if x != nil { + return x.Type + } + return Activity_TYPE_UNSPECIFIED +} + +func (x *Activity) GetLevel() Activity_Level { + if x != nil { + return x.Level + } + return Activity_LEVEL_UNSPECIFIED +} + +func (x *Activity) GetCreateTime() *timestamppb.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *Activity) GetPayload() *ActivityPayload { + if x != nil { + return x.Payload + } + return nil +} + +type ActivityPayload struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Payload: + // + // *ActivityPayload_MemoComment + Payload isActivityPayload_Payload `protobuf_oneof:"payload"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ActivityPayload) Reset() { + *x = ActivityPayload{} + mi := &file_api_v1_activity_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ActivityPayload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ActivityPayload) ProtoMessage() {} + +func (x *ActivityPayload) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_activity_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ActivityPayload.ProtoReflect.Descriptor instead. +func (*ActivityPayload) Descriptor() ([]byte, []int) { + return file_api_v1_activity_service_proto_rawDescGZIP(), []int{1} +} + +func (x *ActivityPayload) GetPayload() isActivityPayload_Payload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *ActivityPayload) GetMemoComment() *ActivityMemoCommentPayload { + if x != nil { + if x, ok := x.Payload.(*ActivityPayload_MemoComment); ok { + return x.MemoComment + } + } + return nil +} + +type isActivityPayload_Payload interface { + isActivityPayload_Payload() +} + +type ActivityPayload_MemoComment struct { + // Memo comment activity payload. + MemoComment *ActivityMemoCommentPayload `protobuf:"bytes,1,opt,name=memo_comment,json=memoComment,proto3,oneof"` +} + +func (*ActivityPayload_MemoComment) isActivityPayload_Payload() {} + +// ActivityMemoCommentPayload represents the payload of a memo comment activity. +type ActivityMemoCommentPayload struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The memo name of comment. + // Format: memos/{memo} + Memo string `protobuf:"bytes,1,opt,name=memo,proto3" json:"memo,omitempty"` + // The name of related memo. + // Format: memos/{memo} + RelatedMemo string `protobuf:"bytes,2,opt,name=related_memo,json=relatedMemo,proto3" json:"related_memo,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ActivityMemoCommentPayload) Reset() { + *x = ActivityMemoCommentPayload{} + mi := &file_api_v1_activity_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ActivityMemoCommentPayload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ActivityMemoCommentPayload) ProtoMessage() {} + +func (x *ActivityMemoCommentPayload) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_activity_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ActivityMemoCommentPayload.ProtoReflect.Descriptor instead. +func (*ActivityMemoCommentPayload) Descriptor() ([]byte, []int) { + return file_api_v1_activity_service_proto_rawDescGZIP(), []int{2} +} + +func (x *ActivityMemoCommentPayload) GetMemo() string { + if x != nil { + return x.Memo + } + return "" +} + +func (x *ActivityMemoCommentPayload) GetRelatedMemo() string { + if x != nil { + return x.RelatedMemo + } + return "" +} + +type ListActivitiesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The maximum number of activities to return. + // The service may return fewer than this value. + // If unspecified, at most 100 activities will be returned. + // The maximum value is 1000; values above 1000 will be coerced to 1000. + PageSize int32 `protobuf:"varint,1,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // A page token, received from a previous `ListActivities` call. + // Provide this to retrieve the subsequent page. + PageToken string `protobuf:"bytes,2,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListActivitiesRequest) Reset() { + *x = ListActivitiesRequest{} + mi := &file_api_v1_activity_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListActivitiesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListActivitiesRequest) ProtoMessage() {} + +func (x *ListActivitiesRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_activity_service_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListActivitiesRequest.ProtoReflect.Descriptor instead. +func (*ListActivitiesRequest) Descriptor() ([]byte, []int) { + return file_api_v1_activity_service_proto_rawDescGZIP(), []int{3} +} + +func (x *ListActivitiesRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListActivitiesRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +type ListActivitiesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The activities. + Activities []*Activity `protobuf:"bytes,1,rep,name=activities,proto3" json:"activities,omitempty"` + // A token to retrieve the next page of results. + // Pass this value in the page_token field in the subsequent call to `ListActivities` + // method to retrieve the next page of results. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListActivitiesResponse) Reset() { + *x = ListActivitiesResponse{} + mi := &file_api_v1_activity_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListActivitiesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListActivitiesResponse) ProtoMessage() {} + +func (x *ListActivitiesResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_activity_service_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListActivitiesResponse.ProtoReflect.Descriptor instead. +func (*ListActivitiesResponse) Descriptor() ([]byte, []int) { + return file_api_v1_activity_service_proto_rawDescGZIP(), []int{4} +} + +func (x *ListActivitiesResponse) GetActivities() []*Activity { + if x != nil { + return x.Activities + } + return nil +} + +func (x *ListActivitiesResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +type GetActivityRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The name of the activity. + // Format: activities/{id}, id is the system generated auto-incremented id. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetActivityRequest) Reset() { + *x = GetActivityRequest{} + mi := &file_api_v1_activity_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetActivityRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetActivityRequest) ProtoMessage() {} + +func (x *GetActivityRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_activity_service_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetActivityRequest.ProtoReflect.Descriptor instead. +func (*GetActivityRequest) Descriptor() ([]byte, []int) { + return file_api_v1_activity_service_proto_rawDescGZIP(), []int{5} +} + +func (x *GetActivityRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +var File_api_v1_activity_service_proto protoreflect.FileDescriptor + +const file_api_v1_activity_service_proto_rawDesc = "" + + "\n" + + "\x1dapi/v1/activity_service.proto\x12\fmemos.api.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x86\x04\n" + + "\bActivity\x12\x1a\n" + + "\x04name\x18\x01 \x01(\tB\x06\xe0A\x03\xe0A\bR\x04name\x12\x1d\n" + + "\acreator\x18\x02 \x01(\tB\x03\xe0A\x03R\acreator\x124\n" + + "\x04type\x18\x03 \x01(\x0e2\x1b.memos.api.v1.Activity.TypeB\x03\xe0A\x03R\x04type\x127\n" + + "\x05level\x18\x04 \x01(\x0e2\x1c.memos.api.v1.Activity.LevelB\x03\xe0A\x03R\x05level\x12@\n" + + "\vcreate_time\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\n" + + "createTime\x12<\n" + + "\apayload\x18\x06 \x01(\v2\x1d.memos.api.v1.ActivityPayloadB\x03\xe0A\x03R\apayload\"B\n" + + "\x04Type\x12\x14\n" + + "\x10TYPE_UNSPECIFIED\x10\x00\x12\x10\n" + + "\fMEMO_COMMENT\x10\x01\x12\x12\n" + + "\x0eVERSION_UPDATE\x10\x02\"=\n" + + "\x05Level\x12\x15\n" + + "\x11LEVEL_UNSPECIFIED\x10\x00\x12\b\n" + + "\x04INFO\x10\x01\x12\b\n" + + "\x04WARN\x10\x02\x12\t\n" + + "\x05ERROR\x10\x03:M\xeaAJ\n" + + "\x15memos.api.v1/Activity\x12\x15activities/{activity}\x1a\x04name*\n" + + "activities2\bactivity\"k\n" + + "\x0fActivityPayload\x12M\n" + + "\fmemo_comment\x18\x01 \x01(\v2(.memos.api.v1.ActivityMemoCommentPayloadH\x00R\vmemoCommentB\t\n" + + "\apayload\"S\n" + + "\x1aActivityMemoCommentPayload\x12\x12\n" + + "\x04memo\x18\x01 \x01(\tR\x04memo\x12!\n" + + "\frelated_memo\x18\x02 \x01(\tR\vrelatedMemo\"S\n" + + "\x15ListActivitiesRequest\x12\x1b\n" + + "\tpage_size\x18\x01 \x01(\x05R\bpageSize\x12\x1d\n" + + "\n" + + "page_token\x18\x02 \x01(\tR\tpageToken\"x\n" + + "\x16ListActivitiesResponse\x126\n" + + "\n" + + "activities\x18\x01 \x03(\v2\x16.memos.api.v1.ActivityR\n" + + "activities\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\"G\n" + + "\x12GetActivityRequest\x121\n" + + "\x04name\x18\x01 \x01(\tB\x1d\xe0A\x02\xfaA\x17\n" + + "\x15memos.api.v1/ActivityR\x04name2\xff\x01\n" + + "\x0fActivityService\x12w\n" + + "\x0eListActivities\x12#.memos.api.v1.ListActivitiesRequest\x1a$.memos.api.v1.ListActivitiesResponse\"\x1a\x82\xd3\xe4\x93\x02\x14\x12\x12/api/v1/activities\x12s\n" + + "\vGetActivity\x12 .memos.api.v1.GetActivityRequest\x1a\x16.memos.api.v1.Activity\"*\xdaA\x04name\x82\xd3\xe4\x93\x02\x1d\x12\x1b/api/v1/{name=activities/*}B\xac\x01\n" + + "\x10com.memos.api.v1B\x14ActivityServiceProtoP\x01Z0github.com/usememos/memos/proto/gen/api/v1;apiv1\xa2\x02\x03MAX\xaa\x02\fMemos.Api.V1\xca\x02\fMemos\\Api\\V1\xe2\x02\x18Memos\\Api\\V1\\GPBMetadata\xea\x02\x0eMemos::Api::V1b\x06proto3" + +var ( + file_api_v1_activity_service_proto_rawDescOnce sync.Once + file_api_v1_activity_service_proto_rawDescData []byte +) + +func file_api_v1_activity_service_proto_rawDescGZIP() []byte { + file_api_v1_activity_service_proto_rawDescOnce.Do(func() { + file_api_v1_activity_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_v1_activity_service_proto_rawDesc), len(file_api_v1_activity_service_proto_rawDesc))) + }) + return file_api_v1_activity_service_proto_rawDescData +} + +var file_api_v1_activity_service_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_api_v1_activity_service_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_api_v1_activity_service_proto_goTypes = []any{ + (Activity_Type)(0), // 0: memos.api.v1.Activity.Type + (Activity_Level)(0), // 1: memos.api.v1.Activity.Level + (*Activity)(nil), // 2: memos.api.v1.Activity + (*ActivityPayload)(nil), // 3: memos.api.v1.ActivityPayload + (*ActivityMemoCommentPayload)(nil), // 4: memos.api.v1.ActivityMemoCommentPayload + (*ListActivitiesRequest)(nil), // 5: memos.api.v1.ListActivitiesRequest + (*ListActivitiesResponse)(nil), // 6: memos.api.v1.ListActivitiesResponse + (*GetActivityRequest)(nil), // 7: memos.api.v1.GetActivityRequest + (*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp +} +var file_api_v1_activity_service_proto_depIdxs = []int32{ + 0, // 0: memos.api.v1.Activity.type:type_name -> memos.api.v1.Activity.Type + 1, // 1: memos.api.v1.Activity.level:type_name -> memos.api.v1.Activity.Level + 8, // 2: memos.api.v1.Activity.create_time:type_name -> google.protobuf.Timestamp + 3, // 3: memos.api.v1.Activity.payload:type_name -> memos.api.v1.ActivityPayload + 4, // 4: memos.api.v1.ActivityPayload.memo_comment:type_name -> memos.api.v1.ActivityMemoCommentPayload + 2, // 5: memos.api.v1.ListActivitiesResponse.activities:type_name -> memos.api.v1.Activity + 5, // 6: memos.api.v1.ActivityService.ListActivities:input_type -> memos.api.v1.ListActivitiesRequest + 7, // 7: memos.api.v1.ActivityService.GetActivity:input_type -> memos.api.v1.GetActivityRequest + 6, // 8: memos.api.v1.ActivityService.ListActivities:output_type -> memos.api.v1.ListActivitiesResponse + 2, // 9: memos.api.v1.ActivityService.GetActivity:output_type -> memos.api.v1.Activity + 8, // [8:10] is the sub-list for method output_type + 6, // [6:8] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_api_v1_activity_service_proto_init() } +func file_api_v1_activity_service_proto_init() { + if File_api_v1_activity_service_proto != nil { + return + } + file_api_v1_activity_service_proto_msgTypes[1].OneofWrappers = []any{ + (*ActivityPayload_MemoComment)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_activity_service_proto_rawDesc), len(file_api_v1_activity_service_proto_rawDesc)), + NumEnums: 2, + NumMessages: 6, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_api_v1_activity_service_proto_goTypes, + DependencyIndexes: file_api_v1_activity_service_proto_depIdxs, + EnumInfos: file_api_v1_activity_service_proto_enumTypes, + MessageInfos: file_api_v1_activity_service_proto_msgTypes, + }.Build() + File_api_v1_activity_service_proto = out.File + file_api_v1_activity_service_proto_goTypes = nil + file_api_v1_activity_service_proto_depIdxs = nil +} diff --git a/proto/gen/api/v1/activity_service.pb.gw.go b/proto/gen/api/v1/activity_service.pb.gw.go new file mode 100644 index 0000000..2aba32b --- /dev/null +++ b/proto/gen/api/v1/activity_service.pb.gw.go @@ -0,0 +1,243 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: api/v1/activity_service.proto + +/* +Package apiv1 is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package apiv1 + +import ( + "context" + "errors" + "io" + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// Suppress "imported and not used" errors +var ( + _ codes.Code + _ io.Reader + _ status.Status + _ = errors.New + _ = runtime.String + _ = utilities.NewDoubleArray + _ = metadata.Join +) + +var filter_ActivityService_ListActivities_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + +func request_ActivityService_ListActivities_0(ctx context.Context, marshaler runtime.Marshaler, client ActivityServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListActivitiesRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ActivityService_ListActivities_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.ListActivities(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_ActivityService_ListActivities_0(ctx context.Context, marshaler runtime.Marshaler, server ActivityServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListActivitiesRequest + metadata runtime.ServerMetadata + ) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ActivityService_ListActivities_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ListActivities(ctx, &protoReq) + return msg, metadata, err +} + +func request_ActivityService_GetActivity_0(ctx context.Context, marshaler runtime.Marshaler, client ActivityServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetActivityRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := client.GetActivity(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_ActivityService_GetActivity_0(ctx context.Context, marshaler runtime.Marshaler, server ActivityServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetActivityRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := server.GetActivity(ctx, &protoReq) + return msg, metadata, err +} + +// RegisterActivityServiceHandlerServer registers the http handlers for service ActivityService to "mux". +// UnaryRPC :call ActivityServiceServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterActivityServiceHandlerFromEndpoint instead. +// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call. +func RegisterActivityServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server ActivityServiceServer) error { + mux.Handle(http.MethodGet, pattern_ActivityService_ListActivities_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.ActivityService/ListActivities", runtime.WithHTTPPathPattern("/api/v1/activities")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ActivityService_ListActivities_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ActivityService_ListActivities_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_ActivityService_GetActivity_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.ActivityService/GetActivity", runtime.WithHTTPPathPattern("/api/v1/{name=activities/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ActivityService_GetActivity_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ActivityService_GetActivity_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + + return nil +} + +// RegisterActivityServiceHandlerFromEndpoint is same as RegisterActivityServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterActivityServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.NewClient(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + return RegisterActivityServiceHandler(ctx, mux, conn) +} + +// RegisterActivityServiceHandler registers the http handlers for service ActivityService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterActivityServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterActivityServiceHandlerClient(ctx, mux, NewActivityServiceClient(conn)) +} + +// RegisterActivityServiceHandlerClient registers the http handlers for service ActivityService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "ActivityServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "ActivityServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "ActivityServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares. +func RegisterActivityServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client ActivityServiceClient) error { + mux.Handle(http.MethodGet, pattern_ActivityService_ListActivities_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.ActivityService/ListActivities", runtime.WithHTTPPathPattern("/api/v1/activities")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ActivityService_ListActivities_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ActivityService_ListActivities_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_ActivityService_GetActivity_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.ActivityService/GetActivity", runtime.WithHTTPPathPattern("/api/v1/{name=activities/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ActivityService_GetActivity_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ActivityService_GetActivity_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + return nil +} + +var ( + pattern_ActivityService_ListActivities_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "activities"}, "")) + pattern_ActivityService_GetActivity_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "activities", "name"}, "")) +) + +var ( + forward_ActivityService_ListActivities_0 = runtime.ForwardResponseMessage + forward_ActivityService_GetActivity_0 = runtime.ForwardResponseMessage +) diff --git a/proto/gen/api/v1/activity_service_grpc.pb.go b/proto/gen/api/v1/activity_service_grpc.pb.go new file mode 100644 index 0000000..d9d0594 --- /dev/null +++ b/proto/gen/api/v1/activity_service_grpc.pb.go @@ -0,0 +1,163 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc (unknown) +// source: api/v1/activity_service.proto + +package apiv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + ActivityService_ListActivities_FullMethodName = "/memos.api.v1.ActivityService/ListActivities" + ActivityService_GetActivity_FullMethodName = "/memos.api.v1.ActivityService/GetActivity" +) + +// ActivityServiceClient is the client API for ActivityService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type ActivityServiceClient interface { + // ListActivities returns a list of activities. + ListActivities(ctx context.Context, in *ListActivitiesRequest, opts ...grpc.CallOption) (*ListActivitiesResponse, error) + // GetActivity returns the activity with the given id. + GetActivity(ctx context.Context, in *GetActivityRequest, opts ...grpc.CallOption) (*Activity, error) +} + +type activityServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewActivityServiceClient(cc grpc.ClientConnInterface) ActivityServiceClient { + return &activityServiceClient{cc} +} + +func (c *activityServiceClient) ListActivities(ctx context.Context, in *ListActivitiesRequest, opts ...grpc.CallOption) (*ListActivitiesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListActivitiesResponse) + err := c.cc.Invoke(ctx, ActivityService_ListActivities_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *activityServiceClient) GetActivity(ctx context.Context, in *GetActivityRequest, opts ...grpc.CallOption) (*Activity, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Activity) + err := c.cc.Invoke(ctx, ActivityService_GetActivity_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ActivityServiceServer is the server API for ActivityService service. +// All implementations must embed UnimplementedActivityServiceServer +// for forward compatibility. +type ActivityServiceServer interface { + // ListActivities returns a list of activities. + ListActivities(context.Context, *ListActivitiesRequest) (*ListActivitiesResponse, error) + // GetActivity returns the activity with the given id. + GetActivity(context.Context, *GetActivityRequest) (*Activity, error) + mustEmbedUnimplementedActivityServiceServer() +} + +// UnimplementedActivityServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedActivityServiceServer struct{} + +func (UnimplementedActivityServiceServer) ListActivities(context.Context, *ListActivitiesRequest) (*ListActivitiesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListActivities not implemented") +} +func (UnimplementedActivityServiceServer) GetActivity(context.Context, *GetActivityRequest) (*Activity, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetActivity not implemented") +} +func (UnimplementedActivityServiceServer) mustEmbedUnimplementedActivityServiceServer() {} +func (UnimplementedActivityServiceServer) testEmbeddedByValue() {} + +// UnsafeActivityServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ActivityServiceServer will +// result in compilation errors. +type UnsafeActivityServiceServer interface { + mustEmbedUnimplementedActivityServiceServer() +} + +func RegisterActivityServiceServer(s grpc.ServiceRegistrar, srv ActivityServiceServer) { + // If the following call pancis, it indicates UnimplementedActivityServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&ActivityService_ServiceDesc, srv) +} + +func _ActivityService_ListActivities_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListActivitiesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ActivityServiceServer).ListActivities(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ActivityService_ListActivities_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ActivityServiceServer).ListActivities(ctx, req.(*ListActivitiesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ActivityService_GetActivity_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetActivityRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ActivityServiceServer).GetActivity(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ActivityService_GetActivity_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ActivityServiceServer).GetActivity(ctx, req.(*GetActivityRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// ActivityService_ServiceDesc is the grpc.ServiceDesc for ActivityService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ActivityService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "memos.api.v1.ActivityService", + HandlerType: (*ActivityServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ListActivities", + Handler: _ActivityService_ListActivities_Handler, + }, + { + MethodName: "GetActivity", + Handler: _ActivityService_GetActivity_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "api/v1/activity_service.proto", +} diff --git a/proto/gen/api/v1/attachment_service.pb.go b/proto/gen/api/v1/attachment_service.pb.go new file mode 100644 index 0000000..d86d70e --- /dev/null +++ b/proto/gen/api/v1/attachment_service.pb.go @@ -0,0 +1,687 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: api/v1/attachment_service.proto + +package apiv1 + +import ( + _ "google.golang.org/genproto/googleapis/api/annotations" + httpbody "google.golang.org/genproto/googleapis/api/httpbody" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + fieldmaskpb "google.golang.org/protobuf/types/known/fieldmaskpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Attachment struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The name of the attachment. + // Format: attachments/{attachment} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Output only. The creation timestamp. + CreateTime *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` + // The filename of the attachment. + Filename string `protobuf:"bytes,3,opt,name=filename,proto3" json:"filename,omitempty"` + // Input only. The content of the attachment. + Content []byte `protobuf:"bytes,4,opt,name=content,proto3" json:"content,omitempty"` + // Optional. The external link of the attachment. + ExternalLink string `protobuf:"bytes,5,opt,name=external_link,json=externalLink,proto3" json:"external_link,omitempty"` + // The MIME type of the attachment. + Type string `protobuf:"bytes,6,opt,name=type,proto3" json:"type,omitempty"` + // Output only. The size of the attachment in bytes. + Size int64 `protobuf:"varint,7,opt,name=size,proto3" json:"size,omitempty"` + // Optional. The related memo. Refer to `Memo.name`. + // Format: memos/{memo} + Memo *string `protobuf:"bytes,8,opt,name=memo,proto3,oneof" json:"memo,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Attachment) Reset() { + *x = Attachment{} + mi := &file_api_v1_attachment_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Attachment) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Attachment) ProtoMessage() {} + +func (x *Attachment) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_attachment_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Attachment.ProtoReflect.Descriptor instead. +func (*Attachment) Descriptor() ([]byte, []int) { + return file_api_v1_attachment_service_proto_rawDescGZIP(), []int{0} +} + +func (x *Attachment) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Attachment) GetCreateTime() *timestamppb.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *Attachment) GetFilename() string { + if x != nil { + return x.Filename + } + return "" +} + +func (x *Attachment) GetContent() []byte { + if x != nil { + return x.Content + } + return nil +} + +func (x *Attachment) GetExternalLink() string { + if x != nil { + return x.ExternalLink + } + return "" +} + +func (x *Attachment) GetType() string { + if x != nil { + return x.Type + } + return "" +} + +func (x *Attachment) GetSize() int64 { + if x != nil { + return x.Size + } + return 0 +} + +func (x *Attachment) GetMemo() string { + if x != nil && x.Memo != nil { + return *x.Memo + } + return "" +} + +type CreateAttachmentRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The attachment to create. + Attachment *Attachment `protobuf:"bytes,1,opt,name=attachment,proto3" json:"attachment,omitempty"` + // Optional. The attachment ID to use for this attachment. + // If empty, a unique ID will be generated. + AttachmentId string `protobuf:"bytes,2,opt,name=attachment_id,json=attachmentId,proto3" json:"attachment_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateAttachmentRequest) Reset() { + *x = CreateAttachmentRequest{} + mi := &file_api_v1_attachment_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateAttachmentRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateAttachmentRequest) ProtoMessage() {} + +func (x *CreateAttachmentRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_attachment_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateAttachmentRequest.ProtoReflect.Descriptor instead. +func (*CreateAttachmentRequest) Descriptor() ([]byte, []int) { + return file_api_v1_attachment_service_proto_rawDescGZIP(), []int{1} +} + +func (x *CreateAttachmentRequest) GetAttachment() *Attachment { + if x != nil { + return x.Attachment + } + return nil +} + +func (x *CreateAttachmentRequest) GetAttachmentId() string { + if x != nil { + return x.AttachmentId + } + return "" +} + +type ListAttachmentsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional. The maximum number of attachments to return. + // The service may return fewer than this value. + // If unspecified, at most 50 attachments will be returned. + // The maximum value is 1000; values above 1000 will be coerced to 1000. + PageSize int32 `protobuf:"varint,1,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Optional. A page token, received from a previous `ListAttachments` call. + // Provide this to retrieve the subsequent page. + PageToken string `protobuf:"bytes,2,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + // Optional. Filter to apply to the list results. + // Example: "type=image/png" or "filename:*.jpg" + // Supported operators: =, !=, <, <=, >, >=, : + // Supported fields: filename, type, size, create_time, memo + Filter string `protobuf:"bytes,3,opt,name=filter,proto3" json:"filter,omitempty"` + // Optional. The order to sort results by. + // Example: "create_time desc" or "filename asc" + OrderBy string `protobuf:"bytes,4,opt,name=order_by,json=orderBy,proto3" json:"order_by,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListAttachmentsRequest) Reset() { + *x = ListAttachmentsRequest{} + mi := &file_api_v1_attachment_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListAttachmentsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAttachmentsRequest) ProtoMessage() {} + +func (x *ListAttachmentsRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_attachment_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListAttachmentsRequest.ProtoReflect.Descriptor instead. +func (*ListAttachmentsRequest) Descriptor() ([]byte, []int) { + return file_api_v1_attachment_service_proto_rawDescGZIP(), []int{2} +} + +func (x *ListAttachmentsRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListAttachmentsRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +func (x *ListAttachmentsRequest) GetFilter() string { + if x != nil { + return x.Filter + } + return "" +} + +func (x *ListAttachmentsRequest) GetOrderBy() string { + if x != nil { + return x.OrderBy + } + return "" +} + +type ListAttachmentsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The list of attachments. + Attachments []*Attachment `protobuf:"bytes,1,rep,name=attachments,proto3" json:"attachments,omitempty"` + // A token that can be sent as `page_token` to retrieve the next page. + // If this field is omitted, there are no subsequent pages. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + // The total count of attachments (may be approximate). + TotalSize int32 `protobuf:"varint,3,opt,name=total_size,json=totalSize,proto3" json:"total_size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListAttachmentsResponse) Reset() { + *x = ListAttachmentsResponse{} + mi := &file_api_v1_attachment_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListAttachmentsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAttachmentsResponse) ProtoMessage() {} + +func (x *ListAttachmentsResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_attachment_service_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListAttachmentsResponse.ProtoReflect.Descriptor instead. +func (*ListAttachmentsResponse) Descriptor() ([]byte, []int) { + return file_api_v1_attachment_service_proto_rawDescGZIP(), []int{3} +} + +func (x *ListAttachmentsResponse) GetAttachments() []*Attachment { + if x != nil { + return x.Attachments + } + return nil +} + +func (x *ListAttachmentsResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +func (x *ListAttachmentsResponse) GetTotalSize() int32 { + if x != nil { + return x.TotalSize + } + return 0 +} + +type GetAttachmentRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The attachment name of the attachment to retrieve. + // Format: attachments/{attachment} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAttachmentRequest) Reset() { + *x = GetAttachmentRequest{} + mi := &file_api_v1_attachment_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAttachmentRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAttachmentRequest) ProtoMessage() {} + +func (x *GetAttachmentRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_attachment_service_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetAttachmentRequest.ProtoReflect.Descriptor instead. +func (*GetAttachmentRequest) Descriptor() ([]byte, []int) { + return file_api_v1_attachment_service_proto_rawDescGZIP(), []int{4} +} + +func (x *GetAttachmentRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type GetAttachmentBinaryRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The attachment name of the attachment. + // Format: attachments/{attachment} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // The filename of the attachment. Mainly used for downloading. + Filename string `protobuf:"bytes,2,opt,name=filename,proto3" json:"filename,omitempty"` + // Optional. A flag indicating if the thumbnail version of the attachment should be returned. + Thumbnail bool `protobuf:"varint,3,opt,name=thumbnail,proto3" json:"thumbnail,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetAttachmentBinaryRequest) Reset() { + *x = GetAttachmentBinaryRequest{} + mi := &file_api_v1_attachment_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetAttachmentBinaryRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetAttachmentBinaryRequest) ProtoMessage() {} + +func (x *GetAttachmentBinaryRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_attachment_service_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetAttachmentBinaryRequest.ProtoReflect.Descriptor instead. +func (*GetAttachmentBinaryRequest) Descriptor() ([]byte, []int) { + return file_api_v1_attachment_service_proto_rawDescGZIP(), []int{5} +} + +func (x *GetAttachmentBinaryRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *GetAttachmentBinaryRequest) GetFilename() string { + if x != nil { + return x.Filename + } + return "" +} + +func (x *GetAttachmentBinaryRequest) GetThumbnail() bool { + if x != nil { + return x.Thumbnail + } + return false +} + +type UpdateAttachmentRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The attachment which replaces the attachment on the server. + Attachment *Attachment `protobuf:"bytes,1,opt,name=attachment,proto3" json:"attachment,omitempty"` + // Required. The list of fields to update. + UpdateMask *fieldmaskpb.FieldMask `protobuf:"bytes,2,opt,name=update_mask,json=updateMask,proto3" json:"update_mask,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateAttachmentRequest) Reset() { + *x = UpdateAttachmentRequest{} + mi := &file_api_v1_attachment_service_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateAttachmentRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateAttachmentRequest) ProtoMessage() {} + +func (x *UpdateAttachmentRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_attachment_service_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateAttachmentRequest.ProtoReflect.Descriptor instead. +func (*UpdateAttachmentRequest) Descriptor() ([]byte, []int) { + return file_api_v1_attachment_service_proto_rawDescGZIP(), []int{6} +} + +func (x *UpdateAttachmentRequest) GetAttachment() *Attachment { + if x != nil { + return x.Attachment + } + return nil +} + +func (x *UpdateAttachmentRequest) GetUpdateMask() *fieldmaskpb.FieldMask { + if x != nil { + return x.UpdateMask + } + return nil +} + +type DeleteAttachmentRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The attachment name of the attachment to delete. + // Format: attachments/{attachment} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteAttachmentRequest) Reset() { + *x = DeleteAttachmentRequest{} + mi := &file_api_v1_attachment_service_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteAttachmentRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteAttachmentRequest) ProtoMessage() {} + +func (x *DeleteAttachmentRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_attachment_service_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteAttachmentRequest.ProtoReflect.Descriptor instead. +func (*DeleteAttachmentRequest) Descriptor() ([]byte, []int) { + return file_api_v1_attachment_service_proto_rawDescGZIP(), []int{7} +} + +func (x *DeleteAttachmentRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +var File_api_v1_attachment_service_proto protoreflect.FileDescriptor + +const file_api_v1_attachment_service_proto_rawDesc = "" + + "\n" + + "\x1fapi/v1/attachment_service.proto\x12\fmemos.api.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/httpbody.proto\x1a\x19google/api/resource.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xfb\x02\n" + + "\n" + + "Attachment\x12\x17\n" + + "\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12@\n" + + "\vcreate_time\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\n" + + "createTime\x12\x1f\n" + + "\bfilename\x18\x03 \x01(\tB\x03\xe0A\x02R\bfilename\x12\x1d\n" + + "\acontent\x18\x04 \x01(\fB\x03\xe0A\x04R\acontent\x12(\n" + + "\rexternal_link\x18\x05 \x01(\tB\x03\xe0A\x01R\fexternalLink\x12\x17\n" + + "\x04type\x18\x06 \x01(\tB\x03\xe0A\x02R\x04type\x12\x17\n" + + "\x04size\x18\a \x01(\x03B\x03\xe0A\x03R\x04size\x12\x1c\n" + + "\x04memo\x18\b \x01(\tB\x03\xe0A\x01H\x00R\x04memo\x88\x01\x01:O\xeaAL\n" + + "\x17memos.api.v1/Attachment\x12\x18attachments/{attachment}*\vattachments2\n" + + "attachmentB\a\n" + + "\x05_memo\"\x82\x01\n" + + "\x17CreateAttachmentRequest\x12=\n" + + "\n" + + "attachment\x18\x01 \x01(\v2\x18.memos.api.v1.AttachmentB\x03\xe0A\x02R\n" + + "attachment\x12(\n" + + "\rattachment_id\x18\x02 \x01(\tB\x03\xe0A\x01R\fattachmentId\"\x9b\x01\n" + + "\x16ListAttachmentsRequest\x12 \n" + + "\tpage_size\x18\x01 \x01(\x05B\x03\xe0A\x01R\bpageSize\x12\"\n" + + "\n" + + "page_token\x18\x02 \x01(\tB\x03\xe0A\x01R\tpageToken\x12\x1b\n" + + "\x06filter\x18\x03 \x01(\tB\x03\xe0A\x01R\x06filter\x12\x1e\n" + + "\border_by\x18\x04 \x01(\tB\x03\xe0A\x01R\aorderBy\"\x9c\x01\n" + + "\x17ListAttachmentsResponse\x12:\n" + + "\vattachments\x18\x01 \x03(\v2\x18.memos.api.v1.AttachmentR\vattachments\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12\x1d\n" + + "\n" + + "total_size\x18\x03 \x01(\x05R\ttotalSize\"K\n" + + "\x14GetAttachmentRequest\x123\n" + + "\x04name\x18\x01 \x01(\tB\x1f\xe0A\x02\xfaA\x19\n" + + "\x17memos.api.v1/AttachmentR\x04name\"\x95\x01\n" + + "\x1aGetAttachmentBinaryRequest\x123\n" + + "\x04name\x18\x01 \x01(\tB\x1f\xe0A\x02\xfaA\x19\n" + + "\x17memos.api.v1/AttachmentR\x04name\x12\x1f\n" + + "\bfilename\x18\x02 \x01(\tB\x03\xe0A\x02R\bfilename\x12!\n" + + "\tthumbnail\x18\x03 \x01(\bB\x03\xe0A\x01R\tthumbnail\"\x9a\x01\n" + + "\x17UpdateAttachmentRequest\x12=\n" + + "\n" + + "attachment\x18\x01 \x01(\v2\x18.memos.api.v1.AttachmentB\x03\xe0A\x02R\n" + + "attachment\x12@\n" + + "\vupdate_mask\x18\x02 \x01(\v2\x1a.google.protobuf.FieldMaskB\x03\xe0A\x02R\n" + + "updateMask\"N\n" + + "\x17DeleteAttachmentRequest\x123\n" + + "\x04name\x18\x01 \x01(\tB\x1f\xe0A\x02\xfaA\x19\n" + + "\x17memos.api.v1/AttachmentR\x04name2\xe5\x06\n" + + "\x11AttachmentService\x12\x89\x01\n" + + "\x10CreateAttachment\x12%.memos.api.v1.CreateAttachmentRequest\x1a\x18.memos.api.v1.Attachment\"4\xdaA\n" + + "attachment\x82\xd3\xe4\x93\x02!:\n" + + "attachment\"\x13/api/v1/attachments\x12{\n" + + "\x0fListAttachments\x12$.memos.api.v1.ListAttachmentsRequest\x1a%.memos.api.v1.ListAttachmentsResponse\"\x1b\x82\xd3\xe4\x93\x02\x15\x12\x13/api/v1/attachments\x12z\n" + + "\rGetAttachment\x12\".memos.api.v1.GetAttachmentRequest\x1a\x18.memos.api.v1.Attachment\"+\xdaA\x04name\x82\xd3\xe4\x93\x02\x1e\x12\x1c/api/v1/{name=attachments/*}\x12\x9e\x01\n" + + "\x13GetAttachmentBinary\x12(.memos.api.v1.GetAttachmentBinaryRequest\x1a\x14.google.api.HttpBody\"G\xdaA\x17name,filename,thumbnail\x82\xd3\xe4\x93\x02'\x12%/file/{name=attachments/*}/{filename}\x12\xa9\x01\n" + + "\x10UpdateAttachment\x12%.memos.api.v1.UpdateAttachmentRequest\x1a\x18.memos.api.v1.Attachment\"T\xdaA\x16attachment,update_mask\x82\xd3\xe4\x93\x025:\n" + + "attachment2'/api/v1/{attachment.name=attachments/*}\x12~\n" + + "\x10DeleteAttachment\x12%.memos.api.v1.DeleteAttachmentRequest\x1a\x16.google.protobuf.Empty\"+\xdaA\x04name\x82\xd3\xe4\x93\x02\x1e*\x1c/api/v1/{name=attachments/*}B\xae\x01\n" + + "\x10com.memos.api.v1B\x16AttachmentServiceProtoP\x01Z0github.com/usememos/memos/proto/gen/api/v1;apiv1\xa2\x02\x03MAX\xaa\x02\fMemos.Api.V1\xca\x02\fMemos\\Api\\V1\xe2\x02\x18Memos\\Api\\V1\\GPBMetadata\xea\x02\x0eMemos::Api::V1b\x06proto3" + +var ( + file_api_v1_attachment_service_proto_rawDescOnce sync.Once + file_api_v1_attachment_service_proto_rawDescData []byte +) + +func file_api_v1_attachment_service_proto_rawDescGZIP() []byte { + file_api_v1_attachment_service_proto_rawDescOnce.Do(func() { + file_api_v1_attachment_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_v1_attachment_service_proto_rawDesc), len(file_api_v1_attachment_service_proto_rawDesc))) + }) + return file_api_v1_attachment_service_proto_rawDescData +} + +var file_api_v1_attachment_service_proto_msgTypes = make([]protoimpl.MessageInfo, 8) +var file_api_v1_attachment_service_proto_goTypes = []any{ + (*Attachment)(nil), // 0: memos.api.v1.Attachment + (*CreateAttachmentRequest)(nil), // 1: memos.api.v1.CreateAttachmentRequest + (*ListAttachmentsRequest)(nil), // 2: memos.api.v1.ListAttachmentsRequest + (*ListAttachmentsResponse)(nil), // 3: memos.api.v1.ListAttachmentsResponse + (*GetAttachmentRequest)(nil), // 4: memos.api.v1.GetAttachmentRequest + (*GetAttachmentBinaryRequest)(nil), // 5: memos.api.v1.GetAttachmentBinaryRequest + (*UpdateAttachmentRequest)(nil), // 6: memos.api.v1.UpdateAttachmentRequest + (*DeleteAttachmentRequest)(nil), // 7: memos.api.v1.DeleteAttachmentRequest + (*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp + (*fieldmaskpb.FieldMask)(nil), // 9: google.protobuf.FieldMask + (*httpbody.HttpBody)(nil), // 10: google.api.HttpBody + (*emptypb.Empty)(nil), // 11: google.protobuf.Empty +} +var file_api_v1_attachment_service_proto_depIdxs = []int32{ + 8, // 0: memos.api.v1.Attachment.create_time:type_name -> google.protobuf.Timestamp + 0, // 1: memos.api.v1.CreateAttachmentRequest.attachment:type_name -> memos.api.v1.Attachment + 0, // 2: memos.api.v1.ListAttachmentsResponse.attachments:type_name -> memos.api.v1.Attachment + 0, // 3: memos.api.v1.UpdateAttachmentRequest.attachment:type_name -> memos.api.v1.Attachment + 9, // 4: memos.api.v1.UpdateAttachmentRequest.update_mask:type_name -> google.protobuf.FieldMask + 1, // 5: memos.api.v1.AttachmentService.CreateAttachment:input_type -> memos.api.v1.CreateAttachmentRequest + 2, // 6: memos.api.v1.AttachmentService.ListAttachments:input_type -> memos.api.v1.ListAttachmentsRequest + 4, // 7: memos.api.v1.AttachmentService.GetAttachment:input_type -> memos.api.v1.GetAttachmentRequest + 5, // 8: memos.api.v1.AttachmentService.GetAttachmentBinary:input_type -> memos.api.v1.GetAttachmentBinaryRequest + 6, // 9: memos.api.v1.AttachmentService.UpdateAttachment:input_type -> memos.api.v1.UpdateAttachmentRequest + 7, // 10: memos.api.v1.AttachmentService.DeleteAttachment:input_type -> memos.api.v1.DeleteAttachmentRequest + 0, // 11: memos.api.v1.AttachmentService.CreateAttachment:output_type -> memos.api.v1.Attachment + 3, // 12: memos.api.v1.AttachmentService.ListAttachments:output_type -> memos.api.v1.ListAttachmentsResponse + 0, // 13: memos.api.v1.AttachmentService.GetAttachment:output_type -> memos.api.v1.Attachment + 10, // 14: memos.api.v1.AttachmentService.GetAttachmentBinary:output_type -> google.api.HttpBody + 0, // 15: memos.api.v1.AttachmentService.UpdateAttachment:output_type -> memos.api.v1.Attachment + 11, // 16: memos.api.v1.AttachmentService.DeleteAttachment:output_type -> google.protobuf.Empty + 11, // [11:17] is the sub-list for method output_type + 5, // [5:11] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_api_v1_attachment_service_proto_init() } +func file_api_v1_attachment_service_proto_init() { + if File_api_v1_attachment_service_proto != nil { + return + } + file_api_v1_attachment_service_proto_msgTypes[0].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_attachment_service_proto_rawDesc), len(file_api_v1_attachment_service_proto_rawDesc)), + NumEnums: 0, + NumMessages: 8, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_api_v1_attachment_service_proto_goTypes, + DependencyIndexes: file_api_v1_attachment_service_proto_depIdxs, + MessageInfos: file_api_v1_attachment_service_proto_msgTypes, + }.Build() + File_api_v1_attachment_service_proto = out.File + file_api_v1_attachment_service_proto_goTypes = nil + file_api_v1_attachment_service_proto_depIdxs = nil +} diff --git a/proto/gen/api/v1/attachment_service.pb.gw.go b/proto/gen/api/v1/attachment_service.pb.gw.go new file mode 100644 index 0000000..d31cc5c --- /dev/null +++ b/proto/gen/api/v1/attachment_service.pb.gw.go @@ -0,0 +1,629 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: api/v1/attachment_service.proto + +/* +Package apiv1 is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package apiv1 + +import ( + "context" + "errors" + "io" + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// Suppress "imported and not used" errors +var ( + _ codes.Code + _ io.Reader + _ status.Status + _ = errors.New + _ = runtime.String + _ = utilities.NewDoubleArray + _ = metadata.Join +) + +var filter_AttachmentService_CreateAttachment_0 = &utilities.DoubleArray{Encoding: map[string]int{"attachment": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} + +func request_AttachmentService_CreateAttachment_0(ctx context.Context, marshaler runtime.Marshaler, client AttachmentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateAttachmentRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Attachment); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_AttachmentService_CreateAttachment_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.CreateAttachment(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_AttachmentService_CreateAttachment_0(ctx context.Context, marshaler runtime.Marshaler, server AttachmentServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateAttachmentRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Attachment); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_AttachmentService_CreateAttachment_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.CreateAttachment(ctx, &protoReq) + return msg, metadata, err +} + +var filter_AttachmentService_ListAttachments_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + +func request_AttachmentService_ListAttachments_0(ctx context.Context, marshaler runtime.Marshaler, client AttachmentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListAttachmentsRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_AttachmentService_ListAttachments_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.ListAttachments(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_AttachmentService_ListAttachments_0(ctx context.Context, marshaler runtime.Marshaler, server AttachmentServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListAttachmentsRequest + metadata runtime.ServerMetadata + ) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_AttachmentService_ListAttachments_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ListAttachments(ctx, &protoReq) + return msg, metadata, err +} + +func request_AttachmentService_GetAttachment_0(ctx context.Context, marshaler runtime.Marshaler, client AttachmentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetAttachmentRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := client.GetAttachment(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_AttachmentService_GetAttachment_0(ctx context.Context, marshaler runtime.Marshaler, server AttachmentServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetAttachmentRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := server.GetAttachment(ctx, &protoReq) + return msg, metadata, err +} + +var filter_AttachmentService_GetAttachmentBinary_0 = &utilities.DoubleArray{Encoding: map[string]int{"name": 0, "filename": 1}, Base: []int{1, 1, 2, 0, 0}, Check: []int{0, 1, 1, 2, 3}} + +func request_AttachmentService_GetAttachmentBinary_0(ctx context.Context, marshaler runtime.Marshaler, client AttachmentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetAttachmentBinaryRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + val, ok = pathParams["filename"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "filename") + } + protoReq.Filename, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "filename", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_AttachmentService_GetAttachmentBinary_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.GetAttachmentBinary(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_AttachmentService_GetAttachmentBinary_0(ctx context.Context, marshaler runtime.Marshaler, server AttachmentServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetAttachmentBinaryRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + val, ok = pathParams["filename"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "filename") + } + protoReq.Filename, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "filename", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_AttachmentService_GetAttachmentBinary_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.GetAttachmentBinary(ctx, &protoReq) + return msg, metadata, err +} + +var filter_AttachmentService_UpdateAttachment_0 = &utilities.DoubleArray{Encoding: map[string]int{"attachment": 0, "name": 1}, Base: []int{1, 2, 1, 0, 0}, Check: []int{0, 1, 2, 3, 2}} + +func request_AttachmentService_UpdateAttachment_0(ctx context.Context, marshaler runtime.Marshaler, client AttachmentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateAttachmentRequest + metadata runtime.ServerMetadata + err error + ) + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Attachment); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 { + if fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Attachment); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } else { + protoReq.UpdateMask = fieldMask + } + } + val, ok := pathParams["attachment.name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "attachment.name") + } + err = runtime.PopulateFieldFromPath(&protoReq, "attachment.name", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "attachment.name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_AttachmentService_UpdateAttachment_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.UpdateAttachment(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_AttachmentService_UpdateAttachment_0(ctx context.Context, marshaler runtime.Marshaler, server AttachmentServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateAttachmentRequest + metadata runtime.ServerMetadata + err error + ) + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Attachment); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 { + if fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Attachment); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } else { + protoReq.UpdateMask = fieldMask + } + } + val, ok := pathParams["attachment.name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "attachment.name") + } + err = runtime.PopulateFieldFromPath(&protoReq, "attachment.name", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "attachment.name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_AttachmentService_UpdateAttachment_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.UpdateAttachment(ctx, &protoReq) + return msg, metadata, err +} + +func request_AttachmentService_DeleteAttachment_0(ctx context.Context, marshaler runtime.Marshaler, client AttachmentServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteAttachmentRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := client.DeleteAttachment(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_AttachmentService_DeleteAttachment_0(ctx context.Context, marshaler runtime.Marshaler, server AttachmentServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteAttachmentRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := server.DeleteAttachment(ctx, &protoReq) + return msg, metadata, err +} + +// RegisterAttachmentServiceHandlerServer registers the http handlers for service AttachmentService to "mux". +// UnaryRPC :call AttachmentServiceServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterAttachmentServiceHandlerFromEndpoint instead. +// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call. +func RegisterAttachmentServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server AttachmentServiceServer) error { + mux.Handle(http.MethodPost, pattern_AttachmentService_CreateAttachment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.AttachmentService/CreateAttachment", runtime.WithHTTPPathPattern("/api/v1/attachments")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_AttachmentService_CreateAttachment_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AttachmentService_CreateAttachment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_AttachmentService_ListAttachments_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.AttachmentService/ListAttachments", runtime.WithHTTPPathPattern("/api/v1/attachments")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_AttachmentService_ListAttachments_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AttachmentService_ListAttachments_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_AttachmentService_GetAttachment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.AttachmentService/GetAttachment", runtime.WithHTTPPathPattern("/api/v1/{name=attachments/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_AttachmentService_GetAttachment_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AttachmentService_GetAttachment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_AttachmentService_GetAttachmentBinary_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.AttachmentService/GetAttachmentBinary", runtime.WithHTTPPathPattern("/file/{name=attachments/*}/{filename}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_AttachmentService_GetAttachmentBinary_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AttachmentService_GetAttachmentBinary_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPatch, pattern_AttachmentService_UpdateAttachment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.AttachmentService/UpdateAttachment", runtime.WithHTTPPathPattern("/api/v1/{attachment.name=attachments/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_AttachmentService_UpdateAttachment_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AttachmentService_UpdateAttachment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_AttachmentService_DeleteAttachment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.AttachmentService/DeleteAttachment", runtime.WithHTTPPathPattern("/api/v1/{name=attachments/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_AttachmentService_DeleteAttachment_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AttachmentService_DeleteAttachment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + + return nil +} + +// RegisterAttachmentServiceHandlerFromEndpoint is same as RegisterAttachmentServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterAttachmentServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.NewClient(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + return RegisterAttachmentServiceHandler(ctx, mux, conn) +} + +// RegisterAttachmentServiceHandler registers the http handlers for service AttachmentService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterAttachmentServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterAttachmentServiceHandlerClient(ctx, mux, NewAttachmentServiceClient(conn)) +} + +// RegisterAttachmentServiceHandlerClient registers the http handlers for service AttachmentService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "AttachmentServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "AttachmentServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "AttachmentServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares. +func RegisterAttachmentServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client AttachmentServiceClient) error { + mux.Handle(http.MethodPost, pattern_AttachmentService_CreateAttachment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.AttachmentService/CreateAttachment", runtime.WithHTTPPathPattern("/api/v1/attachments")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_AttachmentService_CreateAttachment_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AttachmentService_CreateAttachment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_AttachmentService_ListAttachments_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.AttachmentService/ListAttachments", runtime.WithHTTPPathPattern("/api/v1/attachments")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_AttachmentService_ListAttachments_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AttachmentService_ListAttachments_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_AttachmentService_GetAttachment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.AttachmentService/GetAttachment", runtime.WithHTTPPathPattern("/api/v1/{name=attachments/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_AttachmentService_GetAttachment_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AttachmentService_GetAttachment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_AttachmentService_GetAttachmentBinary_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.AttachmentService/GetAttachmentBinary", runtime.WithHTTPPathPattern("/file/{name=attachments/*}/{filename}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_AttachmentService_GetAttachmentBinary_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AttachmentService_GetAttachmentBinary_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPatch, pattern_AttachmentService_UpdateAttachment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.AttachmentService/UpdateAttachment", runtime.WithHTTPPathPattern("/api/v1/{attachment.name=attachments/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_AttachmentService_UpdateAttachment_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AttachmentService_UpdateAttachment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_AttachmentService_DeleteAttachment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.AttachmentService/DeleteAttachment", runtime.WithHTTPPathPattern("/api/v1/{name=attachments/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_AttachmentService_DeleteAttachment_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AttachmentService_DeleteAttachment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + return nil +} + +var ( + pattern_AttachmentService_CreateAttachment_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "attachments"}, "")) + pattern_AttachmentService_ListAttachments_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "attachments"}, "")) + pattern_AttachmentService_GetAttachment_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "attachments", "name"}, "")) + pattern_AttachmentService_GetAttachmentBinary_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 1, 0, 4, 2, 5, 2, 1, 0, 4, 1, 5, 3}, []string{"file", "attachments", "name", "filename"}, "")) + pattern_AttachmentService_UpdateAttachment_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "attachments", "attachment.name"}, "")) + pattern_AttachmentService_DeleteAttachment_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "attachments", "name"}, "")) +) + +var ( + forward_AttachmentService_CreateAttachment_0 = runtime.ForwardResponseMessage + forward_AttachmentService_ListAttachments_0 = runtime.ForwardResponseMessage + forward_AttachmentService_GetAttachment_0 = runtime.ForwardResponseMessage + forward_AttachmentService_GetAttachmentBinary_0 = runtime.ForwardResponseMessage + forward_AttachmentService_UpdateAttachment_0 = runtime.ForwardResponseMessage + forward_AttachmentService_DeleteAttachment_0 = runtime.ForwardResponseMessage +) diff --git a/proto/gen/api/v1/attachment_service_grpc.pb.go b/proto/gen/api/v1/attachment_service_grpc.pb.go new file mode 100644 index 0000000..fa07861 --- /dev/null +++ b/proto/gen/api/v1/attachment_service_grpc.pb.go @@ -0,0 +1,325 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc (unknown) +// source: api/v1/attachment_service.proto + +package apiv1 + +import ( + context "context" + httpbody "google.golang.org/genproto/googleapis/api/httpbody" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + AttachmentService_CreateAttachment_FullMethodName = "/memos.api.v1.AttachmentService/CreateAttachment" + AttachmentService_ListAttachments_FullMethodName = "/memos.api.v1.AttachmentService/ListAttachments" + AttachmentService_GetAttachment_FullMethodName = "/memos.api.v1.AttachmentService/GetAttachment" + AttachmentService_GetAttachmentBinary_FullMethodName = "/memos.api.v1.AttachmentService/GetAttachmentBinary" + AttachmentService_UpdateAttachment_FullMethodName = "/memos.api.v1.AttachmentService/UpdateAttachment" + AttachmentService_DeleteAttachment_FullMethodName = "/memos.api.v1.AttachmentService/DeleteAttachment" +) + +// AttachmentServiceClient is the client API for AttachmentService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type AttachmentServiceClient interface { + // CreateAttachment creates a new attachment. + CreateAttachment(ctx context.Context, in *CreateAttachmentRequest, opts ...grpc.CallOption) (*Attachment, error) + // ListAttachments lists all attachments. + ListAttachments(ctx context.Context, in *ListAttachmentsRequest, opts ...grpc.CallOption) (*ListAttachmentsResponse, error) + // GetAttachment returns a attachment by name. + GetAttachment(ctx context.Context, in *GetAttachmentRequest, opts ...grpc.CallOption) (*Attachment, error) + // GetAttachmentBinary returns a attachment binary by name. + GetAttachmentBinary(ctx context.Context, in *GetAttachmentBinaryRequest, opts ...grpc.CallOption) (*httpbody.HttpBody, error) + // UpdateAttachment updates a attachment. + UpdateAttachment(ctx context.Context, in *UpdateAttachmentRequest, opts ...grpc.CallOption) (*Attachment, error) + // DeleteAttachment deletes a attachment by name. + DeleteAttachment(ctx context.Context, in *DeleteAttachmentRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +type attachmentServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewAttachmentServiceClient(cc grpc.ClientConnInterface) AttachmentServiceClient { + return &attachmentServiceClient{cc} +} + +func (c *attachmentServiceClient) CreateAttachment(ctx context.Context, in *CreateAttachmentRequest, opts ...grpc.CallOption) (*Attachment, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Attachment) + err := c.cc.Invoke(ctx, AttachmentService_CreateAttachment_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *attachmentServiceClient) ListAttachments(ctx context.Context, in *ListAttachmentsRequest, opts ...grpc.CallOption) (*ListAttachmentsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListAttachmentsResponse) + err := c.cc.Invoke(ctx, AttachmentService_ListAttachments_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *attachmentServiceClient) GetAttachment(ctx context.Context, in *GetAttachmentRequest, opts ...grpc.CallOption) (*Attachment, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Attachment) + err := c.cc.Invoke(ctx, AttachmentService_GetAttachment_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *attachmentServiceClient) GetAttachmentBinary(ctx context.Context, in *GetAttachmentBinaryRequest, opts ...grpc.CallOption) (*httpbody.HttpBody, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(httpbody.HttpBody) + err := c.cc.Invoke(ctx, AttachmentService_GetAttachmentBinary_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *attachmentServiceClient) UpdateAttachment(ctx context.Context, in *UpdateAttachmentRequest, opts ...grpc.CallOption) (*Attachment, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Attachment) + err := c.cc.Invoke(ctx, AttachmentService_UpdateAttachment_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *attachmentServiceClient) DeleteAttachment(ctx context.Context, in *DeleteAttachmentRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, AttachmentService_DeleteAttachment_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// AttachmentServiceServer is the server API for AttachmentService service. +// All implementations must embed UnimplementedAttachmentServiceServer +// for forward compatibility. +type AttachmentServiceServer interface { + // CreateAttachment creates a new attachment. + CreateAttachment(context.Context, *CreateAttachmentRequest) (*Attachment, error) + // ListAttachments lists all attachments. + ListAttachments(context.Context, *ListAttachmentsRequest) (*ListAttachmentsResponse, error) + // GetAttachment returns a attachment by name. + GetAttachment(context.Context, *GetAttachmentRequest) (*Attachment, error) + // GetAttachmentBinary returns a attachment binary by name. + GetAttachmentBinary(context.Context, *GetAttachmentBinaryRequest) (*httpbody.HttpBody, error) + // UpdateAttachment updates a attachment. + UpdateAttachment(context.Context, *UpdateAttachmentRequest) (*Attachment, error) + // DeleteAttachment deletes a attachment by name. + DeleteAttachment(context.Context, *DeleteAttachmentRequest) (*emptypb.Empty, error) + mustEmbedUnimplementedAttachmentServiceServer() +} + +// UnimplementedAttachmentServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedAttachmentServiceServer struct{} + +func (UnimplementedAttachmentServiceServer) CreateAttachment(context.Context, *CreateAttachmentRequest) (*Attachment, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateAttachment not implemented") +} +func (UnimplementedAttachmentServiceServer) ListAttachments(context.Context, *ListAttachmentsRequest) (*ListAttachmentsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListAttachments not implemented") +} +func (UnimplementedAttachmentServiceServer) GetAttachment(context.Context, *GetAttachmentRequest) (*Attachment, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetAttachment not implemented") +} +func (UnimplementedAttachmentServiceServer) GetAttachmentBinary(context.Context, *GetAttachmentBinaryRequest) (*httpbody.HttpBody, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetAttachmentBinary not implemented") +} +func (UnimplementedAttachmentServiceServer) UpdateAttachment(context.Context, *UpdateAttachmentRequest) (*Attachment, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateAttachment not implemented") +} +func (UnimplementedAttachmentServiceServer) DeleteAttachment(context.Context, *DeleteAttachmentRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteAttachment not implemented") +} +func (UnimplementedAttachmentServiceServer) mustEmbedUnimplementedAttachmentServiceServer() {} +func (UnimplementedAttachmentServiceServer) testEmbeddedByValue() {} + +// UnsafeAttachmentServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to AttachmentServiceServer will +// result in compilation errors. +type UnsafeAttachmentServiceServer interface { + mustEmbedUnimplementedAttachmentServiceServer() +} + +func RegisterAttachmentServiceServer(s grpc.ServiceRegistrar, srv AttachmentServiceServer) { + // If the following call pancis, it indicates UnimplementedAttachmentServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&AttachmentService_ServiceDesc, srv) +} + +func _AttachmentService_CreateAttachment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateAttachmentRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AttachmentServiceServer).CreateAttachment(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AttachmentService_CreateAttachment_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AttachmentServiceServer).CreateAttachment(ctx, req.(*CreateAttachmentRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AttachmentService_ListAttachments_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListAttachmentsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AttachmentServiceServer).ListAttachments(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AttachmentService_ListAttachments_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AttachmentServiceServer).ListAttachments(ctx, req.(*ListAttachmentsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AttachmentService_GetAttachment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetAttachmentRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AttachmentServiceServer).GetAttachment(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AttachmentService_GetAttachment_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AttachmentServiceServer).GetAttachment(ctx, req.(*GetAttachmentRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AttachmentService_GetAttachmentBinary_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetAttachmentBinaryRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AttachmentServiceServer).GetAttachmentBinary(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AttachmentService_GetAttachmentBinary_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AttachmentServiceServer).GetAttachmentBinary(ctx, req.(*GetAttachmentBinaryRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AttachmentService_UpdateAttachment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateAttachmentRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AttachmentServiceServer).UpdateAttachment(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AttachmentService_UpdateAttachment_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AttachmentServiceServer).UpdateAttachment(ctx, req.(*UpdateAttachmentRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AttachmentService_DeleteAttachment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteAttachmentRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AttachmentServiceServer).DeleteAttachment(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AttachmentService_DeleteAttachment_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AttachmentServiceServer).DeleteAttachment(ctx, req.(*DeleteAttachmentRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// AttachmentService_ServiceDesc is the grpc.ServiceDesc for AttachmentService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var AttachmentService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "memos.api.v1.AttachmentService", + HandlerType: (*AttachmentServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CreateAttachment", + Handler: _AttachmentService_CreateAttachment_Handler, + }, + { + MethodName: "ListAttachments", + Handler: _AttachmentService_ListAttachments_Handler, + }, + { + MethodName: "GetAttachment", + Handler: _AttachmentService_GetAttachment_Handler, + }, + { + MethodName: "GetAttachmentBinary", + Handler: _AttachmentService_GetAttachmentBinary_Handler, + }, + { + MethodName: "UpdateAttachment", + Handler: _AttachmentService_UpdateAttachment_Handler, + }, + { + MethodName: "DeleteAttachment", + Handler: _AttachmentService_DeleteAttachment_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "api/v1/attachment_service.proto", +} diff --git a/proto/gen/api/v1/auth_service.pb.go b/proto/gen/api/v1/auth_service.pb.go new file mode 100644 index 0000000..a194df5 --- /dev/null +++ b/proto/gen/api/v1/auth_service.pb.go @@ -0,0 +1,521 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: api/v1/auth_service.proto + +package apiv1 + +import ( + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type GetCurrentSessionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetCurrentSessionRequest) Reset() { + *x = GetCurrentSessionRequest{} + mi := &file_api_v1_auth_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetCurrentSessionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetCurrentSessionRequest) ProtoMessage() {} + +func (x *GetCurrentSessionRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_auth_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetCurrentSessionRequest.ProtoReflect.Descriptor instead. +func (*GetCurrentSessionRequest) Descriptor() ([]byte, []int) { + return file_api_v1_auth_service_proto_rawDescGZIP(), []int{0} +} + +type GetCurrentSessionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + // Last time the session was accessed. + // Used for sliding expiration calculation (last_accessed_time + 2 weeks). + LastAccessedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=last_accessed_at,json=lastAccessedAt,proto3" json:"last_accessed_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetCurrentSessionResponse) Reset() { + *x = GetCurrentSessionResponse{} + mi := &file_api_v1_auth_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetCurrentSessionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetCurrentSessionResponse) ProtoMessage() {} + +func (x *GetCurrentSessionResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_auth_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetCurrentSessionResponse.ProtoReflect.Descriptor instead. +func (*GetCurrentSessionResponse) Descriptor() ([]byte, []int) { + return file_api_v1_auth_service_proto_rawDescGZIP(), []int{1} +} + +func (x *GetCurrentSessionResponse) GetUser() *User { + if x != nil { + return x.User + } + return nil +} + +func (x *GetCurrentSessionResponse) GetLastAccessedAt() *timestamppb.Timestamp { + if x != nil { + return x.LastAccessedAt + } + return nil +} + +type CreateSessionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Provide one authentication method (username/password or SSO). + // Required field to specify the authentication method. + // + // Types that are valid to be assigned to Credentials: + // + // *CreateSessionRequest_PasswordCredentials_ + // *CreateSessionRequest_SsoCredentials + Credentials isCreateSessionRequest_Credentials `protobuf_oneof:"credentials"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateSessionRequest) Reset() { + *x = CreateSessionRequest{} + mi := &file_api_v1_auth_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateSessionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateSessionRequest) ProtoMessage() {} + +func (x *CreateSessionRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_auth_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateSessionRequest.ProtoReflect.Descriptor instead. +func (*CreateSessionRequest) Descriptor() ([]byte, []int) { + return file_api_v1_auth_service_proto_rawDescGZIP(), []int{2} +} + +func (x *CreateSessionRequest) GetCredentials() isCreateSessionRequest_Credentials { + if x != nil { + return x.Credentials + } + return nil +} + +func (x *CreateSessionRequest) GetPasswordCredentials() *CreateSessionRequest_PasswordCredentials { + if x != nil { + if x, ok := x.Credentials.(*CreateSessionRequest_PasswordCredentials_); ok { + return x.PasswordCredentials + } + } + return nil +} + +func (x *CreateSessionRequest) GetSsoCredentials() *CreateSessionRequest_SSOCredentials { + if x != nil { + if x, ok := x.Credentials.(*CreateSessionRequest_SsoCredentials); ok { + return x.SsoCredentials + } + } + return nil +} + +type isCreateSessionRequest_Credentials interface { + isCreateSessionRequest_Credentials() +} + +type CreateSessionRequest_PasswordCredentials_ struct { + // Username and password authentication method. + PasswordCredentials *CreateSessionRequest_PasswordCredentials `protobuf:"bytes,1,opt,name=password_credentials,json=passwordCredentials,proto3,oneof"` +} + +type CreateSessionRequest_SsoCredentials struct { + // SSO provider authentication method. + SsoCredentials *CreateSessionRequest_SSOCredentials `protobuf:"bytes,2,opt,name=sso_credentials,json=ssoCredentials,proto3,oneof"` +} + +func (*CreateSessionRequest_PasswordCredentials_) isCreateSessionRequest_Credentials() {} + +func (*CreateSessionRequest_SsoCredentials) isCreateSessionRequest_Credentials() {} + +type CreateSessionResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The authenticated user information. + User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + // Last time the session was accessed. + // Used for sliding expiration calculation (last_accessed_time + 2 weeks). + LastAccessedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=last_accessed_at,json=lastAccessedAt,proto3" json:"last_accessed_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateSessionResponse) Reset() { + *x = CreateSessionResponse{} + mi := &file_api_v1_auth_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateSessionResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateSessionResponse) ProtoMessage() {} + +func (x *CreateSessionResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_auth_service_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateSessionResponse.ProtoReflect.Descriptor instead. +func (*CreateSessionResponse) Descriptor() ([]byte, []int) { + return file_api_v1_auth_service_proto_rawDescGZIP(), []int{3} +} + +func (x *CreateSessionResponse) GetUser() *User { + if x != nil { + return x.User + } + return nil +} + +func (x *CreateSessionResponse) GetLastAccessedAt() *timestamppb.Timestamp { + if x != nil { + return x.LastAccessedAt + } + return nil +} + +type DeleteSessionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteSessionRequest) Reset() { + *x = DeleteSessionRequest{} + mi := &file_api_v1_auth_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteSessionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteSessionRequest) ProtoMessage() {} + +func (x *DeleteSessionRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_auth_service_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteSessionRequest.ProtoReflect.Descriptor instead. +func (*DeleteSessionRequest) Descriptor() ([]byte, []int) { + return file_api_v1_auth_service_proto_rawDescGZIP(), []int{4} +} + +// Nested message for password-based authentication credentials. +type CreateSessionRequest_PasswordCredentials struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The username to sign in with. + // Required field for password-based authentication. + Username string `protobuf:"bytes,1,opt,name=username,proto3" json:"username,omitempty"` + // The password to sign in with. + // Required field for password-based authentication. + Password string `protobuf:"bytes,2,opt,name=password,proto3" json:"password,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateSessionRequest_PasswordCredentials) Reset() { + *x = CreateSessionRequest_PasswordCredentials{} + mi := &file_api_v1_auth_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateSessionRequest_PasswordCredentials) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateSessionRequest_PasswordCredentials) ProtoMessage() {} + +func (x *CreateSessionRequest_PasswordCredentials) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_auth_service_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateSessionRequest_PasswordCredentials.ProtoReflect.Descriptor instead. +func (*CreateSessionRequest_PasswordCredentials) Descriptor() ([]byte, []int) { + return file_api_v1_auth_service_proto_rawDescGZIP(), []int{2, 0} +} + +func (x *CreateSessionRequest_PasswordCredentials) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *CreateSessionRequest_PasswordCredentials) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +// Nested message for SSO authentication credentials. +type CreateSessionRequest_SSOCredentials struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The ID of the SSO provider. + // Required field to identify the SSO provider. + IdpId int32 `protobuf:"varint,1,opt,name=idp_id,json=idpId,proto3" json:"idp_id,omitempty"` + // The authorization code from the SSO provider. + // Required field for completing the SSO flow. + Code string `protobuf:"bytes,2,opt,name=code,proto3" json:"code,omitempty"` + // The redirect URI used in the SSO flow. + // Required field for security validation. + RedirectUri string `protobuf:"bytes,3,opt,name=redirect_uri,json=redirectUri,proto3" json:"redirect_uri,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateSessionRequest_SSOCredentials) Reset() { + *x = CreateSessionRequest_SSOCredentials{} + mi := &file_api_v1_auth_service_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateSessionRequest_SSOCredentials) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateSessionRequest_SSOCredentials) ProtoMessage() {} + +func (x *CreateSessionRequest_SSOCredentials) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_auth_service_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateSessionRequest_SSOCredentials.ProtoReflect.Descriptor instead. +func (*CreateSessionRequest_SSOCredentials) Descriptor() ([]byte, []int) { + return file_api_v1_auth_service_proto_rawDescGZIP(), []int{2, 1} +} + +func (x *CreateSessionRequest_SSOCredentials) GetIdpId() int32 { + if x != nil { + return x.IdpId + } + return 0 +} + +func (x *CreateSessionRequest_SSOCredentials) GetCode() string { + if x != nil { + return x.Code + } + return "" +} + +func (x *CreateSessionRequest_SSOCredentials) GetRedirectUri() string { + if x != nil { + return x.RedirectUri + } + return "" +} + +var File_api_v1_auth_service_proto protoreflect.FileDescriptor + +const file_api_v1_auth_service_proto_rawDesc = "" + + "\n" + + "\x19api/v1/auth_service.proto\x12\fmemos.api.v1\x1a\x19api/v1/user_service.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x1a\n" + + "\x18GetCurrentSessionRequest\"\x89\x01\n" + + "\x19GetCurrentSessionResponse\x12&\n" + + "\x04user\x18\x01 \x01(\v2\x12.memos.api.v1.UserR\x04user\x12D\n" + + "\x10last_accessed_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x0elastAccessedAt\"\xb8\x03\n" + + "\x14CreateSessionRequest\x12k\n" + + "\x14password_credentials\x18\x01 \x01(\v26.memos.api.v1.CreateSessionRequest.PasswordCredentialsH\x00R\x13passwordCredentials\x12\\\n" + + "\x0fsso_credentials\x18\x02 \x01(\v21.memos.api.v1.CreateSessionRequest.SSOCredentialsH\x00R\x0essoCredentials\x1aW\n" + + "\x13PasswordCredentials\x12\x1f\n" + + "\busername\x18\x01 \x01(\tB\x03\xe0A\x02R\busername\x12\x1f\n" + + "\bpassword\x18\x02 \x01(\tB\x03\xe0A\x02R\bpassword\x1am\n" + + "\x0eSSOCredentials\x12\x1a\n" + + "\x06idp_id\x18\x01 \x01(\x05B\x03\xe0A\x02R\x05idpId\x12\x17\n" + + "\x04code\x18\x02 \x01(\tB\x03\xe0A\x02R\x04code\x12&\n" + + "\fredirect_uri\x18\x03 \x01(\tB\x03\xe0A\x02R\vredirectUriB\r\n" + + "\vcredentials\"\x85\x01\n" + + "\x15CreateSessionResponse\x12&\n" + + "\x04user\x18\x01 \x01(\v2\x12.memos.api.v1.UserR\x04user\x12D\n" + + "\x10last_accessed_at\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\x0elastAccessedAt\"\x16\n" + + "\x14DeleteSessionRequest2\x8b\x03\n" + + "\vAuthService\x12\x8b\x01\n" + + "\x11GetCurrentSession\x12&.memos.api.v1.GetCurrentSessionRequest\x1a'.memos.api.v1.GetCurrentSessionResponse\"%\x82\xd3\xe4\x93\x02\x1f\x12\x1d/api/v1/auth/sessions/current\x12z\n" + + "\rCreateSession\x12\".memos.api.v1.CreateSessionRequest\x1a#.memos.api.v1.CreateSessionResponse\" \x82\xd3\xe4\x93\x02\x1a:\x01*\"\x15/api/v1/auth/sessions\x12r\n" + + "\rDeleteSession\x12\".memos.api.v1.DeleteSessionRequest\x1a\x16.google.protobuf.Empty\"%\x82\xd3\xe4\x93\x02\x1f*\x1d/api/v1/auth/sessions/currentB\xa8\x01\n" + + "\x10com.memos.api.v1B\x10AuthServiceProtoP\x01Z0github.com/usememos/memos/proto/gen/api/v1;apiv1\xa2\x02\x03MAX\xaa\x02\fMemos.Api.V1\xca\x02\fMemos\\Api\\V1\xe2\x02\x18Memos\\Api\\V1\\GPBMetadata\xea\x02\x0eMemos::Api::V1b\x06proto3" + +var ( + file_api_v1_auth_service_proto_rawDescOnce sync.Once + file_api_v1_auth_service_proto_rawDescData []byte +) + +func file_api_v1_auth_service_proto_rawDescGZIP() []byte { + file_api_v1_auth_service_proto_rawDescOnce.Do(func() { + file_api_v1_auth_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_v1_auth_service_proto_rawDesc), len(file_api_v1_auth_service_proto_rawDesc))) + }) + return file_api_v1_auth_service_proto_rawDescData +} + +var file_api_v1_auth_service_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_api_v1_auth_service_proto_goTypes = []any{ + (*GetCurrentSessionRequest)(nil), // 0: memos.api.v1.GetCurrentSessionRequest + (*GetCurrentSessionResponse)(nil), // 1: memos.api.v1.GetCurrentSessionResponse + (*CreateSessionRequest)(nil), // 2: memos.api.v1.CreateSessionRequest + (*CreateSessionResponse)(nil), // 3: memos.api.v1.CreateSessionResponse + (*DeleteSessionRequest)(nil), // 4: memos.api.v1.DeleteSessionRequest + (*CreateSessionRequest_PasswordCredentials)(nil), // 5: memos.api.v1.CreateSessionRequest.PasswordCredentials + (*CreateSessionRequest_SSOCredentials)(nil), // 6: memos.api.v1.CreateSessionRequest.SSOCredentials + (*User)(nil), // 7: memos.api.v1.User + (*timestamppb.Timestamp)(nil), // 8: google.protobuf.Timestamp + (*emptypb.Empty)(nil), // 9: google.protobuf.Empty +} +var file_api_v1_auth_service_proto_depIdxs = []int32{ + 7, // 0: memos.api.v1.GetCurrentSessionResponse.user:type_name -> memos.api.v1.User + 8, // 1: memos.api.v1.GetCurrentSessionResponse.last_accessed_at:type_name -> google.protobuf.Timestamp + 5, // 2: memos.api.v1.CreateSessionRequest.password_credentials:type_name -> memos.api.v1.CreateSessionRequest.PasswordCredentials + 6, // 3: memos.api.v1.CreateSessionRequest.sso_credentials:type_name -> memos.api.v1.CreateSessionRequest.SSOCredentials + 7, // 4: memos.api.v1.CreateSessionResponse.user:type_name -> memos.api.v1.User + 8, // 5: memos.api.v1.CreateSessionResponse.last_accessed_at:type_name -> google.protobuf.Timestamp + 0, // 6: memos.api.v1.AuthService.GetCurrentSession:input_type -> memos.api.v1.GetCurrentSessionRequest + 2, // 7: memos.api.v1.AuthService.CreateSession:input_type -> memos.api.v1.CreateSessionRequest + 4, // 8: memos.api.v1.AuthService.DeleteSession:input_type -> memos.api.v1.DeleteSessionRequest + 1, // 9: memos.api.v1.AuthService.GetCurrentSession:output_type -> memos.api.v1.GetCurrentSessionResponse + 3, // 10: memos.api.v1.AuthService.CreateSession:output_type -> memos.api.v1.CreateSessionResponse + 9, // 11: memos.api.v1.AuthService.DeleteSession:output_type -> google.protobuf.Empty + 9, // [9:12] is the sub-list for method output_type + 6, // [6:9] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_api_v1_auth_service_proto_init() } +func file_api_v1_auth_service_proto_init() { + if File_api_v1_auth_service_proto != nil { + return + } + file_api_v1_user_service_proto_init() + file_api_v1_auth_service_proto_msgTypes[2].OneofWrappers = []any{ + (*CreateSessionRequest_PasswordCredentials_)(nil), + (*CreateSessionRequest_SsoCredentials)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_auth_service_proto_rawDesc), len(file_api_v1_auth_service_proto_rawDesc)), + NumEnums: 0, + NumMessages: 7, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_api_v1_auth_service_proto_goTypes, + DependencyIndexes: file_api_v1_auth_service_proto_depIdxs, + MessageInfos: file_api_v1_auth_service_proto_msgTypes, + }.Build() + File_api_v1_auth_service_proto = out.File + file_api_v1_auth_service_proto_goTypes = nil + file_api_v1_auth_service_proto_depIdxs = nil +} diff --git a/proto/gen/api/v1/auth_service.pb.gw.go b/proto/gen/api/v1/auth_service.pb.gw.go new file mode 100644 index 0000000..1b603fa --- /dev/null +++ b/proto/gen/api/v1/auth_service.pb.gw.go @@ -0,0 +1,277 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: api/v1/auth_service.proto + +/* +Package apiv1 is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package apiv1 + +import ( + "context" + "errors" + "io" + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// Suppress "imported and not used" errors +var ( + _ codes.Code + _ io.Reader + _ status.Status + _ = errors.New + _ = runtime.String + _ = utilities.NewDoubleArray + _ = metadata.Join +) + +func request_AuthService_GetCurrentSession_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetCurrentSessionRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.GetCurrentSession(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_AuthService_GetCurrentSession_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetCurrentSessionRequest + metadata runtime.ServerMetadata + ) + msg, err := server.GetCurrentSession(ctx, &protoReq) + return msg, metadata, err +} + +func request_AuthService_CreateSession_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateSessionRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.CreateSession(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_AuthService_CreateSession_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateSessionRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.CreateSession(ctx, &protoReq) + return msg, metadata, err +} + +func request_AuthService_DeleteSession_0(ctx context.Context, marshaler runtime.Marshaler, client AuthServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteSessionRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.DeleteSession(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_AuthService_DeleteSession_0(ctx context.Context, marshaler runtime.Marshaler, server AuthServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteSessionRequest + metadata runtime.ServerMetadata + ) + msg, err := server.DeleteSession(ctx, &protoReq) + return msg, metadata, err +} + +// RegisterAuthServiceHandlerServer registers the http handlers for service AuthService to "mux". +// UnaryRPC :call AuthServiceServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterAuthServiceHandlerFromEndpoint instead. +// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call. +func RegisterAuthServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server AuthServiceServer) error { + mux.Handle(http.MethodGet, pattern_AuthService_GetCurrentSession_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.AuthService/GetCurrentSession", runtime.WithHTTPPathPattern("/api/v1/auth/sessions/current")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_AuthService_GetCurrentSession_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_GetCurrentSession_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_AuthService_CreateSession_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.AuthService/CreateSession", runtime.WithHTTPPathPattern("/api/v1/auth/sessions")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_AuthService_CreateSession_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_CreateSession_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_AuthService_DeleteSession_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.AuthService/DeleteSession", runtime.WithHTTPPathPattern("/api/v1/auth/sessions/current")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_AuthService_DeleteSession_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_DeleteSession_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + + return nil +} + +// RegisterAuthServiceHandlerFromEndpoint is same as RegisterAuthServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterAuthServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.NewClient(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + return RegisterAuthServiceHandler(ctx, mux, conn) +} + +// RegisterAuthServiceHandler registers the http handlers for service AuthService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterAuthServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterAuthServiceHandlerClient(ctx, mux, NewAuthServiceClient(conn)) +} + +// RegisterAuthServiceHandlerClient registers the http handlers for service AuthService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "AuthServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "AuthServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "AuthServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares. +func RegisterAuthServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client AuthServiceClient) error { + mux.Handle(http.MethodGet, pattern_AuthService_GetCurrentSession_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.AuthService/GetCurrentSession", runtime.WithHTTPPathPattern("/api/v1/auth/sessions/current")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_AuthService_GetCurrentSession_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_GetCurrentSession_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_AuthService_CreateSession_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.AuthService/CreateSession", runtime.WithHTTPPathPattern("/api/v1/auth/sessions")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_AuthService_CreateSession_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_CreateSession_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_AuthService_DeleteSession_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.AuthService/DeleteSession", runtime.WithHTTPPathPattern("/api/v1/auth/sessions/current")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_AuthService_DeleteSession_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_AuthService_DeleteSession_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + return nil +} + +var ( + pattern_AuthService_GetCurrentSession_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "auth", "sessions", "current"}, "")) + pattern_AuthService_CreateSession_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1", "auth", "sessions"}, "")) + pattern_AuthService_DeleteSession_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 2, 4}, []string{"api", "v1", "auth", "sessions", "current"}, "")) +) + +var ( + forward_AuthService_GetCurrentSession_0 = runtime.ForwardResponseMessage + forward_AuthService_CreateSession_0 = runtime.ForwardResponseMessage + forward_AuthService_DeleteSession_0 = runtime.ForwardResponseMessage +) diff --git a/proto/gen/api/v1/auth_service_grpc.pb.go b/proto/gen/api/v1/auth_service_grpc.pb.go new file mode 100644 index 0000000..2872b28 --- /dev/null +++ b/proto/gen/api/v1/auth_service_grpc.pb.go @@ -0,0 +1,210 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc (unknown) +// source: api/v1/auth_service.proto + +package apiv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + AuthService_GetCurrentSession_FullMethodName = "/memos.api.v1.AuthService/GetCurrentSession" + AuthService_CreateSession_FullMethodName = "/memos.api.v1.AuthService/CreateSession" + AuthService_DeleteSession_FullMethodName = "/memos.api.v1.AuthService/DeleteSession" +) + +// AuthServiceClient is the client API for AuthService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type AuthServiceClient interface { + // GetCurrentSession returns the current active session information. + // This method is idempotent and safe, suitable for checking current session state. + GetCurrentSession(ctx context.Context, in *GetCurrentSessionRequest, opts ...grpc.CallOption) (*GetCurrentSessionResponse, error) + // CreateSession authenticates a user and creates a new session. + // Returns the authenticated user information upon successful authentication. + CreateSession(ctx context.Context, in *CreateSessionRequest, opts ...grpc.CallOption) (*CreateSessionResponse, error) + // DeleteSession terminates the current user session. + // This is an idempotent operation that invalidates the user's authentication. + DeleteSession(ctx context.Context, in *DeleteSessionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +type authServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewAuthServiceClient(cc grpc.ClientConnInterface) AuthServiceClient { + return &authServiceClient{cc} +} + +func (c *authServiceClient) GetCurrentSession(ctx context.Context, in *GetCurrentSessionRequest, opts ...grpc.CallOption) (*GetCurrentSessionResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetCurrentSessionResponse) + err := c.cc.Invoke(ctx, AuthService_GetCurrentSession_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authServiceClient) CreateSession(ctx context.Context, in *CreateSessionRequest, opts ...grpc.CallOption) (*CreateSessionResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CreateSessionResponse) + err := c.cc.Invoke(ctx, AuthService_CreateSession_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *authServiceClient) DeleteSession(ctx context.Context, in *DeleteSessionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, AuthService_DeleteSession_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// AuthServiceServer is the server API for AuthService service. +// All implementations must embed UnimplementedAuthServiceServer +// for forward compatibility. +type AuthServiceServer interface { + // GetCurrentSession returns the current active session information. + // This method is idempotent and safe, suitable for checking current session state. + GetCurrentSession(context.Context, *GetCurrentSessionRequest) (*GetCurrentSessionResponse, error) + // CreateSession authenticates a user and creates a new session. + // Returns the authenticated user information upon successful authentication. + CreateSession(context.Context, *CreateSessionRequest) (*CreateSessionResponse, error) + // DeleteSession terminates the current user session. + // This is an idempotent operation that invalidates the user's authentication. + DeleteSession(context.Context, *DeleteSessionRequest) (*emptypb.Empty, error) + mustEmbedUnimplementedAuthServiceServer() +} + +// UnimplementedAuthServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedAuthServiceServer struct{} + +func (UnimplementedAuthServiceServer) GetCurrentSession(context.Context, *GetCurrentSessionRequest) (*GetCurrentSessionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetCurrentSession not implemented") +} +func (UnimplementedAuthServiceServer) CreateSession(context.Context, *CreateSessionRequest) (*CreateSessionResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateSession not implemented") +} +func (UnimplementedAuthServiceServer) DeleteSession(context.Context, *DeleteSessionRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteSession not implemented") +} +func (UnimplementedAuthServiceServer) mustEmbedUnimplementedAuthServiceServer() {} +func (UnimplementedAuthServiceServer) testEmbeddedByValue() {} + +// UnsafeAuthServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to AuthServiceServer will +// result in compilation errors. +type UnsafeAuthServiceServer interface { + mustEmbedUnimplementedAuthServiceServer() +} + +func RegisterAuthServiceServer(s grpc.ServiceRegistrar, srv AuthServiceServer) { + // If the following call pancis, it indicates UnimplementedAuthServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&AuthService_ServiceDesc, srv) +} + +func _AuthService_GetCurrentSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetCurrentSessionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServiceServer).GetCurrentSession(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthService_GetCurrentSession_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServiceServer).GetCurrentSession(ctx, req.(*GetCurrentSessionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AuthService_CreateSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateSessionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServiceServer).CreateSession(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthService_CreateSession_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServiceServer).CreateSession(ctx, req.(*CreateSessionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AuthService_DeleteSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteSessionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AuthServiceServer).DeleteSession(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AuthService_DeleteSession_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AuthServiceServer).DeleteSession(ctx, req.(*DeleteSessionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// AuthService_ServiceDesc is the grpc.ServiceDesc for AuthService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var AuthService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "memos.api.v1.AuthService", + HandlerType: (*AuthServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetCurrentSession", + Handler: _AuthService_GetCurrentSession_Handler, + }, + { + MethodName: "CreateSession", + Handler: _AuthService_CreateSession_Handler, + }, + { + MethodName: "DeleteSession", + Handler: _AuthService_DeleteSession_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "api/v1/auth_service.proto", +} diff --git a/proto/gen/api/v1/common.pb.go b/proto/gen/api/v1/common.pb.go new file mode 100644 index 0000000..7df11d3 --- /dev/null +++ b/proto/gen/api/v1/common.pb.go @@ -0,0 +1,244 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: api/v1/common.proto + +package apiv1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type State int32 + +const ( + State_STATE_UNSPECIFIED State = 0 + State_NORMAL State = 1 + State_ARCHIVED State = 2 +) + +// Enum value maps for State. +var ( + State_name = map[int32]string{ + 0: "STATE_UNSPECIFIED", + 1: "NORMAL", + 2: "ARCHIVED", + } + State_value = map[string]int32{ + "STATE_UNSPECIFIED": 0, + "NORMAL": 1, + "ARCHIVED": 2, + } +) + +func (x State) Enum() *State { + p := new(State) + *p = x + return p +} + +func (x State) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (State) Descriptor() protoreflect.EnumDescriptor { + return file_api_v1_common_proto_enumTypes[0].Descriptor() +} + +func (State) Type() protoreflect.EnumType { + return &file_api_v1_common_proto_enumTypes[0] +} + +func (x State) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use State.Descriptor instead. +func (State) EnumDescriptor() ([]byte, []int) { + return file_api_v1_common_proto_rawDescGZIP(), []int{0} +} + +type Direction int32 + +const ( + Direction_DIRECTION_UNSPECIFIED Direction = 0 + Direction_ASC Direction = 1 + Direction_DESC Direction = 2 +) + +// Enum value maps for Direction. +var ( + Direction_name = map[int32]string{ + 0: "DIRECTION_UNSPECIFIED", + 1: "ASC", + 2: "DESC", + } + Direction_value = map[string]int32{ + "DIRECTION_UNSPECIFIED": 0, + "ASC": 1, + "DESC": 2, + } +) + +func (x Direction) Enum() *Direction { + p := new(Direction) + *p = x + return p +} + +func (x Direction) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Direction) Descriptor() protoreflect.EnumDescriptor { + return file_api_v1_common_proto_enumTypes[1].Descriptor() +} + +func (Direction) Type() protoreflect.EnumType { + return &file_api_v1_common_proto_enumTypes[1] +} + +func (x Direction) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Direction.Descriptor instead. +func (Direction) EnumDescriptor() ([]byte, []int) { + return file_api_v1_common_proto_rawDescGZIP(), []int{1} +} + +// Used internally for obfuscating the page token. +type PageToken struct { + state protoimpl.MessageState `protogen:"open.v1"` + Limit int32 `protobuf:"varint,1,opt,name=limit,proto3" json:"limit,omitempty"` + Offset int32 `protobuf:"varint,2,opt,name=offset,proto3" json:"offset,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PageToken) Reset() { + *x = PageToken{} + mi := &file_api_v1_common_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PageToken) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PageToken) ProtoMessage() {} + +func (x *PageToken) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_common_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PageToken.ProtoReflect.Descriptor instead. +func (*PageToken) Descriptor() ([]byte, []int) { + return file_api_v1_common_proto_rawDescGZIP(), []int{0} +} + +func (x *PageToken) GetLimit() int32 { + if x != nil { + return x.Limit + } + return 0 +} + +func (x *PageToken) GetOffset() int32 { + if x != nil { + return x.Offset + } + return 0 +} + +var File_api_v1_common_proto protoreflect.FileDescriptor + +const file_api_v1_common_proto_rawDesc = "" + + "\n" + + "\x13api/v1/common.proto\x12\fmemos.api.v1\"9\n" + + "\tPageToken\x12\x14\n" + + "\x05limit\x18\x01 \x01(\x05R\x05limit\x12\x16\n" + + "\x06offset\x18\x02 \x01(\x05R\x06offset*8\n" + + "\x05State\x12\x15\n" + + "\x11STATE_UNSPECIFIED\x10\x00\x12\n" + + "\n" + + "\x06NORMAL\x10\x01\x12\f\n" + + "\bARCHIVED\x10\x02*9\n" + + "\tDirection\x12\x19\n" + + "\x15DIRECTION_UNSPECIFIED\x10\x00\x12\a\n" + + "\x03ASC\x10\x01\x12\b\n" + + "\x04DESC\x10\x02B\xa3\x01\n" + + "\x10com.memos.api.v1B\vCommonProtoP\x01Z0github.com/usememos/memos/proto/gen/api/v1;apiv1\xa2\x02\x03MAX\xaa\x02\fMemos.Api.V1\xca\x02\fMemos\\Api\\V1\xe2\x02\x18Memos\\Api\\V1\\GPBMetadata\xea\x02\x0eMemos::Api::V1b\x06proto3" + +var ( + file_api_v1_common_proto_rawDescOnce sync.Once + file_api_v1_common_proto_rawDescData []byte +) + +func file_api_v1_common_proto_rawDescGZIP() []byte { + file_api_v1_common_proto_rawDescOnce.Do(func() { + file_api_v1_common_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_v1_common_proto_rawDesc), len(file_api_v1_common_proto_rawDesc))) + }) + return file_api_v1_common_proto_rawDescData +} + +var file_api_v1_common_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_api_v1_common_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_api_v1_common_proto_goTypes = []any{ + (State)(0), // 0: memos.api.v1.State + (Direction)(0), // 1: memos.api.v1.Direction + (*PageToken)(nil), // 2: memos.api.v1.PageToken +} +var file_api_v1_common_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_api_v1_common_proto_init() } +func file_api_v1_common_proto_init() { + if File_api_v1_common_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_common_proto_rawDesc), len(file_api_v1_common_proto_rawDesc)), + NumEnums: 2, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_api_v1_common_proto_goTypes, + DependencyIndexes: file_api_v1_common_proto_depIdxs, + EnumInfos: file_api_v1_common_proto_enumTypes, + MessageInfos: file_api_v1_common_proto_msgTypes, + }.Build() + File_api_v1_common_proto = out.File + file_api_v1_common_proto_goTypes = nil + file_api_v1_common_proto_depIdxs = nil +} diff --git a/proto/gen/api/v1/idp_service.pb.go b/proto/gen/api/v1/idp_service.pb.go new file mode 100644 index 0000000..0b20b93 --- /dev/null +++ b/proto/gen/api/v1/idp_service.pb.go @@ -0,0 +1,805 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: api/v1/idp_service.proto + +package apiv1 + +import ( + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + fieldmaskpb "google.golang.org/protobuf/types/known/fieldmaskpb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type IdentityProvider_Type int32 + +const ( + IdentityProvider_TYPE_UNSPECIFIED IdentityProvider_Type = 0 + // OAuth2 identity provider. + IdentityProvider_OAUTH2 IdentityProvider_Type = 1 +) + +// Enum value maps for IdentityProvider_Type. +var ( + IdentityProvider_Type_name = map[int32]string{ + 0: "TYPE_UNSPECIFIED", + 1: "OAUTH2", + } + IdentityProvider_Type_value = map[string]int32{ + "TYPE_UNSPECIFIED": 0, + "OAUTH2": 1, + } +) + +func (x IdentityProvider_Type) Enum() *IdentityProvider_Type { + p := new(IdentityProvider_Type) + *p = x + return p +} + +func (x IdentityProvider_Type) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (IdentityProvider_Type) Descriptor() protoreflect.EnumDescriptor { + return file_api_v1_idp_service_proto_enumTypes[0].Descriptor() +} + +func (IdentityProvider_Type) Type() protoreflect.EnumType { + return &file_api_v1_idp_service_proto_enumTypes[0] +} + +func (x IdentityProvider_Type) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use IdentityProvider_Type.Descriptor instead. +func (IdentityProvider_Type) EnumDescriptor() ([]byte, []int) { + return file_api_v1_idp_service_proto_rawDescGZIP(), []int{0, 0} +} + +type IdentityProvider struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The resource name of the identity provider. + // Format: identityProviders/{idp} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Required. The type of the identity provider. + Type IdentityProvider_Type `protobuf:"varint,2,opt,name=type,proto3,enum=memos.api.v1.IdentityProvider_Type" json:"type,omitempty"` + // Required. The display title of the identity provider. + Title string `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"` + // Optional. Filter applied to user identifiers. + IdentifierFilter string `protobuf:"bytes,4,opt,name=identifier_filter,json=identifierFilter,proto3" json:"identifier_filter,omitempty"` + // Required. Configuration for the identity provider. + Config *IdentityProviderConfig `protobuf:"bytes,5,opt,name=config,proto3" json:"config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IdentityProvider) Reset() { + *x = IdentityProvider{} + mi := &file_api_v1_idp_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IdentityProvider) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IdentityProvider) ProtoMessage() {} + +func (x *IdentityProvider) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_idp_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IdentityProvider.ProtoReflect.Descriptor instead. +func (*IdentityProvider) Descriptor() ([]byte, []int) { + return file_api_v1_idp_service_proto_rawDescGZIP(), []int{0} +} + +func (x *IdentityProvider) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *IdentityProvider) GetType() IdentityProvider_Type { + if x != nil { + return x.Type + } + return IdentityProvider_TYPE_UNSPECIFIED +} + +func (x *IdentityProvider) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *IdentityProvider) GetIdentifierFilter() string { + if x != nil { + return x.IdentifierFilter + } + return "" +} + +func (x *IdentityProvider) GetConfig() *IdentityProviderConfig { + if x != nil { + return x.Config + } + return nil +} + +type IdentityProviderConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Config: + // + // *IdentityProviderConfig_Oauth2Config + Config isIdentityProviderConfig_Config `protobuf_oneof:"config"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IdentityProviderConfig) Reset() { + *x = IdentityProviderConfig{} + mi := &file_api_v1_idp_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IdentityProviderConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IdentityProviderConfig) ProtoMessage() {} + +func (x *IdentityProviderConfig) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_idp_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IdentityProviderConfig.ProtoReflect.Descriptor instead. +func (*IdentityProviderConfig) Descriptor() ([]byte, []int) { + return file_api_v1_idp_service_proto_rawDescGZIP(), []int{1} +} + +func (x *IdentityProviderConfig) GetConfig() isIdentityProviderConfig_Config { + if x != nil { + return x.Config + } + return nil +} + +func (x *IdentityProviderConfig) GetOauth2Config() *OAuth2Config { + if x != nil { + if x, ok := x.Config.(*IdentityProviderConfig_Oauth2Config); ok { + return x.Oauth2Config + } + } + return nil +} + +type isIdentityProviderConfig_Config interface { + isIdentityProviderConfig_Config() +} + +type IdentityProviderConfig_Oauth2Config struct { + Oauth2Config *OAuth2Config `protobuf:"bytes,1,opt,name=oauth2_config,json=oauth2Config,proto3,oneof"` +} + +func (*IdentityProviderConfig_Oauth2Config) isIdentityProviderConfig_Config() {} + +type FieldMapping struct { + state protoimpl.MessageState `protogen:"open.v1"` + Identifier string `protobuf:"bytes,1,opt,name=identifier,proto3" json:"identifier,omitempty"` + DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"` + AvatarUrl string `protobuf:"bytes,4,opt,name=avatar_url,json=avatarUrl,proto3" json:"avatar_url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FieldMapping) Reset() { + *x = FieldMapping{} + mi := &file_api_v1_idp_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FieldMapping) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FieldMapping) ProtoMessage() {} + +func (x *FieldMapping) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_idp_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FieldMapping.ProtoReflect.Descriptor instead. +func (*FieldMapping) Descriptor() ([]byte, []int) { + return file_api_v1_idp_service_proto_rawDescGZIP(), []int{2} +} + +func (x *FieldMapping) GetIdentifier() string { + if x != nil { + return x.Identifier + } + return "" +} + +func (x *FieldMapping) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + +func (x *FieldMapping) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *FieldMapping) GetAvatarUrl() string { + if x != nil { + return x.AvatarUrl + } + return "" +} + +type OAuth2Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + ClientSecret string `protobuf:"bytes,2,opt,name=client_secret,json=clientSecret,proto3" json:"client_secret,omitempty"` + AuthUrl string `protobuf:"bytes,3,opt,name=auth_url,json=authUrl,proto3" json:"auth_url,omitempty"` + TokenUrl string `protobuf:"bytes,4,opt,name=token_url,json=tokenUrl,proto3" json:"token_url,omitempty"` + UserInfoUrl string `protobuf:"bytes,5,opt,name=user_info_url,json=userInfoUrl,proto3" json:"user_info_url,omitempty"` + Scopes []string `protobuf:"bytes,6,rep,name=scopes,proto3" json:"scopes,omitempty"` + FieldMapping *FieldMapping `protobuf:"bytes,7,opt,name=field_mapping,json=fieldMapping,proto3" json:"field_mapping,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OAuth2Config) Reset() { + *x = OAuth2Config{} + mi := &file_api_v1_idp_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OAuth2Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OAuth2Config) ProtoMessage() {} + +func (x *OAuth2Config) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_idp_service_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OAuth2Config.ProtoReflect.Descriptor instead. +func (*OAuth2Config) Descriptor() ([]byte, []int) { + return file_api_v1_idp_service_proto_rawDescGZIP(), []int{3} +} + +func (x *OAuth2Config) GetClientId() string { + if x != nil { + return x.ClientId + } + return "" +} + +func (x *OAuth2Config) GetClientSecret() string { + if x != nil { + return x.ClientSecret + } + return "" +} + +func (x *OAuth2Config) GetAuthUrl() string { + if x != nil { + return x.AuthUrl + } + return "" +} + +func (x *OAuth2Config) GetTokenUrl() string { + if x != nil { + return x.TokenUrl + } + return "" +} + +func (x *OAuth2Config) GetUserInfoUrl() string { + if x != nil { + return x.UserInfoUrl + } + return "" +} + +func (x *OAuth2Config) GetScopes() []string { + if x != nil { + return x.Scopes + } + return nil +} + +func (x *OAuth2Config) GetFieldMapping() *FieldMapping { + if x != nil { + return x.FieldMapping + } + return nil +} + +type ListIdentityProvidersRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListIdentityProvidersRequest) Reset() { + *x = ListIdentityProvidersRequest{} + mi := &file_api_v1_idp_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListIdentityProvidersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListIdentityProvidersRequest) ProtoMessage() {} + +func (x *ListIdentityProvidersRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_idp_service_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListIdentityProvidersRequest.ProtoReflect.Descriptor instead. +func (*ListIdentityProvidersRequest) Descriptor() ([]byte, []int) { + return file_api_v1_idp_service_proto_rawDescGZIP(), []int{4} +} + +type ListIdentityProvidersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The list of identity providers. + IdentityProviders []*IdentityProvider `protobuf:"bytes,1,rep,name=identity_providers,json=identityProviders,proto3" json:"identity_providers,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListIdentityProvidersResponse) Reset() { + *x = ListIdentityProvidersResponse{} + mi := &file_api_v1_idp_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListIdentityProvidersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListIdentityProvidersResponse) ProtoMessage() {} + +func (x *ListIdentityProvidersResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_idp_service_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListIdentityProvidersResponse.ProtoReflect.Descriptor instead. +func (*ListIdentityProvidersResponse) Descriptor() ([]byte, []int) { + return file_api_v1_idp_service_proto_rawDescGZIP(), []int{5} +} + +func (x *ListIdentityProvidersResponse) GetIdentityProviders() []*IdentityProvider { + if x != nil { + return x.IdentityProviders + } + return nil +} + +type GetIdentityProviderRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The resource name of the identity provider to get. + // Format: identityProviders/{idp} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetIdentityProviderRequest) Reset() { + *x = GetIdentityProviderRequest{} + mi := &file_api_v1_idp_service_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetIdentityProviderRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetIdentityProviderRequest) ProtoMessage() {} + +func (x *GetIdentityProviderRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_idp_service_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetIdentityProviderRequest.ProtoReflect.Descriptor instead. +func (*GetIdentityProviderRequest) Descriptor() ([]byte, []int) { + return file_api_v1_idp_service_proto_rawDescGZIP(), []int{6} +} + +func (x *GetIdentityProviderRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type CreateIdentityProviderRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The identity provider to create. + IdentityProvider *IdentityProvider `protobuf:"bytes,1,opt,name=identity_provider,json=identityProvider,proto3" json:"identity_provider,omitempty"` + // Optional. The ID to use for the identity provider, which will become the final component of the resource name. + // If not provided, the system will generate one. + IdentityProviderId string `protobuf:"bytes,2,opt,name=identity_provider_id,json=identityProviderId,proto3" json:"identity_provider_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateIdentityProviderRequest) Reset() { + *x = CreateIdentityProviderRequest{} + mi := &file_api_v1_idp_service_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateIdentityProviderRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateIdentityProviderRequest) ProtoMessage() {} + +func (x *CreateIdentityProviderRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_idp_service_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateIdentityProviderRequest.ProtoReflect.Descriptor instead. +func (*CreateIdentityProviderRequest) Descriptor() ([]byte, []int) { + return file_api_v1_idp_service_proto_rawDescGZIP(), []int{7} +} + +func (x *CreateIdentityProviderRequest) GetIdentityProvider() *IdentityProvider { + if x != nil { + return x.IdentityProvider + } + return nil +} + +func (x *CreateIdentityProviderRequest) GetIdentityProviderId() string { + if x != nil { + return x.IdentityProviderId + } + return "" +} + +type UpdateIdentityProviderRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The identity provider to update. + IdentityProvider *IdentityProvider `protobuf:"bytes,1,opt,name=identity_provider,json=identityProvider,proto3" json:"identity_provider,omitempty"` + // Required. The update mask applies to the resource. Only the top level fields of + // IdentityProvider are supported. + UpdateMask *fieldmaskpb.FieldMask `protobuf:"bytes,2,opt,name=update_mask,json=updateMask,proto3" json:"update_mask,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateIdentityProviderRequest) Reset() { + *x = UpdateIdentityProviderRequest{} + mi := &file_api_v1_idp_service_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateIdentityProviderRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateIdentityProviderRequest) ProtoMessage() {} + +func (x *UpdateIdentityProviderRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_idp_service_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateIdentityProviderRequest.ProtoReflect.Descriptor instead. +func (*UpdateIdentityProviderRequest) Descriptor() ([]byte, []int) { + return file_api_v1_idp_service_proto_rawDescGZIP(), []int{8} +} + +func (x *UpdateIdentityProviderRequest) GetIdentityProvider() *IdentityProvider { + if x != nil { + return x.IdentityProvider + } + return nil +} + +func (x *UpdateIdentityProviderRequest) GetUpdateMask() *fieldmaskpb.FieldMask { + if x != nil { + return x.UpdateMask + } + return nil +} + +type DeleteIdentityProviderRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The resource name of the identity provider to delete. + // Format: identityProviders/{idp} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteIdentityProviderRequest) Reset() { + *x = DeleteIdentityProviderRequest{} + mi := &file_api_v1_idp_service_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteIdentityProviderRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteIdentityProviderRequest) ProtoMessage() {} + +func (x *DeleteIdentityProviderRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_idp_service_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteIdentityProviderRequest.ProtoReflect.Descriptor instead. +func (*DeleteIdentityProviderRequest) Descriptor() ([]byte, []int) { + return file_api_v1_idp_service_proto_rawDescGZIP(), []int{9} +} + +func (x *DeleteIdentityProviderRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +var File_api_v1_idp_service_proto protoreflect.FileDescriptor + +const file_api_v1_idp_service_proto_rawDesc = "" + + "\n" + + "\x18api/v1/idp_service.proto\x12\fmemos.api.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\"\x8b\x03\n" + + "\x10IdentityProvider\x12\x17\n" + + "\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12<\n" + + "\x04type\x18\x02 \x01(\x0e2#.memos.api.v1.IdentityProvider.TypeB\x03\xe0A\x02R\x04type\x12\x19\n" + + "\x05title\x18\x03 \x01(\tB\x03\xe0A\x02R\x05title\x120\n" + + "\x11identifier_filter\x18\x04 \x01(\tB\x03\xe0A\x01R\x10identifierFilter\x12A\n" + + "\x06config\x18\x05 \x01(\v2$.memos.api.v1.IdentityProviderConfigB\x03\xe0A\x02R\x06config\"(\n" + + "\x04Type\x12\x14\n" + + "\x10TYPE_UNSPECIFIED\x10\x00\x12\n" + + "\n" + + "\x06OAUTH2\x10\x01:f\xeaAc\n" + + "\x1dmemos.api.v1/IdentityProvider\x12\x17identityProviders/{idp}\x1a\x04name*\x11identityProviders2\x10identityProvider\"e\n" + + "\x16IdentityProviderConfig\x12A\n" + + "\roauth2_config\x18\x01 \x01(\v2\x1a.memos.api.v1.OAuth2ConfigH\x00R\foauth2ConfigB\b\n" + + "\x06config\"\x86\x01\n" + + "\fFieldMapping\x12\x1e\n" + + "\n" + + "identifier\x18\x01 \x01(\tR\n" + + "identifier\x12!\n" + + "\fdisplay_name\x18\x02 \x01(\tR\vdisplayName\x12\x14\n" + + "\x05email\x18\x03 \x01(\tR\x05email\x12\x1d\n" + + "\n" + + "avatar_url\x18\x04 \x01(\tR\tavatarUrl\"\x85\x02\n" + + "\fOAuth2Config\x12\x1b\n" + + "\tclient_id\x18\x01 \x01(\tR\bclientId\x12#\n" + + "\rclient_secret\x18\x02 \x01(\tR\fclientSecret\x12\x19\n" + + "\bauth_url\x18\x03 \x01(\tR\aauthUrl\x12\x1b\n" + + "\ttoken_url\x18\x04 \x01(\tR\btokenUrl\x12\"\n" + + "\ruser_info_url\x18\x05 \x01(\tR\vuserInfoUrl\x12\x16\n" + + "\x06scopes\x18\x06 \x03(\tR\x06scopes\x12?\n" + + "\rfield_mapping\x18\a \x01(\v2\x1a.memos.api.v1.FieldMappingR\ffieldMapping\"\x1e\n" + + "\x1cListIdentityProvidersRequest\"n\n" + + "\x1dListIdentityProvidersResponse\x12M\n" + + "\x12identity_providers\x18\x01 \x03(\v2\x1e.memos.api.v1.IdentityProviderR\x11identityProviders\"W\n" + + "\x1aGetIdentityProviderRequest\x129\n" + + "\x04name\x18\x01 \x01(\tB%\xe0A\x02\xfaA\x1f\n" + + "\x1dmemos.api.v1/IdentityProviderR\x04name\"\xa8\x01\n" + + "\x1dCreateIdentityProviderRequest\x12P\n" + + "\x11identity_provider\x18\x01 \x01(\v2\x1e.memos.api.v1.IdentityProviderB\x03\xe0A\x02R\x10identityProvider\x125\n" + + "\x14identity_provider_id\x18\x02 \x01(\tB\x03\xe0A\x01R\x12identityProviderId\"\xb3\x01\n" + + "\x1dUpdateIdentityProviderRequest\x12P\n" + + "\x11identity_provider\x18\x01 \x01(\v2\x1e.memos.api.v1.IdentityProviderB\x03\xe0A\x02R\x10identityProvider\x12@\n" + + "\vupdate_mask\x18\x02 \x01(\v2\x1a.google.protobuf.FieldMaskB\x03\xe0A\x02R\n" + + "updateMask\"Z\n" + + "\x1dDeleteIdentityProviderRequest\x129\n" + + "\x04name\x18\x01 \x01(\tB%\xe0A\x02\xfaA\x1f\n" + + "\x1dmemos.api.v1/IdentityProviderR\x04name2\xe2\x06\n" + + "\x17IdentityProviderService\x12\x93\x01\n" + + "\x15ListIdentityProviders\x12*.memos.api.v1.ListIdentityProvidersRequest\x1a+.memos.api.v1.ListIdentityProvidersResponse\"!\x82\xd3\xe4\x93\x02\x1b\x12\x19/api/v1/identityProviders\x12\x92\x01\n" + + "\x13GetIdentityProvider\x12(.memos.api.v1.GetIdentityProviderRequest\x1a\x1e.memos.api.v1.IdentityProvider\"1\xdaA\x04name\x82\xd3\xe4\x93\x02$\x12\"/api/v1/{name=identityProviders/*}\x12\xaf\x01\n" + + "\x16CreateIdentityProvider\x12+.memos.api.v1.CreateIdentityProviderRequest\x1a\x1e.memos.api.v1.IdentityProvider\"H\xdaA\x11identity_provider\x82\xd3\xe4\x93\x02.:\x11identity_provider\"\x19/api/v1/identityProviders\x12\xd6\x01\n" + + "\x16UpdateIdentityProvider\x12+.memos.api.v1.UpdateIdentityProviderRequest\x1a\x1e.memos.api.v1.IdentityProvider\"o\xdaA\x1didentity_provider,update_mask\x82\xd3\xe4\x93\x02I:\x11identity_provider24/api/v1/{identity_provider.name=identityProviders/*}\x12\x90\x01\n" + + "\x16DeleteIdentityProvider\x12+.memos.api.v1.DeleteIdentityProviderRequest\x1a\x16.google.protobuf.Empty\"1\xdaA\x04name\x82\xd3\xe4\x93\x02$*\"/api/v1/{name=identityProviders/*}B\xa7\x01\n" + + "\x10com.memos.api.v1B\x0fIdpServiceProtoP\x01Z0github.com/usememos/memos/proto/gen/api/v1;apiv1\xa2\x02\x03MAX\xaa\x02\fMemos.Api.V1\xca\x02\fMemos\\Api\\V1\xe2\x02\x18Memos\\Api\\V1\\GPBMetadata\xea\x02\x0eMemos::Api::V1b\x06proto3" + +var ( + file_api_v1_idp_service_proto_rawDescOnce sync.Once + file_api_v1_idp_service_proto_rawDescData []byte +) + +func file_api_v1_idp_service_proto_rawDescGZIP() []byte { + file_api_v1_idp_service_proto_rawDescOnce.Do(func() { + file_api_v1_idp_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_v1_idp_service_proto_rawDesc), len(file_api_v1_idp_service_proto_rawDesc))) + }) + return file_api_v1_idp_service_proto_rawDescData +} + +var file_api_v1_idp_service_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_api_v1_idp_service_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_api_v1_idp_service_proto_goTypes = []any{ + (IdentityProvider_Type)(0), // 0: memos.api.v1.IdentityProvider.Type + (*IdentityProvider)(nil), // 1: memos.api.v1.IdentityProvider + (*IdentityProviderConfig)(nil), // 2: memos.api.v1.IdentityProviderConfig + (*FieldMapping)(nil), // 3: memos.api.v1.FieldMapping + (*OAuth2Config)(nil), // 4: memos.api.v1.OAuth2Config + (*ListIdentityProvidersRequest)(nil), // 5: memos.api.v1.ListIdentityProvidersRequest + (*ListIdentityProvidersResponse)(nil), // 6: memos.api.v1.ListIdentityProvidersResponse + (*GetIdentityProviderRequest)(nil), // 7: memos.api.v1.GetIdentityProviderRequest + (*CreateIdentityProviderRequest)(nil), // 8: memos.api.v1.CreateIdentityProviderRequest + (*UpdateIdentityProviderRequest)(nil), // 9: memos.api.v1.UpdateIdentityProviderRequest + (*DeleteIdentityProviderRequest)(nil), // 10: memos.api.v1.DeleteIdentityProviderRequest + (*fieldmaskpb.FieldMask)(nil), // 11: google.protobuf.FieldMask + (*emptypb.Empty)(nil), // 12: google.protobuf.Empty +} +var file_api_v1_idp_service_proto_depIdxs = []int32{ + 0, // 0: memos.api.v1.IdentityProvider.type:type_name -> memos.api.v1.IdentityProvider.Type + 2, // 1: memos.api.v1.IdentityProvider.config:type_name -> memos.api.v1.IdentityProviderConfig + 4, // 2: memos.api.v1.IdentityProviderConfig.oauth2_config:type_name -> memos.api.v1.OAuth2Config + 3, // 3: memos.api.v1.OAuth2Config.field_mapping:type_name -> memos.api.v1.FieldMapping + 1, // 4: memos.api.v1.ListIdentityProvidersResponse.identity_providers:type_name -> memos.api.v1.IdentityProvider + 1, // 5: memos.api.v1.CreateIdentityProviderRequest.identity_provider:type_name -> memos.api.v1.IdentityProvider + 1, // 6: memos.api.v1.UpdateIdentityProviderRequest.identity_provider:type_name -> memos.api.v1.IdentityProvider + 11, // 7: memos.api.v1.UpdateIdentityProviderRequest.update_mask:type_name -> google.protobuf.FieldMask + 5, // 8: memos.api.v1.IdentityProviderService.ListIdentityProviders:input_type -> memos.api.v1.ListIdentityProvidersRequest + 7, // 9: memos.api.v1.IdentityProviderService.GetIdentityProvider:input_type -> memos.api.v1.GetIdentityProviderRequest + 8, // 10: memos.api.v1.IdentityProviderService.CreateIdentityProvider:input_type -> memos.api.v1.CreateIdentityProviderRequest + 9, // 11: memos.api.v1.IdentityProviderService.UpdateIdentityProvider:input_type -> memos.api.v1.UpdateIdentityProviderRequest + 10, // 12: memos.api.v1.IdentityProviderService.DeleteIdentityProvider:input_type -> memos.api.v1.DeleteIdentityProviderRequest + 6, // 13: memos.api.v1.IdentityProviderService.ListIdentityProviders:output_type -> memos.api.v1.ListIdentityProvidersResponse + 1, // 14: memos.api.v1.IdentityProviderService.GetIdentityProvider:output_type -> memos.api.v1.IdentityProvider + 1, // 15: memos.api.v1.IdentityProviderService.CreateIdentityProvider:output_type -> memos.api.v1.IdentityProvider + 1, // 16: memos.api.v1.IdentityProviderService.UpdateIdentityProvider:output_type -> memos.api.v1.IdentityProvider + 12, // 17: memos.api.v1.IdentityProviderService.DeleteIdentityProvider:output_type -> google.protobuf.Empty + 13, // [13:18] is the sub-list for method output_type + 8, // [8:13] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name +} + +func init() { file_api_v1_idp_service_proto_init() } +func file_api_v1_idp_service_proto_init() { + if File_api_v1_idp_service_proto != nil { + return + } + file_api_v1_idp_service_proto_msgTypes[1].OneofWrappers = []any{ + (*IdentityProviderConfig_Oauth2Config)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_idp_service_proto_rawDesc), len(file_api_v1_idp_service_proto_rawDesc)), + NumEnums: 1, + NumMessages: 10, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_api_v1_idp_service_proto_goTypes, + DependencyIndexes: file_api_v1_idp_service_proto_depIdxs, + EnumInfos: file_api_v1_idp_service_proto_enumTypes, + MessageInfos: file_api_v1_idp_service_proto_msgTypes, + }.Build() + File_api_v1_idp_service_proto = out.File + file_api_v1_idp_service_proto_goTypes = nil + file_api_v1_idp_service_proto_depIdxs = nil +} diff --git a/proto/gen/api/v1/idp_service.pb.gw.go b/proto/gen/api/v1/idp_service.pb.gw.go new file mode 100644 index 0000000..94d74df --- /dev/null +++ b/proto/gen/api/v1/idp_service.pb.gw.go @@ -0,0 +1,507 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: api/v1/idp_service.proto + +/* +Package apiv1 is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package apiv1 + +import ( + "context" + "errors" + "io" + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// Suppress "imported and not used" errors +var ( + _ codes.Code + _ io.Reader + _ status.Status + _ = errors.New + _ = runtime.String + _ = utilities.NewDoubleArray + _ = metadata.Join +) + +func request_IdentityProviderService_ListIdentityProviders_0(ctx context.Context, marshaler runtime.Marshaler, client IdentityProviderServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListIdentityProvidersRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.ListIdentityProviders(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_IdentityProviderService_ListIdentityProviders_0(ctx context.Context, marshaler runtime.Marshaler, server IdentityProviderServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListIdentityProvidersRequest + metadata runtime.ServerMetadata + ) + msg, err := server.ListIdentityProviders(ctx, &protoReq) + return msg, metadata, err +} + +func request_IdentityProviderService_GetIdentityProvider_0(ctx context.Context, marshaler runtime.Marshaler, client IdentityProviderServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetIdentityProviderRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := client.GetIdentityProvider(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_IdentityProviderService_GetIdentityProvider_0(ctx context.Context, marshaler runtime.Marshaler, server IdentityProviderServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetIdentityProviderRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := server.GetIdentityProvider(ctx, &protoReq) + return msg, metadata, err +} + +var filter_IdentityProviderService_CreateIdentityProvider_0 = &utilities.DoubleArray{Encoding: map[string]int{"identity_provider": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} + +func request_IdentityProviderService_CreateIdentityProvider_0(ctx context.Context, marshaler runtime.Marshaler, client IdentityProviderServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateIdentityProviderRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.IdentityProvider); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_IdentityProviderService_CreateIdentityProvider_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.CreateIdentityProvider(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_IdentityProviderService_CreateIdentityProvider_0(ctx context.Context, marshaler runtime.Marshaler, server IdentityProviderServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateIdentityProviderRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.IdentityProvider); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_IdentityProviderService_CreateIdentityProvider_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.CreateIdentityProvider(ctx, &protoReq) + return msg, metadata, err +} + +var filter_IdentityProviderService_UpdateIdentityProvider_0 = &utilities.DoubleArray{Encoding: map[string]int{"identity_provider": 0, "name": 1}, Base: []int{1, 2, 1, 0, 0}, Check: []int{0, 1, 2, 3, 2}} + +func request_IdentityProviderService_UpdateIdentityProvider_0(ctx context.Context, marshaler runtime.Marshaler, client IdentityProviderServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateIdentityProviderRequest + metadata runtime.ServerMetadata + err error + ) + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.IdentityProvider); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 { + if fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.IdentityProvider); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } else { + protoReq.UpdateMask = fieldMask + } + } + val, ok := pathParams["identity_provider.name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "identity_provider.name") + } + err = runtime.PopulateFieldFromPath(&protoReq, "identity_provider.name", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "identity_provider.name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_IdentityProviderService_UpdateIdentityProvider_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.UpdateIdentityProvider(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_IdentityProviderService_UpdateIdentityProvider_0(ctx context.Context, marshaler runtime.Marshaler, server IdentityProviderServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateIdentityProviderRequest + metadata runtime.ServerMetadata + err error + ) + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.IdentityProvider); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 { + if fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.IdentityProvider); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } else { + protoReq.UpdateMask = fieldMask + } + } + val, ok := pathParams["identity_provider.name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "identity_provider.name") + } + err = runtime.PopulateFieldFromPath(&protoReq, "identity_provider.name", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "identity_provider.name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_IdentityProviderService_UpdateIdentityProvider_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.UpdateIdentityProvider(ctx, &protoReq) + return msg, metadata, err +} + +func request_IdentityProviderService_DeleteIdentityProvider_0(ctx context.Context, marshaler runtime.Marshaler, client IdentityProviderServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteIdentityProviderRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := client.DeleteIdentityProvider(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_IdentityProviderService_DeleteIdentityProvider_0(ctx context.Context, marshaler runtime.Marshaler, server IdentityProviderServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteIdentityProviderRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := server.DeleteIdentityProvider(ctx, &protoReq) + return msg, metadata, err +} + +// RegisterIdentityProviderServiceHandlerServer registers the http handlers for service IdentityProviderService to "mux". +// UnaryRPC :call IdentityProviderServiceServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterIdentityProviderServiceHandlerFromEndpoint instead. +// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call. +func RegisterIdentityProviderServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server IdentityProviderServiceServer) error { + mux.Handle(http.MethodGet, pattern_IdentityProviderService_ListIdentityProviders_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.IdentityProviderService/ListIdentityProviders", runtime.WithHTTPPathPattern("/api/v1/identityProviders")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_IdentityProviderService_ListIdentityProviders_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_IdentityProviderService_ListIdentityProviders_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_IdentityProviderService_GetIdentityProvider_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.IdentityProviderService/GetIdentityProvider", runtime.WithHTTPPathPattern("/api/v1/{name=identityProviders/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_IdentityProviderService_GetIdentityProvider_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_IdentityProviderService_GetIdentityProvider_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_IdentityProviderService_CreateIdentityProvider_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.IdentityProviderService/CreateIdentityProvider", runtime.WithHTTPPathPattern("/api/v1/identityProviders")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_IdentityProviderService_CreateIdentityProvider_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_IdentityProviderService_CreateIdentityProvider_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPatch, pattern_IdentityProviderService_UpdateIdentityProvider_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.IdentityProviderService/UpdateIdentityProvider", runtime.WithHTTPPathPattern("/api/v1/{identity_provider.name=identityProviders/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_IdentityProviderService_UpdateIdentityProvider_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_IdentityProviderService_UpdateIdentityProvider_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_IdentityProviderService_DeleteIdentityProvider_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.IdentityProviderService/DeleteIdentityProvider", runtime.WithHTTPPathPattern("/api/v1/{name=identityProviders/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_IdentityProviderService_DeleteIdentityProvider_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_IdentityProviderService_DeleteIdentityProvider_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + + return nil +} + +// RegisterIdentityProviderServiceHandlerFromEndpoint is same as RegisterIdentityProviderServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterIdentityProviderServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.NewClient(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + return RegisterIdentityProviderServiceHandler(ctx, mux, conn) +} + +// RegisterIdentityProviderServiceHandler registers the http handlers for service IdentityProviderService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterIdentityProviderServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterIdentityProviderServiceHandlerClient(ctx, mux, NewIdentityProviderServiceClient(conn)) +} + +// RegisterIdentityProviderServiceHandlerClient registers the http handlers for service IdentityProviderService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "IdentityProviderServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "IdentityProviderServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "IdentityProviderServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares. +func RegisterIdentityProviderServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client IdentityProviderServiceClient) error { + mux.Handle(http.MethodGet, pattern_IdentityProviderService_ListIdentityProviders_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.IdentityProviderService/ListIdentityProviders", runtime.WithHTTPPathPattern("/api/v1/identityProviders")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_IdentityProviderService_ListIdentityProviders_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_IdentityProviderService_ListIdentityProviders_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_IdentityProviderService_GetIdentityProvider_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.IdentityProviderService/GetIdentityProvider", runtime.WithHTTPPathPattern("/api/v1/{name=identityProviders/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_IdentityProviderService_GetIdentityProvider_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_IdentityProviderService_GetIdentityProvider_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_IdentityProviderService_CreateIdentityProvider_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.IdentityProviderService/CreateIdentityProvider", runtime.WithHTTPPathPattern("/api/v1/identityProviders")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_IdentityProviderService_CreateIdentityProvider_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_IdentityProviderService_CreateIdentityProvider_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPatch, pattern_IdentityProviderService_UpdateIdentityProvider_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.IdentityProviderService/UpdateIdentityProvider", runtime.WithHTTPPathPattern("/api/v1/{identity_provider.name=identityProviders/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_IdentityProviderService_UpdateIdentityProvider_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_IdentityProviderService_UpdateIdentityProvider_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_IdentityProviderService_DeleteIdentityProvider_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.IdentityProviderService/DeleteIdentityProvider", runtime.WithHTTPPathPattern("/api/v1/{name=identityProviders/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_IdentityProviderService_DeleteIdentityProvider_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_IdentityProviderService_DeleteIdentityProvider_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + return nil +} + +var ( + pattern_IdentityProviderService_ListIdentityProviders_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "identityProviders"}, "")) + pattern_IdentityProviderService_GetIdentityProvider_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "identityProviders", "name"}, "")) + pattern_IdentityProviderService_CreateIdentityProvider_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "identityProviders"}, "")) + pattern_IdentityProviderService_UpdateIdentityProvider_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "identityProviders", "identity_provider.name"}, "")) + pattern_IdentityProviderService_DeleteIdentityProvider_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "identityProviders", "name"}, "")) +) + +var ( + forward_IdentityProviderService_ListIdentityProviders_0 = runtime.ForwardResponseMessage + forward_IdentityProviderService_GetIdentityProvider_0 = runtime.ForwardResponseMessage + forward_IdentityProviderService_CreateIdentityProvider_0 = runtime.ForwardResponseMessage + forward_IdentityProviderService_UpdateIdentityProvider_0 = runtime.ForwardResponseMessage + forward_IdentityProviderService_DeleteIdentityProvider_0 = runtime.ForwardResponseMessage +) diff --git a/proto/gen/api/v1/idp_service_grpc.pb.go b/proto/gen/api/v1/idp_service_grpc.pb.go new file mode 100644 index 0000000..3a9bee2 --- /dev/null +++ b/proto/gen/api/v1/idp_service_grpc.pb.go @@ -0,0 +1,285 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc (unknown) +// source: api/v1/idp_service.proto + +package apiv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + IdentityProviderService_ListIdentityProviders_FullMethodName = "/memos.api.v1.IdentityProviderService/ListIdentityProviders" + IdentityProviderService_GetIdentityProvider_FullMethodName = "/memos.api.v1.IdentityProviderService/GetIdentityProvider" + IdentityProviderService_CreateIdentityProvider_FullMethodName = "/memos.api.v1.IdentityProviderService/CreateIdentityProvider" + IdentityProviderService_UpdateIdentityProvider_FullMethodName = "/memos.api.v1.IdentityProviderService/UpdateIdentityProvider" + IdentityProviderService_DeleteIdentityProvider_FullMethodName = "/memos.api.v1.IdentityProviderService/DeleteIdentityProvider" +) + +// IdentityProviderServiceClient is the client API for IdentityProviderService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type IdentityProviderServiceClient interface { + // ListIdentityProviders lists identity providers. + ListIdentityProviders(ctx context.Context, in *ListIdentityProvidersRequest, opts ...grpc.CallOption) (*ListIdentityProvidersResponse, error) + // GetIdentityProvider gets an identity provider. + GetIdentityProvider(ctx context.Context, in *GetIdentityProviderRequest, opts ...grpc.CallOption) (*IdentityProvider, error) + // CreateIdentityProvider creates an identity provider. + CreateIdentityProvider(ctx context.Context, in *CreateIdentityProviderRequest, opts ...grpc.CallOption) (*IdentityProvider, error) + // UpdateIdentityProvider updates an identity provider. + UpdateIdentityProvider(ctx context.Context, in *UpdateIdentityProviderRequest, opts ...grpc.CallOption) (*IdentityProvider, error) + // DeleteIdentityProvider deletes an identity provider. + DeleteIdentityProvider(ctx context.Context, in *DeleteIdentityProviderRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +type identityProviderServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewIdentityProviderServiceClient(cc grpc.ClientConnInterface) IdentityProviderServiceClient { + return &identityProviderServiceClient{cc} +} + +func (c *identityProviderServiceClient) ListIdentityProviders(ctx context.Context, in *ListIdentityProvidersRequest, opts ...grpc.CallOption) (*ListIdentityProvidersResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListIdentityProvidersResponse) + err := c.cc.Invoke(ctx, IdentityProviderService_ListIdentityProviders_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *identityProviderServiceClient) GetIdentityProvider(ctx context.Context, in *GetIdentityProviderRequest, opts ...grpc.CallOption) (*IdentityProvider, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(IdentityProvider) + err := c.cc.Invoke(ctx, IdentityProviderService_GetIdentityProvider_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *identityProviderServiceClient) CreateIdentityProvider(ctx context.Context, in *CreateIdentityProviderRequest, opts ...grpc.CallOption) (*IdentityProvider, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(IdentityProvider) + err := c.cc.Invoke(ctx, IdentityProviderService_CreateIdentityProvider_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *identityProviderServiceClient) UpdateIdentityProvider(ctx context.Context, in *UpdateIdentityProviderRequest, opts ...grpc.CallOption) (*IdentityProvider, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(IdentityProvider) + err := c.cc.Invoke(ctx, IdentityProviderService_UpdateIdentityProvider_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *identityProviderServiceClient) DeleteIdentityProvider(ctx context.Context, in *DeleteIdentityProviderRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, IdentityProviderService_DeleteIdentityProvider_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// IdentityProviderServiceServer is the server API for IdentityProviderService service. +// All implementations must embed UnimplementedIdentityProviderServiceServer +// for forward compatibility. +type IdentityProviderServiceServer interface { + // ListIdentityProviders lists identity providers. + ListIdentityProviders(context.Context, *ListIdentityProvidersRequest) (*ListIdentityProvidersResponse, error) + // GetIdentityProvider gets an identity provider. + GetIdentityProvider(context.Context, *GetIdentityProviderRequest) (*IdentityProvider, error) + // CreateIdentityProvider creates an identity provider. + CreateIdentityProvider(context.Context, *CreateIdentityProviderRequest) (*IdentityProvider, error) + // UpdateIdentityProvider updates an identity provider. + UpdateIdentityProvider(context.Context, *UpdateIdentityProviderRequest) (*IdentityProvider, error) + // DeleteIdentityProvider deletes an identity provider. + DeleteIdentityProvider(context.Context, *DeleteIdentityProviderRequest) (*emptypb.Empty, error) + mustEmbedUnimplementedIdentityProviderServiceServer() +} + +// UnimplementedIdentityProviderServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedIdentityProviderServiceServer struct{} + +func (UnimplementedIdentityProviderServiceServer) ListIdentityProviders(context.Context, *ListIdentityProvidersRequest) (*ListIdentityProvidersResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListIdentityProviders not implemented") +} +func (UnimplementedIdentityProviderServiceServer) GetIdentityProvider(context.Context, *GetIdentityProviderRequest) (*IdentityProvider, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetIdentityProvider not implemented") +} +func (UnimplementedIdentityProviderServiceServer) CreateIdentityProvider(context.Context, *CreateIdentityProviderRequest) (*IdentityProvider, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateIdentityProvider not implemented") +} +func (UnimplementedIdentityProviderServiceServer) UpdateIdentityProvider(context.Context, *UpdateIdentityProviderRequest) (*IdentityProvider, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateIdentityProvider not implemented") +} +func (UnimplementedIdentityProviderServiceServer) DeleteIdentityProvider(context.Context, *DeleteIdentityProviderRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteIdentityProvider not implemented") +} +func (UnimplementedIdentityProviderServiceServer) mustEmbedUnimplementedIdentityProviderServiceServer() { +} +func (UnimplementedIdentityProviderServiceServer) testEmbeddedByValue() {} + +// UnsafeIdentityProviderServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to IdentityProviderServiceServer will +// result in compilation errors. +type UnsafeIdentityProviderServiceServer interface { + mustEmbedUnimplementedIdentityProviderServiceServer() +} + +func RegisterIdentityProviderServiceServer(s grpc.ServiceRegistrar, srv IdentityProviderServiceServer) { + // If the following call pancis, it indicates UnimplementedIdentityProviderServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&IdentityProviderService_ServiceDesc, srv) +} + +func _IdentityProviderService_ListIdentityProviders_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListIdentityProvidersRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IdentityProviderServiceServer).ListIdentityProviders(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IdentityProviderService_ListIdentityProviders_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IdentityProviderServiceServer).ListIdentityProviders(ctx, req.(*ListIdentityProvidersRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IdentityProviderService_GetIdentityProvider_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetIdentityProviderRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IdentityProviderServiceServer).GetIdentityProvider(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IdentityProviderService_GetIdentityProvider_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IdentityProviderServiceServer).GetIdentityProvider(ctx, req.(*GetIdentityProviderRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IdentityProviderService_CreateIdentityProvider_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateIdentityProviderRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IdentityProviderServiceServer).CreateIdentityProvider(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IdentityProviderService_CreateIdentityProvider_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IdentityProviderServiceServer).CreateIdentityProvider(ctx, req.(*CreateIdentityProviderRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IdentityProviderService_UpdateIdentityProvider_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateIdentityProviderRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IdentityProviderServiceServer).UpdateIdentityProvider(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IdentityProviderService_UpdateIdentityProvider_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IdentityProviderServiceServer).UpdateIdentityProvider(ctx, req.(*UpdateIdentityProviderRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _IdentityProviderService_DeleteIdentityProvider_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteIdentityProviderRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(IdentityProviderServiceServer).DeleteIdentityProvider(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: IdentityProviderService_DeleteIdentityProvider_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(IdentityProviderServiceServer).DeleteIdentityProvider(ctx, req.(*DeleteIdentityProviderRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// IdentityProviderService_ServiceDesc is the grpc.ServiceDesc for IdentityProviderService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var IdentityProviderService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "memos.api.v1.IdentityProviderService", + HandlerType: (*IdentityProviderServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ListIdentityProviders", + Handler: _IdentityProviderService_ListIdentityProviders_Handler, + }, + { + MethodName: "GetIdentityProvider", + Handler: _IdentityProviderService_GetIdentityProvider_Handler, + }, + { + MethodName: "CreateIdentityProvider", + Handler: _IdentityProviderService_CreateIdentityProvider_Handler, + }, + { + MethodName: "UpdateIdentityProvider", + Handler: _IdentityProviderService_UpdateIdentityProvider_Handler, + }, + { + MethodName: "DeleteIdentityProvider", + Handler: _IdentityProviderService_DeleteIdentityProvider_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "api/v1/idp_service.proto", +} diff --git a/proto/gen/api/v1/inbox_service.pb.go b/proto/gen/api/v1/inbox_service.pb.go new file mode 100644 index 0000000..baff0cb --- /dev/null +++ b/proto/gen/api/v1/inbox_service.pb.go @@ -0,0 +1,622 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: api/v1/inbox_service.proto + +package apiv1 + +import ( + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + fieldmaskpb "google.golang.org/protobuf/types/known/fieldmaskpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// Status enumeration for inbox notifications. +type Inbox_Status int32 + +const ( + // Unspecified status. + Inbox_STATUS_UNSPECIFIED Inbox_Status = 0 + // The notification is unread. + Inbox_UNREAD Inbox_Status = 1 + // The notification is archived. + Inbox_ARCHIVED Inbox_Status = 2 +) + +// Enum value maps for Inbox_Status. +var ( + Inbox_Status_name = map[int32]string{ + 0: "STATUS_UNSPECIFIED", + 1: "UNREAD", + 2: "ARCHIVED", + } + Inbox_Status_value = map[string]int32{ + "STATUS_UNSPECIFIED": 0, + "UNREAD": 1, + "ARCHIVED": 2, + } +) + +func (x Inbox_Status) Enum() *Inbox_Status { + p := new(Inbox_Status) + *p = x + return p +} + +func (x Inbox_Status) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Inbox_Status) Descriptor() protoreflect.EnumDescriptor { + return file_api_v1_inbox_service_proto_enumTypes[0].Descriptor() +} + +func (Inbox_Status) Type() protoreflect.EnumType { + return &file_api_v1_inbox_service_proto_enumTypes[0] +} + +func (x Inbox_Status) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Inbox_Status.Descriptor instead. +func (Inbox_Status) EnumDescriptor() ([]byte, []int) { + return file_api_v1_inbox_service_proto_rawDescGZIP(), []int{0, 0} +} + +// Type enumeration for inbox notifications. +type Inbox_Type int32 + +const ( + // Unspecified type. + Inbox_TYPE_UNSPECIFIED Inbox_Type = 0 + // Memo comment notification. + Inbox_MEMO_COMMENT Inbox_Type = 1 + // Version update notification. + Inbox_VERSION_UPDATE Inbox_Type = 2 +) + +// Enum value maps for Inbox_Type. +var ( + Inbox_Type_name = map[int32]string{ + 0: "TYPE_UNSPECIFIED", + 1: "MEMO_COMMENT", + 2: "VERSION_UPDATE", + } + Inbox_Type_value = map[string]int32{ + "TYPE_UNSPECIFIED": 0, + "MEMO_COMMENT": 1, + "VERSION_UPDATE": 2, + } +) + +func (x Inbox_Type) Enum() *Inbox_Type { + p := new(Inbox_Type) + *p = x + return p +} + +func (x Inbox_Type) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Inbox_Type) Descriptor() protoreflect.EnumDescriptor { + return file_api_v1_inbox_service_proto_enumTypes[1].Descriptor() +} + +func (Inbox_Type) Type() protoreflect.EnumType { + return &file_api_v1_inbox_service_proto_enumTypes[1] +} + +func (x Inbox_Type) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Inbox_Type.Descriptor instead. +func (Inbox_Type) EnumDescriptor() ([]byte, []int) { + return file_api_v1_inbox_service_proto_rawDescGZIP(), []int{0, 1} +} + +type Inbox struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The resource name of the inbox. + // Format: inboxes/{inbox} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // The sender of the inbox notification. + // Format: users/{user} + Sender string `protobuf:"bytes,2,opt,name=sender,proto3" json:"sender,omitempty"` + // The receiver of the inbox notification. + // Format: users/{user} + Receiver string `protobuf:"bytes,3,opt,name=receiver,proto3" json:"receiver,omitempty"` + // The status of the inbox notification. + Status Inbox_Status `protobuf:"varint,4,opt,name=status,proto3,enum=memos.api.v1.Inbox_Status" json:"status,omitempty"` + // Output only. The creation timestamp. + CreateTime *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` + // The type of the inbox notification. + Type Inbox_Type `protobuf:"varint,6,opt,name=type,proto3,enum=memos.api.v1.Inbox_Type" json:"type,omitempty"` + // Optional. The activity ID associated with this inbox notification. + ActivityId *int32 `protobuf:"varint,7,opt,name=activity_id,json=activityId,proto3,oneof" json:"activity_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Inbox) Reset() { + *x = Inbox{} + mi := &file_api_v1_inbox_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Inbox) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Inbox) ProtoMessage() {} + +func (x *Inbox) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_inbox_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Inbox.ProtoReflect.Descriptor instead. +func (*Inbox) Descriptor() ([]byte, []int) { + return file_api_v1_inbox_service_proto_rawDescGZIP(), []int{0} +} + +func (x *Inbox) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Inbox) GetSender() string { + if x != nil { + return x.Sender + } + return "" +} + +func (x *Inbox) GetReceiver() string { + if x != nil { + return x.Receiver + } + return "" +} + +func (x *Inbox) GetStatus() Inbox_Status { + if x != nil { + return x.Status + } + return Inbox_STATUS_UNSPECIFIED +} + +func (x *Inbox) GetCreateTime() *timestamppb.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *Inbox) GetType() Inbox_Type { + if x != nil { + return x.Type + } + return Inbox_TYPE_UNSPECIFIED +} + +func (x *Inbox) GetActivityId() int32 { + if x != nil && x.ActivityId != nil { + return *x.ActivityId + } + return 0 +} + +type ListInboxesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The parent resource whose inboxes will be listed. + // Format: users/{user} + Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"` + // Optional. The maximum number of inboxes to return. + // The service may return fewer than this value. + // If unspecified, at most 50 inboxes will be returned. + // The maximum value is 1000; values above 1000 will be coerced to 1000. + PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Optional. A page token, received from a previous `ListInboxes` call. + // Provide this to retrieve the subsequent page. + PageToken string `protobuf:"bytes,3,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + // Optional. Filter to apply to the list results. + // Example: "status=UNREAD" or "type=MEMO_COMMENT" + // Supported operators: =, != + // Supported fields: status, type, sender, create_time + Filter string `protobuf:"bytes,4,opt,name=filter,proto3" json:"filter,omitempty"` + // Optional. The order to sort results by. + // Example: "create_time desc" or "status asc" + OrderBy string `protobuf:"bytes,5,opt,name=order_by,json=orderBy,proto3" json:"order_by,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListInboxesRequest) Reset() { + *x = ListInboxesRequest{} + mi := &file_api_v1_inbox_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListInboxesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListInboxesRequest) ProtoMessage() {} + +func (x *ListInboxesRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_inbox_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListInboxesRequest.ProtoReflect.Descriptor instead. +func (*ListInboxesRequest) Descriptor() ([]byte, []int) { + return file_api_v1_inbox_service_proto_rawDescGZIP(), []int{1} +} + +func (x *ListInboxesRequest) GetParent() string { + if x != nil { + return x.Parent + } + return "" +} + +func (x *ListInboxesRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListInboxesRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +func (x *ListInboxesRequest) GetFilter() string { + if x != nil { + return x.Filter + } + return "" +} + +func (x *ListInboxesRequest) GetOrderBy() string { + if x != nil { + return x.OrderBy + } + return "" +} + +type ListInboxesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The list of inboxes. + Inboxes []*Inbox `protobuf:"bytes,1,rep,name=inboxes,proto3" json:"inboxes,omitempty"` + // A token that can be sent as `page_token` to retrieve the next page. + // If this field is omitted, there are no subsequent pages. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + // The total count of inboxes (may be approximate). + TotalSize int32 `protobuf:"varint,3,opt,name=total_size,json=totalSize,proto3" json:"total_size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListInboxesResponse) Reset() { + *x = ListInboxesResponse{} + mi := &file_api_v1_inbox_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListInboxesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListInboxesResponse) ProtoMessage() {} + +func (x *ListInboxesResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_inbox_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListInboxesResponse.ProtoReflect.Descriptor instead. +func (*ListInboxesResponse) Descriptor() ([]byte, []int) { + return file_api_v1_inbox_service_proto_rawDescGZIP(), []int{2} +} + +func (x *ListInboxesResponse) GetInboxes() []*Inbox { + if x != nil { + return x.Inboxes + } + return nil +} + +func (x *ListInboxesResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +func (x *ListInboxesResponse) GetTotalSize() int32 { + if x != nil { + return x.TotalSize + } + return 0 +} + +type UpdateInboxRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The inbox to update. + Inbox *Inbox `protobuf:"bytes,1,opt,name=inbox,proto3" json:"inbox,omitempty"` + // Required. The list of fields to update. + UpdateMask *fieldmaskpb.FieldMask `protobuf:"bytes,2,opt,name=update_mask,json=updateMask,proto3" json:"update_mask,omitempty"` + // Optional. If set to true, allows updating missing fields. + AllowMissing bool `protobuf:"varint,3,opt,name=allow_missing,json=allowMissing,proto3" json:"allow_missing,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateInboxRequest) Reset() { + *x = UpdateInboxRequest{} + mi := &file_api_v1_inbox_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateInboxRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateInboxRequest) ProtoMessage() {} + +func (x *UpdateInboxRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_inbox_service_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateInboxRequest.ProtoReflect.Descriptor instead. +func (*UpdateInboxRequest) Descriptor() ([]byte, []int) { + return file_api_v1_inbox_service_proto_rawDescGZIP(), []int{3} +} + +func (x *UpdateInboxRequest) GetInbox() *Inbox { + if x != nil { + return x.Inbox + } + return nil +} + +func (x *UpdateInboxRequest) GetUpdateMask() *fieldmaskpb.FieldMask { + if x != nil { + return x.UpdateMask + } + return nil +} + +func (x *UpdateInboxRequest) GetAllowMissing() bool { + if x != nil { + return x.AllowMissing + } + return false +} + +type DeleteInboxRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The resource name of the inbox to delete. + // Format: inboxes/{inbox} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteInboxRequest) Reset() { + *x = DeleteInboxRequest{} + mi := &file_api_v1_inbox_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteInboxRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteInboxRequest) ProtoMessage() {} + +func (x *DeleteInboxRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_inbox_service_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteInboxRequest.ProtoReflect.Descriptor instead. +func (*DeleteInboxRequest) Descriptor() ([]byte, []int) { + return file_api_v1_inbox_service_proto_rawDescGZIP(), []int{4} +} + +func (x *DeleteInboxRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +var File_api_v1_inbox_service_proto protoreflect.FileDescriptor + +const file_api_v1_inbox_service_proto_rawDesc = "" + + "\n" + + "\x1aapi/v1/inbox_service.proto\x12\fmemos.api.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x87\x04\n" + + "\x05Inbox\x12\x17\n" + + "\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12\x1b\n" + + "\x06sender\x18\x02 \x01(\tB\x03\xe0A\x03R\x06sender\x12\x1f\n" + + "\breceiver\x18\x03 \x01(\tB\x03\xe0A\x03R\breceiver\x127\n" + + "\x06status\x18\x04 \x01(\x0e2\x1a.memos.api.v1.Inbox.StatusB\x03\xe0A\x01R\x06status\x12@\n" + + "\vcreate_time\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\n" + + "createTime\x121\n" + + "\x04type\x18\x06 \x01(\x0e2\x18.memos.api.v1.Inbox.TypeB\x03\xe0A\x03R\x04type\x12)\n" + + "\vactivity_id\x18\a \x01(\x05B\x03\xe0A\x01H\x00R\n" + + "activityId\x88\x01\x01\":\n" + + "\x06Status\x12\x16\n" + + "\x12STATUS_UNSPECIFIED\x10\x00\x12\n" + + "\n" + + "\x06UNREAD\x10\x01\x12\f\n" + + "\bARCHIVED\x10\x02\"B\n" + + "\x04Type\x12\x14\n" + + "\x10TYPE_UNSPECIFIED\x10\x00\x12\x10\n" + + "\fMEMO_COMMENT\x10\x01\x12\x12\n" + + "\x0eVERSION_UPDATE\x10\x02:>\xeaA;\n" + + "\x12memos.api.v1/Inbox\x12\x0finboxes/{inbox}\x1a\x04name*\ainboxes2\x05inboxB\x0e\n" + + "\f_activity_id\"\xca\x01\n" + + "\x12ListInboxesRequest\x121\n" + + "\x06parent\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" + + "\x11memos.api.v1/UserR\x06parent\x12 \n" + + "\tpage_size\x18\x02 \x01(\x05B\x03\xe0A\x01R\bpageSize\x12\"\n" + + "\n" + + "page_token\x18\x03 \x01(\tB\x03\xe0A\x01R\tpageToken\x12\x1b\n" + + "\x06filter\x18\x04 \x01(\tB\x03\xe0A\x01R\x06filter\x12\x1e\n" + + "\border_by\x18\x05 \x01(\tB\x03\xe0A\x01R\aorderBy\"\x8b\x01\n" + + "\x13ListInboxesResponse\x12-\n" + + "\ainboxes\x18\x01 \x03(\v2\x13.memos.api.v1.InboxR\ainboxes\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12\x1d\n" + + "\n" + + "total_size\x18\x03 \x01(\x05R\ttotalSize\"\xb0\x01\n" + + "\x12UpdateInboxRequest\x12.\n" + + "\x05inbox\x18\x01 \x01(\v2\x13.memos.api.v1.InboxB\x03\xe0A\x02R\x05inbox\x12@\n" + + "\vupdate_mask\x18\x02 \x01(\v2\x1a.google.protobuf.FieldMaskB\x03\xe0A\x02R\n" + + "updateMask\x12(\n" + + "\rallow_missing\x18\x03 \x01(\bB\x03\xe0A\x01R\fallowMissing\"D\n" + + "\x12DeleteInboxRequest\x12.\n" + + "\x04name\x18\x01 \x01(\tB\x1a\xe0A\x02\xfaA\x14\n" + + "\x12memos.api.v1/InboxR\x04name2\x92\x03\n" + + "\fInboxService\x12\x85\x01\n" + + "\vListInboxes\x12 .memos.api.v1.ListInboxesRequest\x1a!.memos.api.v1.ListInboxesResponse\"1\xdaA\x06parent\x82\xd3\xe4\x93\x02\"\x12 /api/v1/{parent=users/*}/inboxes\x12\x87\x01\n" + + "\vUpdateInbox\x12 .memos.api.v1.UpdateInboxRequest\x1a\x13.memos.api.v1.Inbox\"A\xdaA\x11inbox,update_mask\x82\xd3\xe4\x93\x02':\x05inbox2\x1e/api/v1/{inbox.name=inboxes/*}\x12p\n" + + "\vDeleteInbox\x12 .memos.api.v1.DeleteInboxRequest\x1a\x16.google.protobuf.Empty\"'\xdaA\x04name\x82\xd3\xe4\x93\x02\x1a*\x18/api/v1/{name=inboxes/*}B\xa9\x01\n" + + "\x10com.memos.api.v1B\x11InboxServiceProtoP\x01Z0github.com/usememos/memos/proto/gen/api/v1;apiv1\xa2\x02\x03MAX\xaa\x02\fMemos.Api.V1\xca\x02\fMemos\\Api\\V1\xe2\x02\x18Memos\\Api\\V1\\GPBMetadata\xea\x02\x0eMemos::Api::V1b\x06proto3" + +var ( + file_api_v1_inbox_service_proto_rawDescOnce sync.Once + file_api_v1_inbox_service_proto_rawDescData []byte +) + +func file_api_v1_inbox_service_proto_rawDescGZIP() []byte { + file_api_v1_inbox_service_proto_rawDescOnce.Do(func() { + file_api_v1_inbox_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_v1_inbox_service_proto_rawDesc), len(file_api_v1_inbox_service_proto_rawDesc))) + }) + return file_api_v1_inbox_service_proto_rawDescData +} + +var file_api_v1_inbox_service_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_api_v1_inbox_service_proto_msgTypes = make([]protoimpl.MessageInfo, 5) +var file_api_v1_inbox_service_proto_goTypes = []any{ + (Inbox_Status)(0), // 0: memos.api.v1.Inbox.Status + (Inbox_Type)(0), // 1: memos.api.v1.Inbox.Type + (*Inbox)(nil), // 2: memos.api.v1.Inbox + (*ListInboxesRequest)(nil), // 3: memos.api.v1.ListInboxesRequest + (*ListInboxesResponse)(nil), // 4: memos.api.v1.ListInboxesResponse + (*UpdateInboxRequest)(nil), // 5: memos.api.v1.UpdateInboxRequest + (*DeleteInboxRequest)(nil), // 6: memos.api.v1.DeleteInboxRequest + (*timestamppb.Timestamp)(nil), // 7: google.protobuf.Timestamp + (*fieldmaskpb.FieldMask)(nil), // 8: google.protobuf.FieldMask + (*emptypb.Empty)(nil), // 9: google.protobuf.Empty +} +var file_api_v1_inbox_service_proto_depIdxs = []int32{ + 0, // 0: memos.api.v1.Inbox.status:type_name -> memos.api.v1.Inbox.Status + 7, // 1: memos.api.v1.Inbox.create_time:type_name -> google.protobuf.Timestamp + 1, // 2: memos.api.v1.Inbox.type:type_name -> memos.api.v1.Inbox.Type + 2, // 3: memos.api.v1.ListInboxesResponse.inboxes:type_name -> memos.api.v1.Inbox + 2, // 4: memos.api.v1.UpdateInboxRequest.inbox:type_name -> memos.api.v1.Inbox + 8, // 5: memos.api.v1.UpdateInboxRequest.update_mask:type_name -> google.protobuf.FieldMask + 3, // 6: memos.api.v1.InboxService.ListInboxes:input_type -> memos.api.v1.ListInboxesRequest + 5, // 7: memos.api.v1.InboxService.UpdateInbox:input_type -> memos.api.v1.UpdateInboxRequest + 6, // 8: memos.api.v1.InboxService.DeleteInbox:input_type -> memos.api.v1.DeleteInboxRequest + 4, // 9: memos.api.v1.InboxService.ListInboxes:output_type -> memos.api.v1.ListInboxesResponse + 2, // 10: memos.api.v1.InboxService.UpdateInbox:output_type -> memos.api.v1.Inbox + 9, // 11: memos.api.v1.InboxService.DeleteInbox:output_type -> google.protobuf.Empty + 9, // [9:12] is the sub-list for method output_type + 6, // [6:9] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_api_v1_inbox_service_proto_init() } +func file_api_v1_inbox_service_proto_init() { + if File_api_v1_inbox_service_proto != nil { + return + } + file_api_v1_inbox_service_proto_msgTypes[0].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_inbox_service_proto_rawDesc), len(file_api_v1_inbox_service_proto_rawDesc)), + NumEnums: 2, + NumMessages: 5, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_api_v1_inbox_service_proto_goTypes, + DependencyIndexes: file_api_v1_inbox_service_proto_depIdxs, + EnumInfos: file_api_v1_inbox_service_proto_enumTypes, + MessageInfos: file_api_v1_inbox_service_proto_msgTypes, + }.Build() + File_api_v1_inbox_service_proto = out.File + file_api_v1_inbox_service_proto_goTypes = nil + file_api_v1_inbox_service_proto_depIdxs = nil +} diff --git a/proto/gen/api/v1/inbox_service.pb.gw.go b/proto/gen/api/v1/inbox_service.pb.gw.go new file mode 100644 index 0000000..99d6f95 --- /dev/null +++ b/proto/gen/api/v1/inbox_service.pb.gw.go @@ -0,0 +1,381 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: api/v1/inbox_service.proto + +/* +Package apiv1 is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package apiv1 + +import ( + "context" + "errors" + "io" + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// Suppress "imported and not used" errors +var ( + _ codes.Code + _ io.Reader + _ status.Status + _ = errors.New + _ = runtime.String + _ = utilities.NewDoubleArray + _ = metadata.Join +) + +var filter_InboxService_ListInboxes_0 = &utilities.DoubleArray{Encoding: map[string]int{"parent": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} + +func request_InboxService_ListInboxes_0(ctx context.Context, marshaler runtime.Marshaler, client InboxServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListInboxesRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_InboxService_ListInboxes_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.ListInboxes(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_InboxService_ListInboxes_0(ctx context.Context, marshaler runtime.Marshaler, server InboxServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListInboxesRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_InboxService_ListInboxes_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ListInboxes(ctx, &protoReq) + return msg, metadata, err +} + +var filter_InboxService_UpdateInbox_0 = &utilities.DoubleArray{Encoding: map[string]int{"inbox": 0, "name": 1}, Base: []int{1, 2, 1, 0, 0}, Check: []int{0, 1, 2, 3, 2}} + +func request_InboxService_UpdateInbox_0(ctx context.Context, marshaler runtime.Marshaler, client InboxServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateInboxRequest + metadata runtime.ServerMetadata + err error + ) + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Inbox); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 { + if fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Inbox); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } else { + protoReq.UpdateMask = fieldMask + } + } + val, ok := pathParams["inbox.name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "inbox.name") + } + err = runtime.PopulateFieldFromPath(&protoReq, "inbox.name", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "inbox.name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_InboxService_UpdateInbox_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.UpdateInbox(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_InboxService_UpdateInbox_0(ctx context.Context, marshaler runtime.Marshaler, server InboxServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateInboxRequest + metadata runtime.ServerMetadata + err error + ) + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Inbox); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 { + if fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Inbox); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } else { + protoReq.UpdateMask = fieldMask + } + } + val, ok := pathParams["inbox.name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "inbox.name") + } + err = runtime.PopulateFieldFromPath(&protoReq, "inbox.name", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "inbox.name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_InboxService_UpdateInbox_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.UpdateInbox(ctx, &protoReq) + return msg, metadata, err +} + +func request_InboxService_DeleteInbox_0(ctx context.Context, marshaler runtime.Marshaler, client InboxServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteInboxRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := client.DeleteInbox(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_InboxService_DeleteInbox_0(ctx context.Context, marshaler runtime.Marshaler, server InboxServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteInboxRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := server.DeleteInbox(ctx, &protoReq) + return msg, metadata, err +} + +// RegisterInboxServiceHandlerServer registers the http handlers for service InboxService to "mux". +// UnaryRPC :call InboxServiceServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterInboxServiceHandlerFromEndpoint instead. +// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call. +func RegisterInboxServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server InboxServiceServer) error { + mux.Handle(http.MethodGet, pattern_InboxService_ListInboxes_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.InboxService/ListInboxes", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/inboxes")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_InboxService_ListInboxes_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_InboxService_ListInboxes_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPatch, pattern_InboxService_UpdateInbox_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.InboxService/UpdateInbox", runtime.WithHTTPPathPattern("/api/v1/{inbox.name=inboxes/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_InboxService_UpdateInbox_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_InboxService_UpdateInbox_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_InboxService_DeleteInbox_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.InboxService/DeleteInbox", runtime.WithHTTPPathPattern("/api/v1/{name=inboxes/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_InboxService_DeleteInbox_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_InboxService_DeleteInbox_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + + return nil +} + +// RegisterInboxServiceHandlerFromEndpoint is same as RegisterInboxServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterInboxServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.NewClient(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + return RegisterInboxServiceHandler(ctx, mux, conn) +} + +// RegisterInboxServiceHandler registers the http handlers for service InboxService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterInboxServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterInboxServiceHandlerClient(ctx, mux, NewInboxServiceClient(conn)) +} + +// RegisterInboxServiceHandlerClient registers the http handlers for service InboxService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "InboxServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "InboxServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "InboxServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares. +func RegisterInboxServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client InboxServiceClient) error { + mux.Handle(http.MethodGet, pattern_InboxService_ListInboxes_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.InboxService/ListInboxes", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/inboxes")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_InboxService_ListInboxes_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_InboxService_ListInboxes_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPatch, pattern_InboxService_UpdateInbox_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.InboxService/UpdateInbox", runtime.WithHTTPPathPattern("/api/v1/{inbox.name=inboxes/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_InboxService_UpdateInbox_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_InboxService_UpdateInbox_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_InboxService_DeleteInbox_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.InboxService/DeleteInbox", runtime.WithHTTPPathPattern("/api/v1/{name=inboxes/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_InboxService_DeleteInbox_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_InboxService_DeleteInbox_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + return nil +} + +var ( + pattern_InboxService_ListInboxes_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "inboxes"}, "")) + pattern_InboxService_UpdateInbox_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "inboxes", "inbox.name"}, "")) + pattern_InboxService_DeleteInbox_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "inboxes", "name"}, "")) +) + +var ( + forward_InboxService_ListInboxes_0 = runtime.ForwardResponseMessage + forward_InboxService_UpdateInbox_0 = runtime.ForwardResponseMessage + forward_InboxService_DeleteInbox_0 = runtime.ForwardResponseMessage +) diff --git a/proto/gen/api/v1/inbox_service_grpc.pb.go b/proto/gen/api/v1/inbox_service_grpc.pb.go new file mode 100644 index 0000000..d74a262 --- /dev/null +++ b/proto/gen/api/v1/inbox_service_grpc.pb.go @@ -0,0 +1,204 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc (unknown) +// source: api/v1/inbox_service.proto + +package apiv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + InboxService_ListInboxes_FullMethodName = "/memos.api.v1.InboxService/ListInboxes" + InboxService_UpdateInbox_FullMethodName = "/memos.api.v1.InboxService/UpdateInbox" + InboxService_DeleteInbox_FullMethodName = "/memos.api.v1.InboxService/DeleteInbox" +) + +// InboxServiceClient is the client API for InboxService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type InboxServiceClient interface { + // ListInboxes lists inboxes for a user. + ListInboxes(ctx context.Context, in *ListInboxesRequest, opts ...grpc.CallOption) (*ListInboxesResponse, error) + // UpdateInbox updates an inbox. + UpdateInbox(ctx context.Context, in *UpdateInboxRequest, opts ...grpc.CallOption) (*Inbox, error) + // DeleteInbox deletes an inbox. + DeleteInbox(ctx context.Context, in *DeleteInboxRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +type inboxServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewInboxServiceClient(cc grpc.ClientConnInterface) InboxServiceClient { + return &inboxServiceClient{cc} +} + +func (c *inboxServiceClient) ListInboxes(ctx context.Context, in *ListInboxesRequest, opts ...grpc.CallOption) (*ListInboxesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListInboxesResponse) + err := c.cc.Invoke(ctx, InboxService_ListInboxes_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *inboxServiceClient) UpdateInbox(ctx context.Context, in *UpdateInboxRequest, opts ...grpc.CallOption) (*Inbox, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Inbox) + err := c.cc.Invoke(ctx, InboxService_UpdateInbox_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *inboxServiceClient) DeleteInbox(ctx context.Context, in *DeleteInboxRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, InboxService_DeleteInbox_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// InboxServiceServer is the server API for InboxService service. +// All implementations must embed UnimplementedInboxServiceServer +// for forward compatibility. +type InboxServiceServer interface { + // ListInboxes lists inboxes for a user. + ListInboxes(context.Context, *ListInboxesRequest) (*ListInboxesResponse, error) + // UpdateInbox updates an inbox. + UpdateInbox(context.Context, *UpdateInboxRequest) (*Inbox, error) + // DeleteInbox deletes an inbox. + DeleteInbox(context.Context, *DeleteInboxRequest) (*emptypb.Empty, error) + mustEmbedUnimplementedInboxServiceServer() +} + +// UnimplementedInboxServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedInboxServiceServer struct{} + +func (UnimplementedInboxServiceServer) ListInboxes(context.Context, *ListInboxesRequest) (*ListInboxesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListInboxes not implemented") +} +func (UnimplementedInboxServiceServer) UpdateInbox(context.Context, *UpdateInboxRequest) (*Inbox, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateInbox not implemented") +} +func (UnimplementedInboxServiceServer) DeleteInbox(context.Context, *DeleteInboxRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteInbox not implemented") +} +func (UnimplementedInboxServiceServer) mustEmbedUnimplementedInboxServiceServer() {} +func (UnimplementedInboxServiceServer) testEmbeddedByValue() {} + +// UnsafeInboxServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to InboxServiceServer will +// result in compilation errors. +type UnsafeInboxServiceServer interface { + mustEmbedUnimplementedInboxServiceServer() +} + +func RegisterInboxServiceServer(s grpc.ServiceRegistrar, srv InboxServiceServer) { + // If the following call pancis, it indicates UnimplementedInboxServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&InboxService_ServiceDesc, srv) +} + +func _InboxService_ListInboxes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListInboxesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(InboxServiceServer).ListInboxes(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: InboxService_ListInboxes_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(InboxServiceServer).ListInboxes(ctx, req.(*ListInboxesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _InboxService_UpdateInbox_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateInboxRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(InboxServiceServer).UpdateInbox(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: InboxService_UpdateInbox_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(InboxServiceServer).UpdateInbox(ctx, req.(*UpdateInboxRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _InboxService_DeleteInbox_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteInboxRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(InboxServiceServer).DeleteInbox(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: InboxService_DeleteInbox_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(InboxServiceServer).DeleteInbox(ctx, req.(*DeleteInboxRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// InboxService_ServiceDesc is the grpc.ServiceDesc for InboxService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var InboxService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "memos.api.v1.InboxService", + HandlerType: (*InboxServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ListInboxes", + Handler: _InboxService_ListInboxes_Handler, + }, + { + MethodName: "UpdateInbox", + Handler: _InboxService_UpdateInbox_Handler, + }, + { + MethodName: "DeleteInbox", + Handler: _InboxService_DeleteInbox_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "api/v1/inbox_service.proto", +} diff --git a/proto/gen/api/v1/markdown_service.pb.go b/proto/gen/api/v1/markdown_service.pb.go new file mode 100644 index 0000000..f1a8a14 --- /dev/null +++ b/proto/gen/api/v1/markdown_service.pb.go @@ -0,0 +1,3114 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: api/v1/markdown_service.proto + +package apiv1 + +import ( + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type NodeType int32 + +const ( + NodeType_NODE_UNSPECIFIED NodeType = 0 + // Block nodes. + NodeType_LINE_BREAK NodeType = 1 + NodeType_PARAGRAPH NodeType = 2 + NodeType_CODE_BLOCK NodeType = 3 + NodeType_HEADING NodeType = 4 + NodeType_HORIZONTAL_RULE NodeType = 5 + NodeType_BLOCKQUOTE NodeType = 6 + NodeType_LIST NodeType = 7 + NodeType_ORDERED_LIST_ITEM NodeType = 8 + NodeType_UNORDERED_LIST_ITEM NodeType = 9 + NodeType_TASK_LIST_ITEM NodeType = 10 + NodeType_MATH_BLOCK NodeType = 11 + NodeType_TABLE NodeType = 12 + NodeType_EMBEDDED_CONTENT NodeType = 13 + // Inline nodes. + NodeType_TEXT NodeType = 51 + NodeType_BOLD NodeType = 52 + NodeType_ITALIC NodeType = 53 + NodeType_BOLD_ITALIC NodeType = 54 + NodeType_CODE NodeType = 55 + NodeType_IMAGE NodeType = 56 + NodeType_LINK NodeType = 57 + NodeType_AUTO_LINK NodeType = 58 + NodeType_TAG NodeType = 59 + NodeType_STRIKETHROUGH NodeType = 60 + NodeType_ESCAPING_CHARACTER NodeType = 61 + NodeType_MATH NodeType = 62 + NodeType_HIGHLIGHT NodeType = 63 + NodeType_SUBSCRIPT NodeType = 64 + NodeType_SUPERSCRIPT NodeType = 65 + NodeType_REFERENCED_CONTENT NodeType = 66 + NodeType_SPOILER NodeType = 67 + NodeType_HTML_ELEMENT NodeType = 68 +) + +// Enum value maps for NodeType. +var ( + NodeType_name = map[int32]string{ + 0: "NODE_UNSPECIFIED", + 1: "LINE_BREAK", + 2: "PARAGRAPH", + 3: "CODE_BLOCK", + 4: "HEADING", + 5: "HORIZONTAL_RULE", + 6: "BLOCKQUOTE", + 7: "LIST", + 8: "ORDERED_LIST_ITEM", + 9: "UNORDERED_LIST_ITEM", + 10: "TASK_LIST_ITEM", + 11: "MATH_BLOCK", + 12: "TABLE", + 13: "EMBEDDED_CONTENT", + 51: "TEXT", + 52: "BOLD", + 53: "ITALIC", + 54: "BOLD_ITALIC", + 55: "CODE", + 56: "IMAGE", + 57: "LINK", + 58: "AUTO_LINK", + 59: "TAG", + 60: "STRIKETHROUGH", + 61: "ESCAPING_CHARACTER", + 62: "MATH", + 63: "HIGHLIGHT", + 64: "SUBSCRIPT", + 65: "SUPERSCRIPT", + 66: "REFERENCED_CONTENT", + 67: "SPOILER", + 68: "HTML_ELEMENT", + } + NodeType_value = map[string]int32{ + "NODE_UNSPECIFIED": 0, + "LINE_BREAK": 1, + "PARAGRAPH": 2, + "CODE_BLOCK": 3, + "HEADING": 4, + "HORIZONTAL_RULE": 5, + "BLOCKQUOTE": 6, + "LIST": 7, + "ORDERED_LIST_ITEM": 8, + "UNORDERED_LIST_ITEM": 9, + "TASK_LIST_ITEM": 10, + "MATH_BLOCK": 11, + "TABLE": 12, + "EMBEDDED_CONTENT": 13, + "TEXT": 51, + "BOLD": 52, + "ITALIC": 53, + "BOLD_ITALIC": 54, + "CODE": 55, + "IMAGE": 56, + "LINK": 57, + "AUTO_LINK": 58, + "TAG": 59, + "STRIKETHROUGH": 60, + "ESCAPING_CHARACTER": 61, + "MATH": 62, + "HIGHLIGHT": 63, + "SUBSCRIPT": 64, + "SUPERSCRIPT": 65, + "REFERENCED_CONTENT": 66, + "SPOILER": 67, + "HTML_ELEMENT": 68, + } +) + +func (x NodeType) Enum() *NodeType { + p := new(NodeType) + *p = x + return p +} + +func (x NodeType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (NodeType) Descriptor() protoreflect.EnumDescriptor { + return file_api_v1_markdown_service_proto_enumTypes[0].Descriptor() +} + +func (NodeType) Type() protoreflect.EnumType { + return &file_api_v1_markdown_service_proto_enumTypes[0] +} + +func (x NodeType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use NodeType.Descriptor instead. +func (NodeType) EnumDescriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{0} +} + +type ListNode_Kind int32 + +const ( + ListNode_KIND_UNSPECIFIED ListNode_Kind = 0 + ListNode_ORDERED ListNode_Kind = 1 + ListNode_UNORDERED ListNode_Kind = 2 + ListNode_DESCRIPTION ListNode_Kind = 3 +) + +// Enum value maps for ListNode_Kind. +var ( + ListNode_Kind_name = map[int32]string{ + 0: "KIND_UNSPECIFIED", + 1: "ORDERED", + 2: "UNORDERED", + 3: "DESCRIPTION", + } + ListNode_Kind_value = map[string]int32{ + "KIND_UNSPECIFIED": 0, + "ORDERED": 1, + "UNORDERED": 2, + "DESCRIPTION": 3, + } +) + +func (x ListNode_Kind) Enum() *ListNode_Kind { + p := new(ListNode_Kind) + *p = x + return p +} + +func (x ListNode_Kind) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (ListNode_Kind) Descriptor() protoreflect.EnumDescriptor { + return file_api_v1_markdown_service_proto_enumTypes[1].Descriptor() +} + +func (ListNode_Kind) Type() protoreflect.EnumType { + return &file_api_v1_markdown_service_proto_enumTypes[1] +} + +func (x ListNode_Kind) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use ListNode_Kind.Descriptor instead. +func (ListNode_Kind) EnumDescriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{15, 0} +} + +type ParseMarkdownRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The markdown content to parse. + Markdown string `protobuf:"bytes,1,opt,name=markdown,proto3" json:"markdown,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ParseMarkdownRequest) Reset() { + *x = ParseMarkdownRequest{} + mi := &file_api_v1_markdown_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ParseMarkdownRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ParseMarkdownRequest) ProtoMessage() {} + +func (x *ParseMarkdownRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ParseMarkdownRequest.ProtoReflect.Descriptor instead. +func (*ParseMarkdownRequest) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{0} +} + +func (x *ParseMarkdownRequest) GetMarkdown() string { + if x != nil { + return x.Markdown + } + return "" +} + +type ParseMarkdownResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The parsed markdown nodes. + Nodes []*Node `protobuf:"bytes,1,rep,name=nodes,proto3" json:"nodes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ParseMarkdownResponse) Reset() { + *x = ParseMarkdownResponse{} + mi := &file_api_v1_markdown_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ParseMarkdownResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ParseMarkdownResponse) ProtoMessage() {} + +func (x *ParseMarkdownResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ParseMarkdownResponse.ProtoReflect.Descriptor instead. +func (*ParseMarkdownResponse) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{1} +} + +func (x *ParseMarkdownResponse) GetNodes() []*Node { + if x != nil { + return x.Nodes + } + return nil +} + +type RestoreMarkdownNodesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The nodes to restore to markdown content. + Nodes []*Node `protobuf:"bytes,1,rep,name=nodes,proto3" json:"nodes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RestoreMarkdownNodesRequest) Reset() { + *x = RestoreMarkdownNodesRequest{} + mi := &file_api_v1_markdown_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RestoreMarkdownNodesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RestoreMarkdownNodesRequest) ProtoMessage() {} + +func (x *RestoreMarkdownNodesRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RestoreMarkdownNodesRequest.ProtoReflect.Descriptor instead. +func (*RestoreMarkdownNodesRequest) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{2} +} + +func (x *RestoreMarkdownNodesRequest) GetNodes() []*Node { + if x != nil { + return x.Nodes + } + return nil +} + +type RestoreMarkdownNodesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The restored markdown content. + Markdown string `protobuf:"bytes,1,opt,name=markdown,proto3" json:"markdown,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RestoreMarkdownNodesResponse) Reset() { + *x = RestoreMarkdownNodesResponse{} + mi := &file_api_v1_markdown_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RestoreMarkdownNodesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RestoreMarkdownNodesResponse) ProtoMessage() {} + +func (x *RestoreMarkdownNodesResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RestoreMarkdownNodesResponse.ProtoReflect.Descriptor instead. +func (*RestoreMarkdownNodesResponse) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{3} +} + +func (x *RestoreMarkdownNodesResponse) GetMarkdown() string { + if x != nil { + return x.Markdown + } + return "" +} + +type StringifyMarkdownNodesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The nodes to stringify to plain text. + Nodes []*Node `protobuf:"bytes,1,rep,name=nodes,proto3" json:"nodes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StringifyMarkdownNodesRequest) Reset() { + *x = StringifyMarkdownNodesRequest{} + mi := &file_api_v1_markdown_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StringifyMarkdownNodesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StringifyMarkdownNodesRequest) ProtoMessage() {} + +func (x *StringifyMarkdownNodesRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StringifyMarkdownNodesRequest.ProtoReflect.Descriptor instead. +func (*StringifyMarkdownNodesRequest) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{4} +} + +func (x *StringifyMarkdownNodesRequest) GetNodes() []*Node { + if x != nil { + return x.Nodes + } + return nil +} + +type StringifyMarkdownNodesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The plain text content. + PlainText string `protobuf:"bytes,1,opt,name=plain_text,json=plainText,proto3" json:"plain_text,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StringifyMarkdownNodesResponse) Reset() { + *x = StringifyMarkdownNodesResponse{} + mi := &file_api_v1_markdown_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StringifyMarkdownNodesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StringifyMarkdownNodesResponse) ProtoMessage() {} + +func (x *StringifyMarkdownNodesResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StringifyMarkdownNodesResponse.ProtoReflect.Descriptor instead. +func (*StringifyMarkdownNodesResponse) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{5} +} + +func (x *StringifyMarkdownNodesResponse) GetPlainText() string { + if x != nil { + return x.PlainText + } + return "" +} + +type GetLinkMetadataRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The link URL to get metadata for. + Link string `protobuf:"bytes,1,opt,name=link,proto3" json:"link,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetLinkMetadataRequest) Reset() { + *x = GetLinkMetadataRequest{} + mi := &file_api_v1_markdown_service_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetLinkMetadataRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetLinkMetadataRequest) ProtoMessage() {} + +func (x *GetLinkMetadataRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetLinkMetadataRequest.ProtoReflect.Descriptor instead. +func (*GetLinkMetadataRequest) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{6} +} + +func (x *GetLinkMetadataRequest) GetLink() string { + if x != nil { + return x.Link + } + return "" +} + +type LinkMetadata struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The title of the linked page. + Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"` + // The description of the linked page. + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + // The URL of the preview image for the linked page. + Image string `protobuf:"bytes,3,opt,name=image,proto3" json:"image,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LinkMetadata) Reset() { + *x = LinkMetadata{} + mi := &file_api_v1_markdown_service_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LinkMetadata) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LinkMetadata) ProtoMessage() {} + +func (x *LinkMetadata) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LinkMetadata.ProtoReflect.Descriptor instead. +func (*LinkMetadata) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{7} +} + +func (x *LinkMetadata) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *LinkMetadata) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *LinkMetadata) GetImage() string { + if x != nil { + return x.Image + } + return "" +} + +type Node struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type NodeType `protobuf:"varint,1,opt,name=type,proto3,enum=memos.api.v1.NodeType" json:"type,omitempty"` + // Types that are valid to be assigned to Node: + // + // *Node_LineBreakNode + // *Node_ParagraphNode + // *Node_CodeBlockNode + // *Node_HeadingNode + // *Node_HorizontalRuleNode + // *Node_BlockquoteNode + // *Node_ListNode + // *Node_OrderedListItemNode + // *Node_UnorderedListItemNode + // *Node_TaskListItemNode + // *Node_MathBlockNode + // *Node_TableNode + // *Node_EmbeddedContentNode + // *Node_TextNode + // *Node_BoldNode + // *Node_ItalicNode + // *Node_BoldItalicNode + // *Node_CodeNode + // *Node_ImageNode + // *Node_LinkNode + // *Node_AutoLinkNode + // *Node_TagNode + // *Node_StrikethroughNode + // *Node_EscapingCharacterNode + // *Node_MathNode + // *Node_HighlightNode + // *Node_SubscriptNode + // *Node_SuperscriptNode + // *Node_ReferencedContentNode + // *Node_SpoilerNode + // *Node_HtmlElementNode + Node isNode_Node `protobuf_oneof:"node"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Node) Reset() { + *x = Node{} + mi := &file_api_v1_markdown_service_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Node) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Node) ProtoMessage() {} + +func (x *Node) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Node.ProtoReflect.Descriptor instead. +func (*Node) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{8} +} + +func (x *Node) GetType() NodeType { + if x != nil { + return x.Type + } + return NodeType_NODE_UNSPECIFIED +} + +func (x *Node) GetNode() isNode_Node { + if x != nil { + return x.Node + } + return nil +} + +func (x *Node) GetLineBreakNode() *LineBreakNode { + if x != nil { + if x, ok := x.Node.(*Node_LineBreakNode); ok { + return x.LineBreakNode + } + } + return nil +} + +func (x *Node) GetParagraphNode() *ParagraphNode { + if x != nil { + if x, ok := x.Node.(*Node_ParagraphNode); ok { + return x.ParagraphNode + } + } + return nil +} + +func (x *Node) GetCodeBlockNode() *CodeBlockNode { + if x != nil { + if x, ok := x.Node.(*Node_CodeBlockNode); ok { + return x.CodeBlockNode + } + } + return nil +} + +func (x *Node) GetHeadingNode() *HeadingNode { + if x != nil { + if x, ok := x.Node.(*Node_HeadingNode); ok { + return x.HeadingNode + } + } + return nil +} + +func (x *Node) GetHorizontalRuleNode() *HorizontalRuleNode { + if x != nil { + if x, ok := x.Node.(*Node_HorizontalRuleNode); ok { + return x.HorizontalRuleNode + } + } + return nil +} + +func (x *Node) GetBlockquoteNode() *BlockquoteNode { + if x != nil { + if x, ok := x.Node.(*Node_BlockquoteNode); ok { + return x.BlockquoteNode + } + } + return nil +} + +func (x *Node) GetListNode() *ListNode { + if x != nil { + if x, ok := x.Node.(*Node_ListNode); ok { + return x.ListNode + } + } + return nil +} + +func (x *Node) GetOrderedListItemNode() *OrderedListItemNode { + if x != nil { + if x, ok := x.Node.(*Node_OrderedListItemNode); ok { + return x.OrderedListItemNode + } + } + return nil +} + +func (x *Node) GetUnorderedListItemNode() *UnorderedListItemNode { + if x != nil { + if x, ok := x.Node.(*Node_UnorderedListItemNode); ok { + return x.UnorderedListItemNode + } + } + return nil +} + +func (x *Node) GetTaskListItemNode() *TaskListItemNode { + if x != nil { + if x, ok := x.Node.(*Node_TaskListItemNode); ok { + return x.TaskListItemNode + } + } + return nil +} + +func (x *Node) GetMathBlockNode() *MathBlockNode { + if x != nil { + if x, ok := x.Node.(*Node_MathBlockNode); ok { + return x.MathBlockNode + } + } + return nil +} + +func (x *Node) GetTableNode() *TableNode { + if x != nil { + if x, ok := x.Node.(*Node_TableNode); ok { + return x.TableNode + } + } + return nil +} + +func (x *Node) GetEmbeddedContentNode() *EmbeddedContentNode { + if x != nil { + if x, ok := x.Node.(*Node_EmbeddedContentNode); ok { + return x.EmbeddedContentNode + } + } + return nil +} + +func (x *Node) GetTextNode() *TextNode { + if x != nil { + if x, ok := x.Node.(*Node_TextNode); ok { + return x.TextNode + } + } + return nil +} + +func (x *Node) GetBoldNode() *BoldNode { + if x != nil { + if x, ok := x.Node.(*Node_BoldNode); ok { + return x.BoldNode + } + } + return nil +} + +func (x *Node) GetItalicNode() *ItalicNode { + if x != nil { + if x, ok := x.Node.(*Node_ItalicNode); ok { + return x.ItalicNode + } + } + return nil +} + +func (x *Node) GetBoldItalicNode() *BoldItalicNode { + if x != nil { + if x, ok := x.Node.(*Node_BoldItalicNode); ok { + return x.BoldItalicNode + } + } + return nil +} + +func (x *Node) GetCodeNode() *CodeNode { + if x != nil { + if x, ok := x.Node.(*Node_CodeNode); ok { + return x.CodeNode + } + } + return nil +} + +func (x *Node) GetImageNode() *ImageNode { + if x != nil { + if x, ok := x.Node.(*Node_ImageNode); ok { + return x.ImageNode + } + } + return nil +} + +func (x *Node) GetLinkNode() *LinkNode { + if x != nil { + if x, ok := x.Node.(*Node_LinkNode); ok { + return x.LinkNode + } + } + return nil +} + +func (x *Node) GetAutoLinkNode() *AutoLinkNode { + if x != nil { + if x, ok := x.Node.(*Node_AutoLinkNode); ok { + return x.AutoLinkNode + } + } + return nil +} + +func (x *Node) GetTagNode() *TagNode { + if x != nil { + if x, ok := x.Node.(*Node_TagNode); ok { + return x.TagNode + } + } + return nil +} + +func (x *Node) GetStrikethroughNode() *StrikethroughNode { + if x != nil { + if x, ok := x.Node.(*Node_StrikethroughNode); ok { + return x.StrikethroughNode + } + } + return nil +} + +func (x *Node) GetEscapingCharacterNode() *EscapingCharacterNode { + if x != nil { + if x, ok := x.Node.(*Node_EscapingCharacterNode); ok { + return x.EscapingCharacterNode + } + } + return nil +} + +func (x *Node) GetMathNode() *MathNode { + if x != nil { + if x, ok := x.Node.(*Node_MathNode); ok { + return x.MathNode + } + } + return nil +} + +func (x *Node) GetHighlightNode() *HighlightNode { + if x != nil { + if x, ok := x.Node.(*Node_HighlightNode); ok { + return x.HighlightNode + } + } + return nil +} + +func (x *Node) GetSubscriptNode() *SubscriptNode { + if x != nil { + if x, ok := x.Node.(*Node_SubscriptNode); ok { + return x.SubscriptNode + } + } + return nil +} + +func (x *Node) GetSuperscriptNode() *SuperscriptNode { + if x != nil { + if x, ok := x.Node.(*Node_SuperscriptNode); ok { + return x.SuperscriptNode + } + } + return nil +} + +func (x *Node) GetReferencedContentNode() *ReferencedContentNode { + if x != nil { + if x, ok := x.Node.(*Node_ReferencedContentNode); ok { + return x.ReferencedContentNode + } + } + return nil +} + +func (x *Node) GetSpoilerNode() *SpoilerNode { + if x != nil { + if x, ok := x.Node.(*Node_SpoilerNode); ok { + return x.SpoilerNode + } + } + return nil +} + +func (x *Node) GetHtmlElementNode() *HTMLElementNode { + if x != nil { + if x, ok := x.Node.(*Node_HtmlElementNode); ok { + return x.HtmlElementNode + } + } + return nil +} + +type isNode_Node interface { + isNode_Node() +} + +type Node_LineBreakNode struct { + // Block nodes. + LineBreakNode *LineBreakNode `protobuf:"bytes,11,opt,name=line_break_node,json=lineBreakNode,proto3,oneof"` +} + +type Node_ParagraphNode struct { + ParagraphNode *ParagraphNode `protobuf:"bytes,12,opt,name=paragraph_node,json=paragraphNode,proto3,oneof"` +} + +type Node_CodeBlockNode struct { + CodeBlockNode *CodeBlockNode `protobuf:"bytes,13,opt,name=code_block_node,json=codeBlockNode,proto3,oneof"` +} + +type Node_HeadingNode struct { + HeadingNode *HeadingNode `protobuf:"bytes,14,opt,name=heading_node,json=headingNode,proto3,oneof"` +} + +type Node_HorizontalRuleNode struct { + HorizontalRuleNode *HorizontalRuleNode `protobuf:"bytes,15,opt,name=horizontal_rule_node,json=horizontalRuleNode,proto3,oneof"` +} + +type Node_BlockquoteNode struct { + BlockquoteNode *BlockquoteNode `protobuf:"bytes,16,opt,name=blockquote_node,json=blockquoteNode,proto3,oneof"` +} + +type Node_ListNode struct { + ListNode *ListNode `protobuf:"bytes,17,opt,name=list_node,json=listNode,proto3,oneof"` +} + +type Node_OrderedListItemNode struct { + OrderedListItemNode *OrderedListItemNode `protobuf:"bytes,18,opt,name=ordered_list_item_node,json=orderedListItemNode,proto3,oneof"` +} + +type Node_UnorderedListItemNode struct { + UnorderedListItemNode *UnorderedListItemNode `protobuf:"bytes,19,opt,name=unordered_list_item_node,json=unorderedListItemNode,proto3,oneof"` +} + +type Node_TaskListItemNode struct { + TaskListItemNode *TaskListItemNode `protobuf:"bytes,20,opt,name=task_list_item_node,json=taskListItemNode,proto3,oneof"` +} + +type Node_MathBlockNode struct { + MathBlockNode *MathBlockNode `protobuf:"bytes,21,opt,name=math_block_node,json=mathBlockNode,proto3,oneof"` +} + +type Node_TableNode struct { + TableNode *TableNode `protobuf:"bytes,22,opt,name=table_node,json=tableNode,proto3,oneof"` +} + +type Node_EmbeddedContentNode struct { + EmbeddedContentNode *EmbeddedContentNode `protobuf:"bytes,23,opt,name=embedded_content_node,json=embeddedContentNode,proto3,oneof"` +} + +type Node_TextNode struct { + // Inline nodes. + TextNode *TextNode `protobuf:"bytes,51,opt,name=text_node,json=textNode,proto3,oneof"` +} + +type Node_BoldNode struct { + BoldNode *BoldNode `protobuf:"bytes,52,opt,name=bold_node,json=boldNode,proto3,oneof"` +} + +type Node_ItalicNode struct { + ItalicNode *ItalicNode `protobuf:"bytes,53,opt,name=italic_node,json=italicNode,proto3,oneof"` +} + +type Node_BoldItalicNode struct { + BoldItalicNode *BoldItalicNode `protobuf:"bytes,54,opt,name=bold_italic_node,json=boldItalicNode,proto3,oneof"` +} + +type Node_CodeNode struct { + CodeNode *CodeNode `protobuf:"bytes,55,opt,name=code_node,json=codeNode,proto3,oneof"` +} + +type Node_ImageNode struct { + ImageNode *ImageNode `protobuf:"bytes,56,opt,name=image_node,json=imageNode,proto3,oneof"` +} + +type Node_LinkNode struct { + LinkNode *LinkNode `protobuf:"bytes,57,opt,name=link_node,json=linkNode,proto3,oneof"` +} + +type Node_AutoLinkNode struct { + AutoLinkNode *AutoLinkNode `protobuf:"bytes,58,opt,name=auto_link_node,json=autoLinkNode,proto3,oneof"` +} + +type Node_TagNode struct { + TagNode *TagNode `protobuf:"bytes,59,opt,name=tag_node,json=tagNode,proto3,oneof"` +} + +type Node_StrikethroughNode struct { + StrikethroughNode *StrikethroughNode `protobuf:"bytes,60,opt,name=strikethrough_node,json=strikethroughNode,proto3,oneof"` +} + +type Node_EscapingCharacterNode struct { + EscapingCharacterNode *EscapingCharacterNode `protobuf:"bytes,61,opt,name=escaping_character_node,json=escapingCharacterNode,proto3,oneof"` +} + +type Node_MathNode struct { + MathNode *MathNode `protobuf:"bytes,62,opt,name=math_node,json=mathNode,proto3,oneof"` +} + +type Node_HighlightNode struct { + HighlightNode *HighlightNode `protobuf:"bytes,63,opt,name=highlight_node,json=highlightNode,proto3,oneof"` +} + +type Node_SubscriptNode struct { + SubscriptNode *SubscriptNode `protobuf:"bytes,64,opt,name=subscript_node,json=subscriptNode,proto3,oneof"` +} + +type Node_SuperscriptNode struct { + SuperscriptNode *SuperscriptNode `protobuf:"bytes,65,opt,name=superscript_node,json=superscriptNode,proto3,oneof"` +} + +type Node_ReferencedContentNode struct { + ReferencedContentNode *ReferencedContentNode `protobuf:"bytes,66,opt,name=referenced_content_node,json=referencedContentNode,proto3,oneof"` +} + +type Node_SpoilerNode struct { + SpoilerNode *SpoilerNode `protobuf:"bytes,67,opt,name=spoiler_node,json=spoilerNode,proto3,oneof"` +} + +type Node_HtmlElementNode struct { + HtmlElementNode *HTMLElementNode `protobuf:"bytes,68,opt,name=html_element_node,json=htmlElementNode,proto3,oneof"` +} + +func (*Node_LineBreakNode) isNode_Node() {} + +func (*Node_ParagraphNode) isNode_Node() {} + +func (*Node_CodeBlockNode) isNode_Node() {} + +func (*Node_HeadingNode) isNode_Node() {} + +func (*Node_HorizontalRuleNode) isNode_Node() {} + +func (*Node_BlockquoteNode) isNode_Node() {} + +func (*Node_ListNode) isNode_Node() {} + +func (*Node_OrderedListItemNode) isNode_Node() {} + +func (*Node_UnorderedListItemNode) isNode_Node() {} + +func (*Node_TaskListItemNode) isNode_Node() {} + +func (*Node_MathBlockNode) isNode_Node() {} + +func (*Node_TableNode) isNode_Node() {} + +func (*Node_EmbeddedContentNode) isNode_Node() {} + +func (*Node_TextNode) isNode_Node() {} + +func (*Node_BoldNode) isNode_Node() {} + +func (*Node_ItalicNode) isNode_Node() {} + +func (*Node_BoldItalicNode) isNode_Node() {} + +func (*Node_CodeNode) isNode_Node() {} + +func (*Node_ImageNode) isNode_Node() {} + +func (*Node_LinkNode) isNode_Node() {} + +func (*Node_AutoLinkNode) isNode_Node() {} + +func (*Node_TagNode) isNode_Node() {} + +func (*Node_StrikethroughNode) isNode_Node() {} + +func (*Node_EscapingCharacterNode) isNode_Node() {} + +func (*Node_MathNode) isNode_Node() {} + +func (*Node_HighlightNode) isNode_Node() {} + +func (*Node_SubscriptNode) isNode_Node() {} + +func (*Node_SuperscriptNode) isNode_Node() {} + +func (*Node_ReferencedContentNode) isNode_Node() {} + +func (*Node_SpoilerNode) isNode_Node() {} + +func (*Node_HtmlElementNode) isNode_Node() {} + +type LineBreakNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LineBreakNode) Reset() { + *x = LineBreakNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LineBreakNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LineBreakNode) ProtoMessage() {} + +func (x *LineBreakNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LineBreakNode.ProtoReflect.Descriptor instead. +func (*LineBreakNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{9} +} + +type ParagraphNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Children []*Node `protobuf:"bytes,1,rep,name=children,proto3" json:"children,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ParagraphNode) Reset() { + *x = ParagraphNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ParagraphNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ParagraphNode) ProtoMessage() {} + +func (x *ParagraphNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ParagraphNode.ProtoReflect.Descriptor instead. +func (*ParagraphNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{10} +} + +func (x *ParagraphNode) GetChildren() []*Node { + if x != nil { + return x.Children + } + return nil +} + +type CodeBlockNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Language string `protobuf:"bytes,1,opt,name=language,proto3" json:"language,omitempty"` + Content string `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CodeBlockNode) Reset() { + *x = CodeBlockNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CodeBlockNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CodeBlockNode) ProtoMessage() {} + +func (x *CodeBlockNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CodeBlockNode.ProtoReflect.Descriptor instead. +func (*CodeBlockNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{11} +} + +func (x *CodeBlockNode) GetLanguage() string { + if x != nil { + return x.Language + } + return "" +} + +func (x *CodeBlockNode) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +type HeadingNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Level int32 `protobuf:"varint,1,opt,name=level,proto3" json:"level,omitempty"` + Children []*Node `protobuf:"bytes,2,rep,name=children,proto3" json:"children,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HeadingNode) Reset() { + *x = HeadingNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HeadingNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HeadingNode) ProtoMessage() {} + +func (x *HeadingNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HeadingNode.ProtoReflect.Descriptor instead. +func (*HeadingNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{12} +} + +func (x *HeadingNode) GetLevel() int32 { + if x != nil { + return x.Level + } + return 0 +} + +func (x *HeadingNode) GetChildren() []*Node { + if x != nil { + return x.Children + } + return nil +} + +type HorizontalRuleNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Symbol string `protobuf:"bytes,1,opt,name=symbol,proto3" json:"symbol,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HorizontalRuleNode) Reset() { + *x = HorizontalRuleNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HorizontalRuleNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HorizontalRuleNode) ProtoMessage() {} + +func (x *HorizontalRuleNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HorizontalRuleNode.ProtoReflect.Descriptor instead. +func (*HorizontalRuleNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{13} +} + +func (x *HorizontalRuleNode) GetSymbol() string { + if x != nil { + return x.Symbol + } + return "" +} + +type BlockquoteNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Children []*Node `protobuf:"bytes,1,rep,name=children,proto3" json:"children,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BlockquoteNode) Reset() { + *x = BlockquoteNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BlockquoteNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BlockquoteNode) ProtoMessage() {} + +func (x *BlockquoteNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BlockquoteNode.ProtoReflect.Descriptor instead. +func (*BlockquoteNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{14} +} + +func (x *BlockquoteNode) GetChildren() []*Node { + if x != nil { + return x.Children + } + return nil +} + +type ListNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Kind ListNode_Kind `protobuf:"varint,1,opt,name=kind,proto3,enum=memos.api.v1.ListNode_Kind" json:"kind,omitempty"` + Indent int32 `protobuf:"varint,2,opt,name=indent,proto3" json:"indent,omitempty"` + Children []*Node `protobuf:"bytes,3,rep,name=children,proto3" json:"children,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListNode) Reset() { + *x = ListNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListNode) ProtoMessage() {} + +func (x *ListNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListNode.ProtoReflect.Descriptor instead. +func (*ListNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{15} +} + +func (x *ListNode) GetKind() ListNode_Kind { + if x != nil { + return x.Kind + } + return ListNode_KIND_UNSPECIFIED +} + +func (x *ListNode) GetIndent() int32 { + if x != nil { + return x.Indent + } + return 0 +} + +func (x *ListNode) GetChildren() []*Node { + if x != nil { + return x.Children + } + return nil +} + +type OrderedListItemNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Number string `protobuf:"bytes,1,opt,name=number,proto3" json:"number,omitempty"` + Indent int32 `protobuf:"varint,2,opt,name=indent,proto3" json:"indent,omitempty"` + Children []*Node `protobuf:"bytes,3,rep,name=children,proto3" json:"children,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OrderedListItemNode) Reset() { + *x = OrderedListItemNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OrderedListItemNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OrderedListItemNode) ProtoMessage() {} + +func (x *OrderedListItemNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OrderedListItemNode.ProtoReflect.Descriptor instead. +func (*OrderedListItemNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{16} +} + +func (x *OrderedListItemNode) GetNumber() string { + if x != nil { + return x.Number + } + return "" +} + +func (x *OrderedListItemNode) GetIndent() int32 { + if x != nil { + return x.Indent + } + return 0 +} + +func (x *OrderedListItemNode) GetChildren() []*Node { + if x != nil { + return x.Children + } + return nil +} + +type UnorderedListItemNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Symbol string `protobuf:"bytes,1,opt,name=symbol,proto3" json:"symbol,omitempty"` + Indent int32 `protobuf:"varint,2,opt,name=indent,proto3" json:"indent,omitempty"` + Children []*Node `protobuf:"bytes,3,rep,name=children,proto3" json:"children,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UnorderedListItemNode) Reset() { + *x = UnorderedListItemNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UnorderedListItemNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UnorderedListItemNode) ProtoMessage() {} + +func (x *UnorderedListItemNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UnorderedListItemNode.ProtoReflect.Descriptor instead. +func (*UnorderedListItemNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{17} +} + +func (x *UnorderedListItemNode) GetSymbol() string { + if x != nil { + return x.Symbol + } + return "" +} + +func (x *UnorderedListItemNode) GetIndent() int32 { + if x != nil { + return x.Indent + } + return 0 +} + +func (x *UnorderedListItemNode) GetChildren() []*Node { + if x != nil { + return x.Children + } + return nil +} + +type TaskListItemNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Symbol string `protobuf:"bytes,1,opt,name=symbol,proto3" json:"symbol,omitempty"` + Indent int32 `protobuf:"varint,2,opt,name=indent,proto3" json:"indent,omitempty"` + Complete bool `protobuf:"varint,3,opt,name=complete,proto3" json:"complete,omitempty"` + Children []*Node `protobuf:"bytes,4,rep,name=children,proto3" json:"children,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TaskListItemNode) Reset() { + *x = TaskListItemNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TaskListItemNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TaskListItemNode) ProtoMessage() {} + +func (x *TaskListItemNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TaskListItemNode.ProtoReflect.Descriptor instead. +func (*TaskListItemNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{18} +} + +func (x *TaskListItemNode) GetSymbol() string { + if x != nil { + return x.Symbol + } + return "" +} + +func (x *TaskListItemNode) GetIndent() int32 { + if x != nil { + return x.Indent + } + return 0 +} + +func (x *TaskListItemNode) GetComplete() bool { + if x != nil { + return x.Complete + } + return false +} + +func (x *TaskListItemNode) GetChildren() []*Node { + if x != nil { + return x.Children + } + return nil +} + +type MathBlockNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MathBlockNode) Reset() { + *x = MathBlockNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MathBlockNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MathBlockNode) ProtoMessage() {} + +func (x *MathBlockNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MathBlockNode.ProtoReflect.Descriptor instead. +func (*MathBlockNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{19} +} + +func (x *MathBlockNode) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +type TableNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Header []*Node `protobuf:"bytes,1,rep,name=header,proto3" json:"header,omitempty"` + Delimiter []string `protobuf:"bytes,2,rep,name=delimiter,proto3" json:"delimiter,omitempty"` + Rows []*TableNode_Row `protobuf:"bytes,3,rep,name=rows,proto3" json:"rows,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TableNode) Reset() { + *x = TableNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TableNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TableNode) ProtoMessage() {} + +func (x *TableNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TableNode.ProtoReflect.Descriptor instead. +func (*TableNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{20} +} + +func (x *TableNode) GetHeader() []*Node { + if x != nil { + return x.Header + } + return nil +} + +func (x *TableNode) GetDelimiter() []string { + if x != nil { + return x.Delimiter + } + return nil +} + +func (x *TableNode) GetRows() []*TableNode_Row { + if x != nil { + return x.Rows + } + return nil +} + +type EmbeddedContentNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The resource name of the embedded content. + ResourceName string `protobuf:"bytes,1,opt,name=resource_name,json=resourceName,proto3" json:"resource_name,omitempty"` + // Additional parameters for the embedded content. + Params string `protobuf:"bytes,2,opt,name=params,proto3" json:"params,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EmbeddedContentNode) Reset() { + *x = EmbeddedContentNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EmbeddedContentNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EmbeddedContentNode) ProtoMessage() {} + +func (x *EmbeddedContentNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EmbeddedContentNode.ProtoReflect.Descriptor instead. +func (*EmbeddedContentNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{21} +} + +func (x *EmbeddedContentNode) GetResourceName() string { + if x != nil { + return x.ResourceName + } + return "" +} + +func (x *EmbeddedContentNode) GetParams() string { + if x != nil { + return x.Params + } + return "" +} + +type TextNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TextNode) Reset() { + *x = TextNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TextNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TextNode) ProtoMessage() {} + +func (x *TextNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TextNode.ProtoReflect.Descriptor instead. +func (*TextNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{22} +} + +func (x *TextNode) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +type BoldNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Symbol string `protobuf:"bytes,1,opt,name=symbol,proto3" json:"symbol,omitempty"` + Children []*Node `protobuf:"bytes,2,rep,name=children,proto3" json:"children,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BoldNode) Reset() { + *x = BoldNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BoldNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BoldNode) ProtoMessage() {} + +func (x *BoldNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BoldNode.ProtoReflect.Descriptor instead. +func (*BoldNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{23} +} + +func (x *BoldNode) GetSymbol() string { + if x != nil { + return x.Symbol + } + return "" +} + +func (x *BoldNode) GetChildren() []*Node { + if x != nil { + return x.Children + } + return nil +} + +type ItalicNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Symbol string `protobuf:"bytes,1,opt,name=symbol,proto3" json:"symbol,omitempty"` + Children []*Node `protobuf:"bytes,2,rep,name=children,proto3" json:"children,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ItalicNode) Reset() { + *x = ItalicNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ItalicNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ItalicNode) ProtoMessage() {} + +func (x *ItalicNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ItalicNode.ProtoReflect.Descriptor instead. +func (*ItalicNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{24} +} + +func (x *ItalicNode) GetSymbol() string { + if x != nil { + return x.Symbol + } + return "" +} + +func (x *ItalicNode) GetChildren() []*Node { + if x != nil { + return x.Children + } + return nil +} + +type BoldItalicNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Symbol string `protobuf:"bytes,1,opt,name=symbol,proto3" json:"symbol,omitempty"` + Content string `protobuf:"bytes,2,opt,name=content,proto3" json:"content,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BoldItalicNode) Reset() { + *x = BoldItalicNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BoldItalicNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BoldItalicNode) ProtoMessage() {} + +func (x *BoldItalicNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use BoldItalicNode.ProtoReflect.Descriptor instead. +func (*BoldItalicNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{25} +} + +func (x *BoldItalicNode) GetSymbol() string { + if x != nil { + return x.Symbol + } + return "" +} + +func (x *BoldItalicNode) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +type CodeNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CodeNode) Reset() { + *x = CodeNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CodeNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CodeNode) ProtoMessage() {} + +func (x *CodeNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CodeNode.ProtoReflect.Descriptor instead. +func (*CodeNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{26} +} + +func (x *CodeNode) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +type ImageNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + AltText string `protobuf:"bytes,1,opt,name=alt_text,json=altText,proto3" json:"alt_text,omitempty"` + Url string `protobuf:"bytes,2,opt,name=url,proto3" json:"url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ImageNode) Reset() { + *x = ImageNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ImageNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ImageNode) ProtoMessage() {} + +func (x *ImageNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ImageNode.ProtoReflect.Descriptor instead. +func (*ImageNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{27} +} + +func (x *ImageNode) GetAltText() string { + if x != nil { + return x.AltText + } + return "" +} + +func (x *ImageNode) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +type LinkNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Content []*Node `protobuf:"bytes,1,rep,name=content,proto3" json:"content,omitempty"` + Url string `protobuf:"bytes,2,opt,name=url,proto3" json:"url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *LinkNode) Reset() { + *x = LinkNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LinkNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LinkNode) ProtoMessage() {} + +func (x *LinkNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LinkNode.ProtoReflect.Descriptor instead. +func (*LinkNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{28} +} + +func (x *LinkNode) GetContent() []*Node { + if x != nil { + return x.Content + } + return nil +} + +func (x *LinkNode) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +type AutoLinkNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"` + IsRawText bool `protobuf:"varint,2,opt,name=is_raw_text,json=isRawText,proto3" json:"is_raw_text,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AutoLinkNode) Reset() { + *x = AutoLinkNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AutoLinkNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AutoLinkNode) ProtoMessage() {} + +func (x *AutoLinkNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AutoLinkNode.ProtoReflect.Descriptor instead. +func (*AutoLinkNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{29} +} + +func (x *AutoLinkNode) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +func (x *AutoLinkNode) GetIsRawText() bool { + if x != nil { + return x.IsRawText + } + return false +} + +type TagNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TagNode) Reset() { + *x = TagNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TagNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TagNode) ProtoMessage() {} + +func (x *TagNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TagNode.ProtoReflect.Descriptor instead. +func (*TagNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{30} +} + +func (x *TagNode) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +type StrikethroughNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StrikethroughNode) Reset() { + *x = StrikethroughNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StrikethroughNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StrikethroughNode) ProtoMessage() {} + +func (x *StrikethroughNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StrikethroughNode.ProtoReflect.Descriptor instead. +func (*StrikethroughNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{31} +} + +func (x *StrikethroughNode) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +type EscapingCharacterNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Symbol string `protobuf:"bytes,1,opt,name=symbol,proto3" json:"symbol,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *EscapingCharacterNode) Reset() { + *x = EscapingCharacterNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *EscapingCharacterNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EscapingCharacterNode) ProtoMessage() {} + +func (x *EscapingCharacterNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[32] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EscapingCharacterNode.ProtoReflect.Descriptor instead. +func (*EscapingCharacterNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{32} +} + +func (x *EscapingCharacterNode) GetSymbol() string { + if x != nil { + return x.Symbol + } + return "" +} + +type MathNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MathNode) Reset() { + *x = MathNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MathNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MathNode) ProtoMessage() {} + +func (x *MathNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MathNode.ProtoReflect.Descriptor instead. +func (*MathNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{33} +} + +func (x *MathNode) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +type HighlightNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HighlightNode) Reset() { + *x = HighlightNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HighlightNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HighlightNode) ProtoMessage() {} + +func (x *HighlightNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HighlightNode.ProtoReflect.Descriptor instead. +func (*HighlightNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{34} +} + +func (x *HighlightNode) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +type SubscriptNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SubscriptNode) Reset() { + *x = SubscriptNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SubscriptNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SubscriptNode) ProtoMessage() {} + +func (x *SubscriptNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SubscriptNode.ProtoReflect.Descriptor instead. +func (*SubscriptNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{35} +} + +func (x *SubscriptNode) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +type SuperscriptNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SuperscriptNode) Reset() { + *x = SuperscriptNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SuperscriptNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SuperscriptNode) ProtoMessage() {} + +func (x *SuperscriptNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SuperscriptNode.ProtoReflect.Descriptor instead. +func (*SuperscriptNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{36} +} + +func (x *SuperscriptNode) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +type ReferencedContentNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The resource name of the referenced content. + ResourceName string `protobuf:"bytes,1,opt,name=resource_name,json=resourceName,proto3" json:"resource_name,omitempty"` + // Additional parameters for the referenced content. + Params string `protobuf:"bytes,2,opt,name=params,proto3" json:"params,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ReferencedContentNode) Reset() { + *x = ReferencedContentNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ReferencedContentNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ReferencedContentNode) ProtoMessage() {} + +func (x *ReferencedContentNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ReferencedContentNode.ProtoReflect.Descriptor instead. +func (*ReferencedContentNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{37} +} + +func (x *ReferencedContentNode) GetResourceName() string { + if x != nil { + return x.ResourceName + } + return "" +} + +func (x *ReferencedContentNode) GetParams() string { + if x != nil { + return x.Params + } + return "" +} + +type SpoilerNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + Content string `protobuf:"bytes,1,opt,name=content,proto3" json:"content,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SpoilerNode) Reset() { + *x = SpoilerNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SpoilerNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SpoilerNode) ProtoMessage() {} + +func (x *SpoilerNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[38] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SpoilerNode.ProtoReflect.Descriptor instead. +func (*SpoilerNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{38} +} + +func (x *SpoilerNode) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +type HTMLElementNode struct { + state protoimpl.MessageState `protogen:"open.v1"` + TagName string `protobuf:"bytes,1,opt,name=tag_name,json=tagName,proto3" json:"tag_name,omitempty"` + Attributes map[string]string `protobuf:"bytes,2,rep,name=attributes,proto3" json:"attributes,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *HTMLElementNode) Reset() { + *x = HTMLElementNode{} + mi := &file_api_v1_markdown_service_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *HTMLElementNode) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*HTMLElementNode) ProtoMessage() {} + +func (x *HTMLElementNode) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[39] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use HTMLElementNode.ProtoReflect.Descriptor instead. +func (*HTMLElementNode) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{39} +} + +func (x *HTMLElementNode) GetTagName() string { + if x != nil { + return x.TagName + } + return "" +} + +func (x *HTMLElementNode) GetAttributes() map[string]string { + if x != nil { + return x.Attributes + } + return nil +} + +type TableNode_Row struct { + state protoimpl.MessageState `protogen:"open.v1"` + Cells []*Node `protobuf:"bytes,1,rep,name=cells,proto3" json:"cells,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *TableNode_Row) Reset() { + *x = TableNode_Row{} + mi := &file_api_v1_markdown_service_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *TableNode_Row) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*TableNode_Row) ProtoMessage() {} + +func (x *TableNode_Row) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_markdown_service_proto_msgTypes[40] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use TableNode_Row.ProtoReflect.Descriptor instead. +func (*TableNode_Row) Descriptor() ([]byte, []int) { + return file_api_v1_markdown_service_proto_rawDescGZIP(), []int{20, 0} +} + +func (x *TableNode_Row) GetCells() []*Node { + if x != nil { + return x.Cells + } + return nil +} + +var File_api_v1_markdown_service_proto protoreflect.FileDescriptor + +const file_api_v1_markdown_service_proto_rawDesc = "" + + "\n" + + "\x1dapi/v1/markdown_service.proto\x12\fmemos.api.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x1fgoogle/api/field_behavior.proto\"7\n" + + "\x14ParseMarkdownRequest\x12\x1f\n" + + "\bmarkdown\x18\x01 \x01(\tB\x03\xe0A\x02R\bmarkdown\"A\n" + + "\x15ParseMarkdownResponse\x12(\n" + + "\x05nodes\x18\x01 \x03(\v2\x12.memos.api.v1.NodeR\x05nodes\"L\n" + + "\x1bRestoreMarkdownNodesRequest\x12-\n" + + "\x05nodes\x18\x01 \x03(\v2\x12.memos.api.v1.NodeB\x03\xe0A\x02R\x05nodes\":\n" + + "\x1cRestoreMarkdownNodesResponse\x12\x1a\n" + + "\bmarkdown\x18\x01 \x01(\tR\bmarkdown\"N\n" + + "\x1dStringifyMarkdownNodesRequest\x12-\n" + + "\x05nodes\x18\x01 \x03(\v2\x12.memos.api.v1.NodeB\x03\xe0A\x02R\x05nodes\"?\n" + + "\x1eStringifyMarkdownNodesResponse\x12\x1d\n" + + "\n" + + "plain_text\x18\x01 \x01(\tR\tplainText\"1\n" + + "\x16GetLinkMetadataRequest\x12\x17\n" + + "\x04link\x18\x01 \x01(\tB\x03\xe0A\x02R\x04link\"\\\n" + + "\fLinkMetadata\x12\x14\n" + + "\x05title\x18\x01 \x01(\tR\x05title\x12 \n" + + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x14\n" + + "\x05image\x18\x03 \x01(\tR\x05image\"\xca\x11\n" + + "\x04Node\x12*\n" + + "\x04type\x18\x01 \x01(\x0e2\x16.memos.api.v1.NodeTypeR\x04type\x12E\n" + + "\x0fline_break_node\x18\v \x01(\v2\x1b.memos.api.v1.LineBreakNodeH\x00R\rlineBreakNode\x12D\n" + + "\x0eparagraph_node\x18\f \x01(\v2\x1b.memos.api.v1.ParagraphNodeH\x00R\rparagraphNode\x12E\n" + + "\x0fcode_block_node\x18\r \x01(\v2\x1b.memos.api.v1.CodeBlockNodeH\x00R\rcodeBlockNode\x12>\n" + + "\fheading_node\x18\x0e \x01(\v2\x19.memos.api.v1.HeadingNodeH\x00R\vheadingNode\x12T\n" + + "\x14horizontal_rule_node\x18\x0f \x01(\v2 .memos.api.v1.HorizontalRuleNodeH\x00R\x12horizontalRuleNode\x12G\n" + + "\x0fblockquote_node\x18\x10 \x01(\v2\x1c.memos.api.v1.BlockquoteNodeH\x00R\x0eblockquoteNode\x125\n" + + "\tlist_node\x18\x11 \x01(\v2\x16.memos.api.v1.ListNodeH\x00R\blistNode\x12X\n" + + "\x16ordered_list_item_node\x18\x12 \x01(\v2!.memos.api.v1.OrderedListItemNodeH\x00R\x13orderedListItemNode\x12^\n" + + "\x18unordered_list_item_node\x18\x13 \x01(\v2#.memos.api.v1.UnorderedListItemNodeH\x00R\x15unorderedListItemNode\x12O\n" + + "\x13task_list_item_node\x18\x14 \x01(\v2\x1e.memos.api.v1.TaskListItemNodeH\x00R\x10taskListItemNode\x12E\n" + + "\x0fmath_block_node\x18\x15 \x01(\v2\x1b.memos.api.v1.MathBlockNodeH\x00R\rmathBlockNode\x128\n" + + "\n" + + "table_node\x18\x16 \x01(\v2\x17.memos.api.v1.TableNodeH\x00R\ttableNode\x12W\n" + + "\x15embedded_content_node\x18\x17 \x01(\v2!.memos.api.v1.EmbeddedContentNodeH\x00R\x13embeddedContentNode\x125\n" + + "\ttext_node\x183 \x01(\v2\x16.memos.api.v1.TextNodeH\x00R\btextNode\x125\n" + + "\tbold_node\x184 \x01(\v2\x16.memos.api.v1.BoldNodeH\x00R\bboldNode\x12;\n" + + "\vitalic_node\x185 \x01(\v2\x18.memos.api.v1.ItalicNodeH\x00R\n" + + "italicNode\x12H\n" + + "\x10bold_italic_node\x186 \x01(\v2\x1c.memos.api.v1.BoldItalicNodeH\x00R\x0eboldItalicNode\x125\n" + + "\tcode_node\x187 \x01(\v2\x16.memos.api.v1.CodeNodeH\x00R\bcodeNode\x128\n" + + "\n" + + "image_node\x188 \x01(\v2\x17.memos.api.v1.ImageNodeH\x00R\timageNode\x125\n" + + "\tlink_node\x189 \x01(\v2\x16.memos.api.v1.LinkNodeH\x00R\blinkNode\x12B\n" + + "\x0eauto_link_node\x18: \x01(\v2\x1a.memos.api.v1.AutoLinkNodeH\x00R\fautoLinkNode\x122\n" + + "\btag_node\x18; \x01(\v2\x15.memos.api.v1.TagNodeH\x00R\atagNode\x12P\n" + + "\x12strikethrough_node\x18< \x01(\v2\x1f.memos.api.v1.StrikethroughNodeH\x00R\x11strikethroughNode\x12]\n" + + "\x17escaping_character_node\x18= \x01(\v2#.memos.api.v1.EscapingCharacterNodeH\x00R\x15escapingCharacterNode\x125\n" + + "\tmath_node\x18> \x01(\v2\x16.memos.api.v1.MathNodeH\x00R\bmathNode\x12D\n" + + "\x0ehighlight_node\x18? \x01(\v2\x1b.memos.api.v1.HighlightNodeH\x00R\rhighlightNode\x12D\n" + + "\x0esubscript_node\x18@ \x01(\v2\x1b.memos.api.v1.SubscriptNodeH\x00R\rsubscriptNode\x12J\n" + + "\x10superscript_node\x18A \x01(\v2\x1d.memos.api.v1.SuperscriptNodeH\x00R\x0fsuperscriptNode\x12]\n" + + "\x17referenced_content_node\x18B \x01(\v2#.memos.api.v1.ReferencedContentNodeH\x00R\x15referencedContentNode\x12>\n" + + "\fspoiler_node\x18C \x01(\v2\x19.memos.api.v1.SpoilerNodeH\x00R\vspoilerNode\x12K\n" + + "\x11html_element_node\x18D \x01(\v2\x1d.memos.api.v1.HTMLElementNodeH\x00R\x0fhtmlElementNodeB\x06\n" + + "\x04node\"\x0f\n" + + "\rLineBreakNode\"?\n" + + "\rParagraphNode\x12.\n" + + "\bchildren\x18\x01 \x03(\v2\x12.memos.api.v1.NodeR\bchildren\"E\n" + + "\rCodeBlockNode\x12\x1a\n" + + "\blanguage\x18\x01 \x01(\tR\blanguage\x12\x18\n" + + "\acontent\x18\x02 \x01(\tR\acontent\"S\n" + + "\vHeadingNode\x12\x14\n" + + "\x05level\x18\x01 \x01(\x05R\x05level\x12.\n" + + "\bchildren\x18\x02 \x03(\v2\x12.memos.api.v1.NodeR\bchildren\",\n" + + "\x12HorizontalRuleNode\x12\x16\n" + + "\x06symbol\x18\x01 \x01(\tR\x06symbol\"@\n" + + "\x0eBlockquoteNode\x12.\n" + + "\bchildren\x18\x01 \x03(\v2\x12.memos.api.v1.NodeR\bchildren\"\xce\x01\n" + + "\bListNode\x12/\n" + + "\x04kind\x18\x01 \x01(\x0e2\x1b.memos.api.v1.ListNode.KindR\x04kind\x12\x16\n" + + "\x06indent\x18\x02 \x01(\x05R\x06indent\x12.\n" + + "\bchildren\x18\x03 \x03(\v2\x12.memos.api.v1.NodeR\bchildren\"I\n" + + "\x04Kind\x12\x14\n" + + "\x10KIND_UNSPECIFIED\x10\x00\x12\v\n" + + "\aORDERED\x10\x01\x12\r\n" + + "\tUNORDERED\x10\x02\x12\x0f\n" + + "\vDESCRIPTION\x10\x03\"u\n" + + "\x13OrderedListItemNode\x12\x16\n" + + "\x06number\x18\x01 \x01(\tR\x06number\x12\x16\n" + + "\x06indent\x18\x02 \x01(\x05R\x06indent\x12.\n" + + "\bchildren\x18\x03 \x03(\v2\x12.memos.api.v1.NodeR\bchildren\"w\n" + + "\x15UnorderedListItemNode\x12\x16\n" + + "\x06symbol\x18\x01 \x01(\tR\x06symbol\x12\x16\n" + + "\x06indent\x18\x02 \x01(\x05R\x06indent\x12.\n" + + "\bchildren\x18\x03 \x03(\v2\x12.memos.api.v1.NodeR\bchildren\"\x8e\x01\n" + + "\x10TaskListItemNode\x12\x16\n" + + "\x06symbol\x18\x01 \x01(\tR\x06symbol\x12\x16\n" + + "\x06indent\x18\x02 \x01(\x05R\x06indent\x12\x1a\n" + + "\bcomplete\x18\x03 \x01(\bR\bcomplete\x12.\n" + + "\bchildren\x18\x04 \x03(\v2\x12.memos.api.v1.NodeR\bchildren\")\n" + + "\rMathBlockNode\x12\x18\n" + + "\acontent\x18\x01 \x01(\tR\acontent\"\xb7\x01\n" + + "\tTableNode\x12*\n" + + "\x06header\x18\x01 \x03(\v2\x12.memos.api.v1.NodeR\x06header\x12\x1c\n" + + "\tdelimiter\x18\x02 \x03(\tR\tdelimiter\x12/\n" + + "\x04rows\x18\x03 \x03(\v2\x1b.memos.api.v1.TableNode.RowR\x04rows\x1a/\n" + + "\x03Row\x12(\n" + + "\x05cells\x18\x01 \x03(\v2\x12.memos.api.v1.NodeR\x05cells\"R\n" + + "\x13EmbeddedContentNode\x12#\n" + + "\rresource_name\x18\x01 \x01(\tR\fresourceName\x12\x16\n" + + "\x06params\x18\x02 \x01(\tR\x06params\"$\n" + + "\bTextNode\x12\x18\n" + + "\acontent\x18\x01 \x01(\tR\acontent\"R\n" + + "\bBoldNode\x12\x16\n" + + "\x06symbol\x18\x01 \x01(\tR\x06symbol\x12.\n" + + "\bchildren\x18\x02 \x03(\v2\x12.memos.api.v1.NodeR\bchildren\"T\n" + + "\n" + + "ItalicNode\x12\x16\n" + + "\x06symbol\x18\x01 \x01(\tR\x06symbol\x12.\n" + + "\bchildren\x18\x02 \x03(\v2\x12.memos.api.v1.NodeR\bchildren\"B\n" + + "\x0eBoldItalicNode\x12\x16\n" + + "\x06symbol\x18\x01 \x01(\tR\x06symbol\x12\x18\n" + + "\acontent\x18\x02 \x01(\tR\acontent\"$\n" + + "\bCodeNode\x12\x18\n" + + "\acontent\x18\x01 \x01(\tR\acontent\"8\n" + + "\tImageNode\x12\x19\n" + + "\balt_text\x18\x01 \x01(\tR\aaltText\x12\x10\n" + + "\x03url\x18\x02 \x01(\tR\x03url\"J\n" + + "\bLinkNode\x12,\n" + + "\acontent\x18\x01 \x03(\v2\x12.memos.api.v1.NodeR\acontent\x12\x10\n" + + "\x03url\x18\x02 \x01(\tR\x03url\"@\n" + + "\fAutoLinkNode\x12\x10\n" + + "\x03url\x18\x01 \x01(\tR\x03url\x12\x1e\n" + + "\vis_raw_text\x18\x02 \x01(\bR\tisRawText\"#\n" + + "\aTagNode\x12\x18\n" + + "\acontent\x18\x01 \x01(\tR\acontent\"-\n" + + "\x11StrikethroughNode\x12\x18\n" + + "\acontent\x18\x01 \x01(\tR\acontent\"/\n" + + "\x15EscapingCharacterNode\x12\x16\n" + + "\x06symbol\x18\x01 \x01(\tR\x06symbol\"$\n" + + "\bMathNode\x12\x18\n" + + "\acontent\x18\x01 \x01(\tR\acontent\")\n" + + "\rHighlightNode\x12\x18\n" + + "\acontent\x18\x01 \x01(\tR\acontent\")\n" + + "\rSubscriptNode\x12\x18\n" + + "\acontent\x18\x01 \x01(\tR\acontent\"+\n" + + "\x0fSuperscriptNode\x12\x18\n" + + "\acontent\x18\x01 \x01(\tR\acontent\"T\n" + + "\x15ReferencedContentNode\x12#\n" + + "\rresource_name\x18\x01 \x01(\tR\fresourceName\x12\x16\n" + + "\x06params\x18\x02 \x01(\tR\x06params\"'\n" + + "\vSpoilerNode\x12\x18\n" + + "\acontent\x18\x01 \x01(\tR\acontent\"\xba\x01\n" + + "\x0fHTMLElementNode\x12\x19\n" + + "\btag_name\x18\x01 \x01(\tR\atagName\x12M\n" + + "\n" + + "attributes\x18\x02 \x03(\v2-.memos.api.v1.HTMLElementNode.AttributesEntryR\n" + + "attributes\x1a=\n" + + "\x0fAttributesEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01*\x83\x04\n" + + "\bNodeType\x12\x14\n" + + "\x10NODE_UNSPECIFIED\x10\x00\x12\x0e\n" + + "\n" + + "LINE_BREAK\x10\x01\x12\r\n" + + "\tPARAGRAPH\x10\x02\x12\x0e\n" + + "\n" + + "CODE_BLOCK\x10\x03\x12\v\n" + + "\aHEADING\x10\x04\x12\x13\n" + + "\x0fHORIZONTAL_RULE\x10\x05\x12\x0e\n" + + "\n" + + "BLOCKQUOTE\x10\x06\x12\b\n" + + "\x04LIST\x10\a\x12\x15\n" + + "\x11ORDERED_LIST_ITEM\x10\b\x12\x17\n" + + "\x13UNORDERED_LIST_ITEM\x10\t\x12\x12\n" + + "\x0eTASK_LIST_ITEM\x10\n" + + "\x12\x0e\n" + + "\n" + + "MATH_BLOCK\x10\v\x12\t\n" + + "\x05TABLE\x10\f\x12\x14\n" + + "\x10EMBEDDED_CONTENT\x10\r\x12\b\n" + + "\x04TEXT\x103\x12\b\n" + + "\x04BOLD\x104\x12\n" + + "\n" + + "\x06ITALIC\x105\x12\x0f\n" + + "\vBOLD_ITALIC\x106\x12\b\n" + + "\x04CODE\x107\x12\t\n" + + "\x05IMAGE\x108\x12\b\n" + + "\x04LINK\x109\x12\r\n" + + "\tAUTO_LINK\x10:\x12\a\n" + + "\x03TAG\x10;\x12\x11\n" + + "\rSTRIKETHROUGH\x10<\x12\x16\n" + + "\x12ESCAPING_CHARACTER\x10=\x12\b\n" + + "\x04MATH\x10>\x12\r\n" + + "\tHIGHLIGHT\x10?\x12\r\n" + + "\tSUBSCRIPT\x10@\x12\x0f\n" + + "\vSUPERSCRIPT\x10A\x12\x16\n" + + "\x12REFERENCED_CONTENT\x10B\x12\v\n" + + "\aSPOILER\x10C\x12\x10\n" + + "\fHTML_ELEMENT\x10D2\xc1\x04\n" + + "\x0fMarkdownService\x12{\n" + + "\rParseMarkdown\x12\".memos.api.v1.ParseMarkdownRequest\x1a#.memos.api.v1.ParseMarkdownResponse\"!\x82\xd3\xe4\x93\x02\x1b:\x01*\"\x16/api/v1/markdown:parse\x12\x92\x01\n" + + "\x14RestoreMarkdownNodes\x12).memos.api.v1.RestoreMarkdownNodesRequest\x1a*.memos.api.v1.RestoreMarkdownNodesResponse\"#\x82\xd3\xe4\x93\x02\x1d:\x01*\"\x18/api/v1/markdown:restore\x12\x9a\x01\n" + + "\x16StringifyMarkdownNodes\x12+.memos.api.v1.StringifyMarkdownNodesRequest\x1a,.memos.api.v1.StringifyMarkdownNodesResponse\"%\x82\xd3\xe4\x93\x02\x1f:\x01*\"\x1a/api/v1/markdown:stringify\x12\x7f\n" + + "\x0fGetLinkMetadata\x12$.memos.api.v1.GetLinkMetadataRequest\x1a\x1a.memos.api.v1.LinkMetadata\"*\x82\xd3\xe4\x93\x02$\x12\"/api/v1/markdown/links:getMetadataB\xac\x01\n" + + "\x10com.memos.api.v1B\x14MarkdownServiceProtoP\x01Z0github.com/usememos/memos/proto/gen/api/v1;apiv1\xa2\x02\x03MAX\xaa\x02\fMemos.Api.V1\xca\x02\fMemos\\Api\\V1\xe2\x02\x18Memos\\Api\\V1\\GPBMetadata\xea\x02\x0eMemos::Api::V1b\x06proto3" + +var ( + file_api_v1_markdown_service_proto_rawDescOnce sync.Once + file_api_v1_markdown_service_proto_rawDescData []byte +) + +func file_api_v1_markdown_service_proto_rawDescGZIP() []byte { + file_api_v1_markdown_service_proto_rawDescOnce.Do(func() { + file_api_v1_markdown_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_v1_markdown_service_proto_rawDesc), len(file_api_v1_markdown_service_proto_rawDesc))) + }) + return file_api_v1_markdown_service_proto_rawDescData +} + +var file_api_v1_markdown_service_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_api_v1_markdown_service_proto_msgTypes = make([]protoimpl.MessageInfo, 42) +var file_api_v1_markdown_service_proto_goTypes = []any{ + (NodeType)(0), // 0: memos.api.v1.NodeType + (ListNode_Kind)(0), // 1: memos.api.v1.ListNode.Kind + (*ParseMarkdownRequest)(nil), // 2: memos.api.v1.ParseMarkdownRequest + (*ParseMarkdownResponse)(nil), // 3: memos.api.v1.ParseMarkdownResponse + (*RestoreMarkdownNodesRequest)(nil), // 4: memos.api.v1.RestoreMarkdownNodesRequest + (*RestoreMarkdownNodesResponse)(nil), // 5: memos.api.v1.RestoreMarkdownNodesResponse + (*StringifyMarkdownNodesRequest)(nil), // 6: memos.api.v1.StringifyMarkdownNodesRequest + (*StringifyMarkdownNodesResponse)(nil), // 7: memos.api.v1.StringifyMarkdownNodesResponse + (*GetLinkMetadataRequest)(nil), // 8: memos.api.v1.GetLinkMetadataRequest + (*LinkMetadata)(nil), // 9: memos.api.v1.LinkMetadata + (*Node)(nil), // 10: memos.api.v1.Node + (*LineBreakNode)(nil), // 11: memos.api.v1.LineBreakNode + (*ParagraphNode)(nil), // 12: memos.api.v1.ParagraphNode + (*CodeBlockNode)(nil), // 13: memos.api.v1.CodeBlockNode + (*HeadingNode)(nil), // 14: memos.api.v1.HeadingNode + (*HorizontalRuleNode)(nil), // 15: memos.api.v1.HorizontalRuleNode + (*BlockquoteNode)(nil), // 16: memos.api.v1.BlockquoteNode + (*ListNode)(nil), // 17: memos.api.v1.ListNode + (*OrderedListItemNode)(nil), // 18: memos.api.v1.OrderedListItemNode + (*UnorderedListItemNode)(nil), // 19: memos.api.v1.UnorderedListItemNode + (*TaskListItemNode)(nil), // 20: memos.api.v1.TaskListItemNode + (*MathBlockNode)(nil), // 21: memos.api.v1.MathBlockNode + (*TableNode)(nil), // 22: memos.api.v1.TableNode + (*EmbeddedContentNode)(nil), // 23: memos.api.v1.EmbeddedContentNode + (*TextNode)(nil), // 24: memos.api.v1.TextNode + (*BoldNode)(nil), // 25: memos.api.v1.BoldNode + (*ItalicNode)(nil), // 26: memos.api.v1.ItalicNode + (*BoldItalicNode)(nil), // 27: memos.api.v1.BoldItalicNode + (*CodeNode)(nil), // 28: memos.api.v1.CodeNode + (*ImageNode)(nil), // 29: memos.api.v1.ImageNode + (*LinkNode)(nil), // 30: memos.api.v1.LinkNode + (*AutoLinkNode)(nil), // 31: memos.api.v1.AutoLinkNode + (*TagNode)(nil), // 32: memos.api.v1.TagNode + (*StrikethroughNode)(nil), // 33: memos.api.v1.StrikethroughNode + (*EscapingCharacterNode)(nil), // 34: memos.api.v1.EscapingCharacterNode + (*MathNode)(nil), // 35: memos.api.v1.MathNode + (*HighlightNode)(nil), // 36: memos.api.v1.HighlightNode + (*SubscriptNode)(nil), // 37: memos.api.v1.SubscriptNode + (*SuperscriptNode)(nil), // 38: memos.api.v1.SuperscriptNode + (*ReferencedContentNode)(nil), // 39: memos.api.v1.ReferencedContentNode + (*SpoilerNode)(nil), // 40: memos.api.v1.SpoilerNode + (*HTMLElementNode)(nil), // 41: memos.api.v1.HTMLElementNode + (*TableNode_Row)(nil), // 42: memos.api.v1.TableNode.Row + nil, // 43: memos.api.v1.HTMLElementNode.AttributesEntry +} +var file_api_v1_markdown_service_proto_depIdxs = []int32{ + 10, // 0: memos.api.v1.ParseMarkdownResponse.nodes:type_name -> memos.api.v1.Node + 10, // 1: memos.api.v1.RestoreMarkdownNodesRequest.nodes:type_name -> memos.api.v1.Node + 10, // 2: memos.api.v1.StringifyMarkdownNodesRequest.nodes:type_name -> memos.api.v1.Node + 0, // 3: memos.api.v1.Node.type:type_name -> memos.api.v1.NodeType + 11, // 4: memos.api.v1.Node.line_break_node:type_name -> memos.api.v1.LineBreakNode + 12, // 5: memos.api.v1.Node.paragraph_node:type_name -> memos.api.v1.ParagraphNode + 13, // 6: memos.api.v1.Node.code_block_node:type_name -> memos.api.v1.CodeBlockNode + 14, // 7: memos.api.v1.Node.heading_node:type_name -> memos.api.v1.HeadingNode + 15, // 8: memos.api.v1.Node.horizontal_rule_node:type_name -> memos.api.v1.HorizontalRuleNode + 16, // 9: memos.api.v1.Node.blockquote_node:type_name -> memos.api.v1.BlockquoteNode + 17, // 10: memos.api.v1.Node.list_node:type_name -> memos.api.v1.ListNode + 18, // 11: memos.api.v1.Node.ordered_list_item_node:type_name -> memos.api.v1.OrderedListItemNode + 19, // 12: memos.api.v1.Node.unordered_list_item_node:type_name -> memos.api.v1.UnorderedListItemNode + 20, // 13: memos.api.v1.Node.task_list_item_node:type_name -> memos.api.v1.TaskListItemNode + 21, // 14: memos.api.v1.Node.math_block_node:type_name -> memos.api.v1.MathBlockNode + 22, // 15: memos.api.v1.Node.table_node:type_name -> memos.api.v1.TableNode + 23, // 16: memos.api.v1.Node.embedded_content_node:type_name -> memos.api.v1.EmbeddedContentNode + 24, // 17: memos.api.v1.Node.text_node:type_name -> memos.api.v1.TextNode + 25, // 18: memos.api.v1.Node.bold_node:type_name -> memos.api.v1.BoldNode + 26, // 19: memos.api.v1.Node.italic_node:type_name -> memos.api.v1.ItalicNode + 27, // 20: memos.api.v1.Node.bold_italic_node:type_name -> memos.api.v1.BoldItalicNode + 28, // 21: memos.api.v1.Node.code_node:type_name -> memos.api.v1.CodeNode + 29, // 22: memos.api.v1.Node.image_node:type_name -> memos.api.v1.ImageNode + 30, // 23: memos.api.v1.Node.link_node:type_name -> memos.api.v1.LinkNode + 31, // 24: memos.api.v1.Node.auto_link_node:type_name -> memos.api.v1.AutoLinkNode + 32, // 25: memos.api.v1.Node.tag_node:type_name -> memos.api.v1.TagNode + 33, // 26: memos.api.v1.Node.strikethrough_node:type_name -> memos.api.v1.StrikethroughNode + 34, // 27: memos.api.v1.Node.escaping_character_node:type_name -> memos.api.v1.EscapingCharacterNode + 35, // 28: memos.api.v1.Node.math_node:type_name -> memos.api.v1.MathNode + 36, // 29: memos.api.v1.Node.highlight_node:type_name -> memos.api.v1.HighlightNode + 37, // 30: memos.api.v1.Node.subscript_node:type_name -> memos.api.v1.SubscriptNode + 38, // 31: memos.api.v1.Node.superscript_node:type_name -> memos.api.v1.SuperscriptNode + 39, // 32: memos.api.v1.Node.referenced_content_node:type_name -> memos.api.v1.ReferencedContentNode + 40, // 33: memos.api.v1.Node.spoiler_node:type_name -> memos.api.v1.SpoilerNode + 41, // 34: memos.api.v1.Node.html_element_node:type_name -> memos.api.v1.HTMLElementNode + 10, // 35: memos.api.v1.ParagraphNode.children:type_name -> memos.api.v1.Node + 10, // 36: memos.api.v1.HeadingNode.children:type_name -> memos.api.v1.Node + 10, // 37: memos.api.v1.BlockquoteNode.children:type_name -> memos.api.v1.Node + 1, // 38: memos.api.v1.ListNode.kind:type_name -> memos.api.v1.ListNode.Kind + 10, // 39: memos.api.v1.ListNode.children:type_name -> memos.api.v1.Node + 10, // 40: memos.api.v1.OrderedListItemNode.children:type_name -> memos.api.v1.Node + 10, // 41: memos.api.v1.UnorderedListItemNode.children:type_name -> memos.api.v1.Node + 10, // 42: memos.api.v1.TaskListItemNode.children:type_name -> memos.api.v1.Node + 10, // 43: memos.api.v1.TableNode.header:type_name -> memos.api.v1.Node + 42, // 44: memos.api.v1.TableNode.rows:type_name -> memos.api.v1.TableNode.Row + 10, // 45: memos.api.v1.BoldNode.children:type_name -> memos.api.v1.Node + 10, // 46: memos.api.v1.ItalicNode.children:type_name -> memos.api.v1.Node + 10, // 47: memos.api.v1.LinkNode.content:type_name -> memos.api.v1.Node + 43, // 48: memos.api.v1.HTMLElementNode.attributes:type_name -> memos.api.v1.HTMLElementNode.AttributesEntry + 10, // 49: memos.api.v1.TableNode.Row.cells:type_name -> memos.api.v1.Node + 2, // 50: memos.api.v1.MarkdownService.ParseMarkdown:input_type -> memos.api.v1.ParseMarkdownRequest + 4, // 51: memos.api.v1.MarkdownService.RestoreMarkdownNodes:input_type -> memos.api.v1.RestoreMarkdownNodesRequest + 6, // 52: memos.api.v1.MarkdownService.StringifyMarkdownNodes:input_type -> memos.api.v1.StringifyMarkdownNodesRequest + 8, // 53: memos.api.v1.MarkdownService.GetLinkMetadata:input_type -> memos.api.v1.GetLinkMetadataRequest + 3, // 54: memos.api.v1.MarkdownService.ParseMarkdown:output_type -> memos.api.v1.ParseMarkdownResponse + 5, // 55: memos.api.v1.MarkdownService.RestoreMarkdownNodes:output_type -> memos.api.v1.RestoreMarkdownNodesResponse + 7, // 56: memos.api.v1.MarkdownService.StringifyMarkdownNodes:output_type -> memos.api.v1.StringifyMarkdownNodesResponse + 9, // 57: memos.api.v1.MarkdownService.GetLinkMetadata:output_type -> memos.api.v1.LinkMetadata + 54, // [54:58] is the sub-list for method output_type + 50, // [50:54] is the sub-list for method input_type + 50, // [50:50] is the sub-list for extension type_name + 50, // [50:50] is the sub-list for extension extendee + 0, // [0:50] is the sub-list for field type_name +} + +func init() { file_api_v1_markdown_service_proto_init() } +func file_api_v1_markdown_service_proto_init() { + if File_api_v1_markdown_service_proto != nil { + return + } + file_api_v1_markdown_service_proto_msgTypes[8].OneofWrappers = []any{ + (*Node_LineBreakNode)(nil), + (*Node_ParagraphNode)(nil), + (*Node_CodeBlockNode)(nil), + (*Node_HeadingNode)(nil), + (*Node_HorizontalRuleNode)(nil), + (*Node_BlockquoteNode)(nil), + (*Node_ListNode)(nil), + (*Node_OrderedListItemNode)(nil), + (*Node_UnorderedListItemNode)(nil), + (*Node_TaskListItemNode)(nil), + (*Node_MathBlockNode)(nil), + (*Node_TableNode)(nil), + (*Node_EmbeddedContentNode)(nil), + (*Node_TextNode)(nil), + (*Node_BoldNode)(nil), + (*Node_ItalicNode)(nil), + (*Node_BoldItalicNode)(nil), + (*Node_CodeNode)(nil), + (*Node_ImageNode)(nil), + (*Node_LinkNode)(nil), + (*Node_AutoLinkNode)(nil), + (*Node_TagNode)(nil), + (*Node_StrikethroughNode)(nil), + (*Node_EscapingCharacterNode)(nil), + (*Node_MathNode)(nil), + (*Node_HighlightNode)(nil), + (*Node_SubscriptNode)(nil), + (*Node_SuperscriptNode)(nil), + (*Node_ReferencedContentNode)(nil), + (*Node_SpoilerNode)(nil), + (*Node_HtmlElementNode)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_markdown_service_proto_rawDesc), len(file_api_v1_markdown_service_proto_rawDesc)), + NumEnums: 2, + NumMessages: 42, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_api_v1_markdown_service_proto_goTypes, + DependencyIndexes: file_api_v1_markdown_service_proto_depIdxs, + EnumInfos: file_api_v1_markdown_service_proto_enumTypes, + MessageInfos: file_api_v1_markdown_service_proto_msgTypes, + }.Build() + File_api_v1_markdown_service_proto = out.File + file_api_v1_markdown_service_proto_goTypes = nil + file_api_v1_markdown_service_proto_depIdxs = nil +} diff --git a/proto/gen/api/v1/markdown_service.pb.gw.go b/proto/gen/api/v1/markdown_service.pb.gw.go new file mode 100644 index 0000000..39e162d --- /dev/null +++ b/proto/gen/api/v1/markdown_service.pb.gw.go @@ -0,0 +1,363 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: api/v1/markdown_service.proto + +/* +Package apiv1 is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package apiv1 + +import ( + "context" + "errors" + "io" + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// Suppress "imported and not used" errors +var ( + _ codes.Code + _ io.Reader + _ status.Status + _ = errors.New + _ = runtime.String + _ = utilities.NewDoubleArray + _ = metadata.Join +) + +func request_MarkdownService_ParseMarkdown_0(ctx context.Context, marshaler runtime.Marshaler, client MarkdownServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ParseMarkdownRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.ParseMarkdown(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_MarkdownService_ParseMarkdown_0(ctx context.Context, marshaler runtime.Marshaler, server MarkdownServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ParseMarkdownRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ParseMarkdown(ctx, &protoReq) + return msg, metadata, err +} + +func request_MarkdownService_RestoreMarkdownNodes_0(ctx context.Context, marshaler runtime.Marshaler, client MarkdownServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq RestoreMarkdownNodesRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.RestoreMarkdownNodes(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_MarkdownService_RestoreMarkdownNodes_0(ctx context.Context, marshaler runtime.Marshaler, server MarkdownServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq RestoreMarkdownNodesRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.RestoreMarkdownNodes(ctx, &protoReq) + return msg, metadata, err +} + +func request_MarkdownService_StringifyMarkdownNodes_0(ctx context.Context, marshaler runtime.Marshaler, client MarkdownServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq StringifyMarkdownNodesRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.StringifyMarkdownNodes(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_MarkdownService_StringifyMarkdownNodes_0(ctx context.Context, marshaler runtime.Marshaler, server MarkdownServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq StringifyMarkdownNodesRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.StringifyMarkdownNodes(ctx, &protoReq) + return msg, metadata, err +} + +var filter_MarkdownService_GetLinkMetadata_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + +func request_MarkdownService_GetLinkMetadata_0(ctx context.Context, marshaler runtime.Marshaler, client MarkdownServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetLinkMetadataRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MarkdownService_GetLinkMetadata_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.GetLinkMetadata(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_MarkdownService_GetLinkMetadata_0(ctx context.Context, marshaler runtime.Marshaler, server MarkdownServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetLinkMetadataRequest + metadata runtime.ServerMetadata + ) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MarkdownService_GetLinkMetadata_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.GetLinkMetadata(ctx, &protoReq) + return msg, metadata, err +} + +// RegisterMarkdownServiceHandlerServer registers the http handlers for service MarkdownService to "mux". +// UnaryRPC :call MarkdownServiceServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterMarkdownServiceHandlerFromEndpoint instead. +// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call. +func RegisterMarkdownServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server MarkdownServiceServer) error { + mux.Handle(http.MethodPost, pattern_MarkdownService_ParseMarkdown_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.MarkdownService/ParseMarkdown", runtime.WithHTTPPathPattern("/api/v1/markdown:parse")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_MarkdownService_ParseMarkdown_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MarkdownService_ParseMarkdown_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_MarkdownService_RestoreMarkdownNodes_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.MarkdownService/RestoreMarkdownNodes", runtime.WithHTTPPathPattern("/api/v1/markdown:restore")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_MarkdownService_RestoreMarkdownNodes_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MarkdownService_RestoreMarkdownNodes_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_MarkdownService_StringifyMarkdownNodes_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.MarkdownService/StringifyMarkdownNodes", runtime.WithHTTPPathPattern("/api/v1/markdown:stringify")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_MarkdownService_StringifyMarkdownNodes_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MarkdownService_StringifyMarkdownNodes_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_MarkdownService_GetLinkMetadata_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.MarkdownService/GetLinkMetadata", runtime.WithHTTPPathPattern("/api/v1/markdown/links:getMetadata")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_MarkdownService_GetLinkMetadata_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MarkdownService_GetLinkMetadata_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + + return nil +} + +// RegisterMarkdownServiceHandlerFromEndpoint is same as RegisterMarkdownServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterMarkdownServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.NewClient(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + return RegisterMarkdownServiceHandler(ctx, mux, conn) +} + +// RegisterMarkdownServiceHandler registers the http handlers for service MarkdownService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterMarkdownServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterMarkdownServiceHandlerClient(ctx, mux, NewMarkdownServiceClient(conn)) +} + +// RegisterMarkdownServiceHandlerClient registers the http handlers for service MarkdownService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "MarkdownServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "MarkdownServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "MarkdownServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares. +func RegisterMarkdownServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client MarkdownServiceClient) error { + mux.Handle(http.MethodPost, pattern_MarkdownService_ParseMarkdown_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.MarkdownService/ParseMarkdown", runtime.WithHTTPPathPattern("/api/v1/markdown:parse")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_MarkdownService_ParseMarkdown_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MarkdownService_ParseMarkdown_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_MarkdownService_RestoreMarkdownNodes_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.MarkdownService/RestoreMarkdownNodes", runtime.WithHTTPPathPattern("/api/v1/markdown:restore")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_MarkdownService_RestoreMarkdownNodes_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MarkdownService_RestoreMarkdownNodes_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_MarkdownService_StringifyMarkdownNodes_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.MarkdownService/StringifyMarkdownNodes", runtime.WithHTTPPathPattern("/api/v1/markdown:stringify")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_MarkdownService_StringifyMarkdownNodes_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MarkdownService_StringifyMarkdownNodes_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_MarkdownService_GetLinkMetadata_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.MarkdownService/GetLinkMetadata", runtime.WithHTTPPathPattern("/api/v1/markdown/links:getMetadata")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_MarkdownService_GetLinkMetadata_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MarkdownService_GetLinkMetadata_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + return nil +} + +var ( + pattern_MarkdownService_ParseMarkdown_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "markdown"}, "parse")) + pattern_MarkdownService_RestoreMarkdownNodes_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "markdown"}, "restore")) + pattern_MarkdownService_StringifyMarkdownNodes_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "markdown"}, "stringify")) + pattern_MarkdownService_GetLinkMetadata_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1", "markdown", "links"}, "getMetadata")) +) + +var ( + forward_MarkdownService_ParseMarkdown_0 = runtime.ForwardResponseMessage + forward_MarkdownService_RestoreMarkdownNodes_0 = runtime.ForwardResponseMessage + forward_MarkdownService_StringifyMarkdownNodes_0 = runtime.ForwardResponseMessage + forward_MarkdownService_GetLinkMetadata_0 = runtime.ForwardResponseMessage +) diff --git a/proto/gen/api/v1/markdown_service_grpc.pb.go b/proto/gen/api/v1/markdown_service_grpc.pb.go new file mode 100644 index 0000000..569ef62 --- /dev/null +++ b/proto/gen/api/v1/markdown_service_grpc.pb.go @@ -0,0 +1,251 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc (unknown) +// source: api/v1/markdown_service.proto + +package apiv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + MarkdownService_ParseMarkdown_FullMethodName = "/memos.api.v1.MarkdownService/ParseMarkdown" + MarkdownService_RestoreMarkdownNodes_FullMethodName = "/memos.api.v1.MarkdownService/RestoreMarkdownNodes" + MarkdownService_StringifyMarkdownNodes_FullMethodName = "/memos.api.v1.MarkdownService/StringifyMarkdownNodes" + MarkdownService_GetLinkMetadata_FullMethodName = "/memos.api.v1.MarkdownService/GetLinkMetadata" +) + +// MarkdownServiceClient is the client API for MarkdownService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type MarkdownServiceClient interface { + // ParseMarkdown parses the given markdown content and returns a list of nodes. + // This is a utility method that transforms markdown text into structured nodes. + ParseMarkdown(ctx context.Context, in *ParseMarkdownRequest, opts ...grpc.CallOption) (*ParseMarkdownResponse, error) + // RestoreMarkdownNodes restores the given nodes to markdown content. + // This is the inverse operation of ParseMarkdown. + RestoreMarkdownNodes(ctx context.Context, in *RestoreMarkdownNodesRequest, opts ...grpc.CallOption) (*RestoreMarkdownNodesResponse, error) + // StringifyMarkdownNodes stringify the given nodes to plain text content. + // This removes all markdown formatting and returns plain text. + StringifyMarkdownNodes(ctx context.Context, in *StringifyMarkdownNodesRequest, opts ...grpc.CallOption) (*StringifyMarkdownNodesResponse, error) + // GetLinkMetadata returns metadata for a given link. + // This is useful for generating link previews. + GetLinkMetadata(ctx context.Context, in *GetLinkMetadataRequest, opts ...grpc.CallOption) (*LinkMetadata, error) +} + +type markdownServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewMarkdownServiceClient(cc grpc.ClientConnInterface) MarkdownServiceClient { + return &markdownServiceClient{cc} +} + +func (c *markdownServiceClient) ParseMarkdown(ctx context.Context, in *ParseMarkdownRequest, opts ...grpc.CallOption) (*ParseMarkdownResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ParseMarkdownResponse) + err := c.cc.Invoke(ctx, MarkdownService_ParseMarkdown_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *markdownServiceClient) RestoreMarkdownNodes(ctx context.Context, in *RestoreMarkdownNodesRequest, opts ...grpc.CallOption) (*RestoreMarkdownNodesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(RestoreMarkdownNodesResponse) + err := c.cc.Invoke(ctx, MarkdownService_RestoreMarkdownNodes_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *markdownServiceClient) StringifyMarkdownNodes(ctx context.Context, in *StringifyMarkdownNodesRequest, opts ...grpc.CallOption) (*StringifyMarkdownNodesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(StringifyMarkdownNodesResponse) + err := c.cc.Invoke(ctx, MarkdownService_StringifyMarkdownNodes_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *markdownServiceClient) GetLinkMetadata(ctx context.Context, in *GetLinkMetadataRequest, opts ...grpc.CallOption) (*LinkMetadata, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(LinkMetadata) + err := c.cc.Invoke(ctx, MarkdownService_GetLinkMetadata_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// MarkdownServiceServer is the server API for MarkdownService service. +// All implementations must embed UnimplementedMarkdownServiceServer +// for forward compatibility. +type MarkdownServiceServer interface { + // ParseMarkdown parses the given markdown content and returns a list of nodes. + // This is a utility method that transforms markdown text into structured nodes. + ParseMarkdown(context.Context, *ParseMarkdownRequest) (*ParseMarkdownResponse, error) + // RestoreMarkdownNodes restores the given nodes to markdown content. + // This is the inverse operation of ParseMarkdown. + RestoreMarkdownNodes(context.Context, *RestoreMarkdownNodesRequest) (*RestoreMarkdownNodesResponse, error) + // StringifyMarkdownNodes stringify the given nodes to plain text content. + // This removes all markdown formatting and returns plain text. + StringifyMarkdownNodes(context.Context, *StringifyMarkdownNodesRequest) (*StringifyMarkdownNodesResponse, error) + // GetLinkMetadata returns metadata for a given link. + // This is useful for generating link previews. + GetLinkMetadata(context.Context, *GetLinkMetadataRequest) (*LinkMetadata, error) + mustEmbedUnimplementedMarkdownServiceServer() +} + +// UnimplementedMarkdownServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedMarkdownServiceServer struct{} + +func (UnimplementedMarkdownServiceServer) ParseMarkdown(context.Context, *ParseMarkdownRequest) (*ParseMarkdownResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ParseMarkdown not implemented") +} +func (UnimplementedMarkdownServiceServer) RestoreMarkdownNodes(context.Context, *RestoreMarkdownNodesRequest) (*RestoreMarkdownNodesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method RestoreMarkdownNodes not implemented") +} +func (UnimplementedMarkdownServiceServer) StringifyMarkdownNodes(context.Context, *StringifyMarkdownNodesRequest) (*StringifyMarkdownNodesResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method StringifyMarkdownNodes not implemented") +} +func (UnimplementedMarkdownServiceServer) GetLinkMetadata(context.Context, *GetLinkMetadataRequest) (*LinkMetadata, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetLinkMetadata not implemented") +} +func (UnimplementedMarkdownServiceServer) mustEmbedUnimplementedMarkdownServiceServer() {} +func (UnimplementedMarkdownServiceServer) testEmbeddedByValue() {} + +// UnsafeMarkdownServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to MarkdownServiceServer will +// result in compilation errors. +type UnsafeMarkdownServiceServer interface { + mustEmbedUnimplementedMarkdownServiceServer() +} + +func RegisterMarkdownServiceServer(s grpc.ServiceRegistrar, srv MarkdownServiceServer) { + // If the following call pancis, it indicates UnimplementedMarkdownServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&MarkdownService_ServiceDesc, srv) +} + +func _MarkdownService_ParseMarkdown_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ParseMarkdownRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MarkdownServiceServer).ParseMarkdown(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MarkdownService_ParseMarkdown_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MarkdownServiceServer).ParseMarkdown(ctx, req.(*ParseMarkdownRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _MarkdownService_RestoreMarkdownNodes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RestoreMarkdownNodesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MarkdownServiceServer).RestoreMarkdownNodes(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MarkdownService_RestoreMarkdownNodes_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MarkdownServiceServer).RestoreMarkdownNodes(ctx, req.(*RestoreMarkdownNodesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _MarkdownService_StringifyMarkdownNodes_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(StringifyMarkdownNodesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MarkdownServiceServer).StringifyMarkdownNodes(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MarkdownService_StringifyMarkdownNodes_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MarkdownServiceServer).StringifyMarkdownNodes(ctx, req.(*StringifyMarkdownNodesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _MarkdownService_GetLinkMetadata_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetLinkMetadataRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MarkdownServiceServer).GetLinkMetadata(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MarkdownService_GetLinkMetadata_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MarkdownServiceServer).GetLinkMetadata(ctx, req.(*GetLinkMetadataRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// MarkdownService_ServiceDesc is the grpc.ServiceDesc for MarkdownService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var MarkdownService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "memos.api.v1.MarkdownService", + HandlerType: (*MarkdownServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ParseMarkdown", + Handler: _MarkdownService_ParseMarkdown_Handler, + }, + { + MethodName: "RestoreMarkdownNodes", + Handler: _MarkdownService_RestoreMarkdownNodes_Handler, + }, + { + MethodName: "StringifyMarkdownNodes", + Handler: _MarkdownService_StringifyMarkdownNodes_Handler, + }, + { + MethodName: "GetLinkMetadata", + Handler: _MarkdownService_GetLinkMetadata_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "api/v1/markdown_service.proto", +} diff --git a/proto/gen/api/v1/memo_service.pb.go b/proto/gen/api/v1/memo_service.pb.go new file mode 100644 index 0000000..f7053cf --- /dev/null +++ b/proto/gen/api/v1/memo_service.pb.go @@ -0,0 +1,2873 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: api/v1/memo_service.proto + +package apiv1 + +import ( + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + fieldmaskpb "google.golang.org/protobuf/types/known/fieldmaskpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Visibility int32 + +const ( + Visibility_VISIBILITY_UNSPECIFIED Visibility = 0 + Visibility_PRIVATE Visibility = 1 + Visibility_PROTECTED Visibility = 2 + Visibility_PUBLIC Visibility = 3 +) + +// Enum value maps for Visibility. +var ( + Visibility_name = map[int32]string{ + 0: "VISIBILITY_UNSPECIFIED", + 1: "PRIVATE", + 2: "PROTECTED", + 3: "PUBLIC", + } + Visibility_value = map[string]int32{ + "VISIBILITY_UNSPECIFIED": 0, + "PRIVATE": 1, + "PROTECTED": 2, + "PUBLIC": 3, + } +) + +func (x Visibility) Enum() *Visibility { + p := new(Visibility) + *p = x + return p +} + +func (x Visibility) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (Visibility) Descriptor() protoreflect.EnumDescriptor { + return file_api_v1_memo_service_proto_enumTypes[0].Descriptor() +} + +func (Visibility) Type() protoreflect.EnumType { + return &file_api_v1_memo_service_proto_enumTypes[0] +} + +func (x Visibility) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use Visibility.Descriptor instead. +func (Visibility) EnumDescriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{0} +} + +// The type of the relation. +type MemoRelation_Type int32 + +const ( + MemoRelation_TYPE_UNSPECIFIED MemoRelation_Type = 0 + MemoRelation_REFERENCE MemoRelation_Type = 1 + MemoRelation_COMMENT MemoRelation_Type = 2 +) + +// Enum value maps for MemoRelation_Type. +var ( + MemoRelation_Type_name = map[int32]string{ + 0: "TYPE_UNSPECIFIED", + 1: "REFERENCE", + 2: "COMMENT", + } + MemoRelation_Type_value = map[string]int32{ + "TYPE_UNSPECIFIED": 0, + "REFERENCE": 1, + "COMMENT": 2, + } +) + +func (x MemoRelation_Type) Enum() *MemoRelation_Type { + p := new(MemoRelation_Type) + *p = x + return p +} + +func (x MemoRelation_Type) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (MemoRelation_Type) Descriptor() protoreflect.EnumDescriptor { + return file_api_v1_memo_service_proto_enumTypes[1].Descriptor() +} + +func (MemoRelation_Type) Type() protoreflect.EnumType { + return &file_api_v1_memo_service_proto_enumTypes[1] +} + +func (x MemoRelation_Type) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use MemoRelation_Type.Descriptor instead. +func (MemoRelation_Type) EnumDescriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{14, 0} +} + +type Reaction struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The resource name of the reaction. + // Format: reactions/{reaction} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // The resource name of the creator. + // Format: users/{user} + Creator string `protobuf:"bytes,2,opt,name=creator,proto3" json:"creator,omitempty"` + // The resource name of the content. + // For memo reactions, this should be the memo's resource name. + // Format: memos/{memo} + ContentId string `protobuf:"bytes,3,opt,name=content_id,json=contentId,proto3" json:"content_id,omitempty"` + // Required. The type of reaction (e.g., "👍", "❤️", "😄"). + ReactionType string `protobuf:"bytes,4,opt,name=reaction_type,json=reactionType,proto3" json:"reaction_type,omitempty"` + // Output only. The creation timestamp. + CreateTime *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Reaction) Reset() { + *x = Reaction{} + mi := &file_api_v1_memo_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Reaction) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Reaction) ProtoMessage() {} + +func (x *Reaction) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Reaction.ProtoReflect.Descriptor instead. +func (*Reaction) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{0} +} + +func (x *Reaction) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Reaction) GetCreator() string { + if x != nil { + return x.Creator + } + return "" +} + +func (x *Reaction) GetContentId() string { + if x != nil { + return x.ContentId + } + return "" +} + +func (x *Reaction) GetReactionType() string { + if x != nil { + return x.ReactionType + } + return "" +} + +func (x *Reaction) GetCreateTime() *timestamppb.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +type Memo struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The resource name of the memo. + // Format: memos/{memo}, memo is the user defined id or uuid. + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // The state of the memo. + State State `protobuf:"varint,2,opt,name=state,proto3,enum=memos.api.v1.State" json:"state,omitempty"` + // The name of the creator. + // Format: users/{user} + Creator string `protobuf:"bytes,3,opt,name=creator,proto3" json:"creator,omitempty"` + // Output only. The creation timestamp. + CreateTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` + // Output only. The last update timestamp. + UpdateTime *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty"` + // The display timestamp of the memo. + DisplayTime *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=display_time,json=displayTime,proto3" json:"display_time,omitempty"` + // Required. The content of the memo in Markdown format. + Content string `protobuf:"bytes,7,opt,name=content,proto3" json:"content,omitempty"` + // Output only. The parsed nodes from the content. + Nodes []*Node `protobuf:"bytes,8,rep,name=nodes,proto3" json:"nodes,omitempty"` + // The visibility of the memo. + Visibility Visibility `protobuf:"varint,9,opt,name=visibility,proto3,enum=memos.api.v1.Visibility" json:"visibility,omitempty"` + // Output only. The tags extracted from the content. + Tags []string `protobuf:"bytes,10,rep,name=tags,proto3" json:"tags,omitempty"` + // Whether the memo is pinned. + Pinned bool `protobuf:"varint,11,opt,name=pinned,proto3" json:"pinned,omitempty"` + // Optional. The attachments of the memo. + Attachments []*Attachment `protobuf:"bytes,12,rep,name=attachments,proto3" json:"attachments,omitempty"` + // Optional. The relations of the memo. + Relations []*MemoRelation `protobuf:"bytes,13,rep,name=relations,proto3" json:"relations,omitempty"` + // Output only. The reactions to the memo. + Reactions []*Reaction `protobuf:"bytes,14,rep,name=reactions,proto3" json:"reactions,omitempty"` + // Output only. The computed properties of the memo. + Property *Memo_Property `protobuf:"bytes,15,opt,name=property,proto3" json:"property,omitempty"` + // Output only. The name of the parent memo. + // Format: memos/{memo} + Parent *string `protobuf:"bytes,16,opt,name=parent,proto3,oneof" json:"parent,omitempty"` + // Output only. The snippet of the memo content. Plain text only. + Snippet string `protobuf:"bytes,17,opt,name=snippet,proto3" json:"snippet,omitempty"` + // Optional. The location of the memo. + Location *Location `protobuf:"bytes,18,opt,name=location,proto3,oneof" json:"location,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Memo) Reset() { + *x = Memo{} + mi := &file_api_v1_memo_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Memo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Memo) ProtoMessage() {} + +func (x *Memo) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Memo.ProtoReflect.Descriptor instead. +func (*Memo) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{1} +} + +func (x *Memo) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Memo) GetState() State { + if x != nil { + return x.State + } + return State_STATE_UNSPECIFIED +} + +func (x *Memo) GetCreator() string { + if x != nil { + return x.Creator + } + return "" +} + +func (x *Memo) GetCreateTime() *timestamppb.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *Memo) GetUpdateTime() *timestamppb.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +func (x *Memo) GetDisplayTime() *timestamppb.Timestamp { + if x != nil { + return x.DisplayTime + } + return nil +} + +func (x *Memo) GetContent() string { + if x != nil { + return x.Content + } + return "" +} + +func (x *Memo) GetNodes() []*Node { + if x != nil { + return x.Nodes + } + return nil +} + +func (x *Memo) GetVisibility() Visibility { + if x != nil { + return x.Visibility + } + return Visibility_VISIBILITY_UNSPECIFIED +} + +func (x *Memo) GetTags() []string { + if x != nil { + return x.Tags + } + return nil +} + +func (x *Memo) GetPinned() bool { + if x != nil { + return x.Pinned + } + return false +} + +func (x *Memo) GetAttachments() []*Attachment { + if x != nil { + return x.Attachments + } + return nil +} + +func (x *Memo) GetRelations() []*MemoRelation { + if x != nil { + return x.Relations + } + return nil +} + +func (x *Memo) GetReactions() []*Reaction { + if x != nil { + return x.Reactions + } + return nil +} + +func (x *Memo) GetProperty() *Memo_Property { + if x != nil { + return x.Property + } + return nil +} + +func (x *Memo) GetParent() string { + if x != nil && x.Parent != nil { + return *x.Parent + } + return "" +} + +func (x *Memo) GetSnippet() string { + if x != nil { + return x.Snippet + } + return "" +} + +func (x *Memo) GetLocation() *Location { + if x != nil { + return x.Location + } + return nil +} + +type Location struct { + state protoimpl.MessageState `protogen:"open.v1"` + // A placeholder text for the location. + Placeholder string `protobuf:"bytes,1,opt,name=placeholder,proto3" json:"placeholder,omitempty"` + // The latitude of the location. + Latitude float64 `protobuf:"fixed64,2,opt,name=latitude,proto3" json:"latitude,omitempty"` + // The longitude of the location. + Longitude float64 `protobuf:"fixed64,3,opt,name=longitude,proto3" json:"longitude,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Location) Reset() { + *x = Location{} + mi := &file_api_v1_memo_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Location) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Location) ProtoMessage() {} + +func (x *Location) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Location.ProtoReflect.Descriptor instead. +func (*Location) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{2} +} + +func (x *Location) GetPlaceholder() string { + if x != nil { + return x.Placeholder + } + return "" +} + +func (x *Location) GetLatitude() float64 { + if x != nil { + return x.Latitude + } + return 0 +} + +func (x *Location) GetLongitude() float64 { + if x != nil { + return x.Longitude + } + return 0 +} + +type CreateMemoRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The memo to create. + Memo *Memo `protobuf:"bytes,1,opt,name=memo,proto3" json:"memo,omitempty"` + // Optional. The memo ID to use for this memo. + // If empty, a unique ID will be generated. + MemoId string `protobuf:"bytes,2,opt,name=memo_id,json=memoId,proto3" json:"memo_id,omitempty"` + // Optional. If set, validate the request but don't actually create the memo. + ValidateOnly bool `protobuf:"varint,3,opt,name=validate_only,json=validateOnly,proto3" json:"validate_only,omitempty"` + // Optional. An idempotency token. + RequestId string `protobuf:"bytes,4,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateMemoRequest) Reset() { + *x = CreateMemoRequest{} + mi := &file_api_v1_memo_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateMemoRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateMemoRequest) ProtoMessage() {} + +func (x *CreateMemoRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateMemoRequest.ProtoReflect.Descriptor instead. +func (*CreateMemoRequest) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{3} +} + +func (x *CreateMemoRequest) GetMemo() *Memo { + if x != nil { + return x.Memo + } + return nil +} + +func (x *CreateMemoRequest) GetMemoId() string { + if x != nil { + return x.MemoId + } + return "" +} + +func (x *CreateMemoRequest) GetValidateOnly() bool { + if x != nil { + return x.ValidateOnly + } + return false +} + +func (x *CreateMemoRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +type ListMemosRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional. The parent is the owner of the memos. + // If not specified or `users/-`, it will list all memos. + // Format: users/{user} + Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"` + // Optional. The maximum number of memos to return. + // The service may return fewer than this value. + // If unspecified, at most 50 memos will be returned. + // The maximum value is 1000; values above 1000 will be coerced to 1000. + PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Optional. A page token, received from a previous `ListMemos` call. + // Provide this to retrieve the subsequent page. + PageToken string `protobuf:"bytes,3,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + // Optional. The state of the memos to list. + // Default to `NORMAL`. Set to `ARCHIVED` to list archived memos. + State State `protobuf:"varint,4,opt,name=state,proto3,enum=memos.api.v1.State" json:"state,omitempty"` + // Optional. The order to sort results by. + // Default to "display_time desc". + // Example: "display_time desc" or "create_time asc" + OrderBy string `protobuf:"bytes,5,opt,name=order_by,json=orderBy,proto3" json:"order_by,omitempty"` + // Optional. Filter to apply to the list results. + // Filter is a CEL expression to filter memos. + // Refer to `Shortcut.filter`. + Filter string `protobuf:"bytes,6,opt,name=filter,proto3" json:"filter,omitempty"` + // Optional. If true, show deleted memos in the response. + ShowDeleted bool `protobuf:"varint,7,opt,name=show_deleted,json=showDeleted,proto3" json:"show_deleted,omitempty"` + // [Deprecated] Old filter contains some specific conditions to filter memos. + // Format: "creator == 'users/{user}' && visibilities == ['PUBLIC', 'PROTECTED']" + OldFilter string `protobuf:"bytes,8,opt,name=old_filter,json=oldFilter,proto3" json:"old_filter,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListMemosRequest) Reset() { + *x = ListMemosRequest{} + mi := &file_api_v1_memo_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListMemosRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListMemosRequest) ProtoMessage() {} + +func (x *ListMemosRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListMemosRequest.ProtoReflect.Descriptor instead. +func (*ListMemosRequest) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{4} +} + +func (x *ListMemosRequest) GetParent() string { + if x != nil { + return x.Parent + } + return "" +} + +func (x *ListMemosRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListMemosRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +func (x *ListMemosRequest) GetState() State { + if x != nil { + return x.State + } + return State_STATE_UNSPECIFIED +} + +func (x *ListMemosRequest) GetOrderBy() string { + if x != nil { + return x.OrderBy + } + return "" +} + +func (x *ListMemosRequest) GetFilter() string { + if x != nil { + return x.Filter + } + return "" +} + +func (x *ListMemosRequest) GetShowDeleted() bool { + if x != nil { + return x.ShowDeleted + } + return false +} + +func (x *ListMemosRequest) GetOldFilter() string { + if x != nil { + return x.OldFilter + } + return "" +} + +type ListMemosResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The list of memos. + Memos []*Memo `protobuf:"bytes,1,rep,name=memos,proto3" json:"memos,omitempty"` + // A token that can be sent as `page_token` to retrieve the next page. + // If this field is omitted, there are no subsequent pages. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + // The total count of memos (may be approximate). + TotalSize int32 `protobuf:"varint,3,opt,name=total_size,json=totalSize,proto3" json:"total_size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListMemosResponse) Reset() { + *x = ListMemosResponse{} + mi := &file_api_v1_memo_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListMemosResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListMemosResponse) ProtoMessage() {} + +func (x *ListMemosResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListMemosResponse.ProtoReflect.Descriptor instead. +func (*ListMemosResponse) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{5} +} + +func (x *ListMemosResponse) GetMemos() []*Memo { + if x != nil { + return x.Memos + } + return nil +} + +func (x *ListMemosResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +func (x *ListMemosResponse) GetTotalSize() int32 { + if x != nil { + return x.TotalSize + } + return 0 +} + +type GetMemoRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The resource name of the memo. + // Format: memos/{memo} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Optional. The fields to return in the response. + // If not specified, all fields are returned. + ReadMask *fieldmaskpb.FieldMask `protobuf:"bytes,2,opt,name=read_mask,json=readMask,proto3" json:"read_mask,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetMemoRequest) Reset() { + *x = GetMemoRequest{} + mi := &file_api_v1_memo_service_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetMemoRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetMemoRequest) ProtoMessage() {} + +func (x *GetMemoRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetMemoRequest.ProtoReflect.Descriptor instead. +func (*GetMemoRequest) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{6} +} + +func (x *GetMemoRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *GetMemoRequest) GetReadMask() *fieldmaskpb.FieldMask { + if x != nil { + return x.ReadMask + } + return nil +} + +type UpdateMemoRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The memo to update. + // The `name` field is required. + Memo *Memo `protobuf:"bytes,1,opt,name=memo,proto3" json:"memo,omitempty"` + // Required. The list of fields to update. + UpdateMask *fieldmaskpb.FieldMask `protobuf:"bytes,2,opt,name=update_mask,json=updateMask,proto3" json:"update_mask,omitempty"` + // Optional. If set to true, allows updating sensitive fields. + AllowMissing bool `protobuf:"varint,3,opt,name=allow_missing,json=allowMissing,proto3" json:"allow_missing,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateMemoRequest) Reset() { + *x = UpdateMemoRequest{} + mi := &file_api_v1_memo_service_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateMemoRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateMemoRequest) ProtoMessage() {} + +func (x *UpdateMemoRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateMemoRequest.ProtoReflect.Descriptor instead. +func (*UpdateMemoRequest) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{7} +} + +func (x *UpdateMemoRequest) GetMemo() *Memo { + if x != nil { + return x.Memo + } + return nil +} + +func (x *UpdateMemoRequest) GetUpdateMask() *fieldmaskpb.FieldMask { + if x != nil { + return x.UpdateMask + } + return nil +} + +func (x *UpdateMemoRequest) GetAllowMissing() bool { + if x != nil { + return x.AllowMissing + } + return false +} + +type DeleteMemoRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The resource name of the memo to delete. + // Format: memos/{memo} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Optional. If set to true, the memo will be deleted even if it has associated data. + Force bool `protobuf:"varint,2,opt,name=force,proto3" json:"force,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteMemoRequest) Reset() { + *x = DeleteMemoRequest{} + mi := &file_api_v1_memo_service_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteMemoRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteMemoRequest) ProtoMessage() {} + +func (x *DeleteMemoRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteMemoRequest.ProtoReflect.Descriptor instead. +func (*DeleteMemoRequest) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{8} +} + +func (x *DeleteMemoRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *DeleteMemoRequest) GetForce() bool { + if x != nil { + return x.Force + } + return false +} + +type RenameMemoTagRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The parent, who owns the tags. + // Format: memos/{memo}. Use "memos/-" to rename all tags. + Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"` + // Required. The old tag name to rename. + OldTag string `protobuf:"bytes,2,opt,name=old_tag,json=oldTag,proto3" json:"old_tag,omitempty"` + // Required. The new tag name. + NewTag string `protobuf:"bytes,3,opt,name=new_tag,json=newTag,proto3" json:"new_tag,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RenameMemoTagRequest) Reset() { + *x = RenameMemoTagRequest{} + mi := &file_api_v1_memo_service_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RenameMemoTagRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RenameMemoTagRequest) ProtoMessage() {} + +func (x *RenameMemoTagRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RenameMemoTagRequest.ProtoReflect.Descriptor instead. +func (*RenameMemoTagRequest) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{9} +} + +func (x *RenameMemoTagRequest) GetParent() string { + if x != nil { + return x.Parent + } + return "" +} + +func (x *RenameMemoTagRequest) GetOldTag() string { + if x != nil { + return x.OldTag + } + return "" +} + +func (x *RenameMemoTagRequest) GetNewTag() string { + if x != nil { + return x.NewTag + } + return "" +} + +type DeleteMemoTagRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The parent, who owns the tags. + // Format: memos/{memo}. Use "memos/-" to delete all tags. + Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"` + // Required. The tag name to delete. + Tag string `protobuf:"bytes,2,opt,name=tag,proto3" json:"tag,omitempty"` + // Optional. Whether to delete related memos. + DeleteRelatedMemos bool `protobuf:"varint,3,opt,name=delete_related_memos,json=deleteRelatedMemos,proto3" json:"delete_related_memos,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteMemoTagRequest) Reset() { + *x = DeleteMemoTagRequest{} + mi := &file_api_v1_memo_service_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteMemoTagRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteMemoTagRequest) ProtoMessage() {} + +func (x *DeleteMemoTagRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteMemoTagRequest.ProtoReflect.Descriptor instead. +func (*DeleteMemoTagRequest) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{10} +} + +func (x *DeleteMemoTagRequest) GetParent() string { + if x != nil { + return x.Parent + } + return "" +} + +func (x *DeleteMemoTagRequest) GetTag() string { + if x != nil { + return x.Tag + } + return "" +} + +func (x *DeleteMemoTagRequest) GetDeleteRelatedMemos() bool { + if x != nil { + return x.DeleteRelatedMemos + } + return false +} + +type SetMemoAttachmentsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The resource name of the memo. + // Format: memos/{memo} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Required. The attachments to set for the memo. + Attachments []*Attachment `protobuf:"bytes,2,rep,name=attachments,proto3" json:"attachments,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetMemoAttachmentsRequest) Reset() { + *x = SetMemoAttachmentsRequest{} + mi := &file_api_v1_memo_service_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetMemoAttachmentsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetMemoAttachmentsRequest) ProtoMessage() {} + +func (x *SetMemoAttachmentsRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetMemoAttachmentsRequest.ProtoReflect.Descriptor instead. +func (*SetMemoAttachmentsRequest) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{11} +} + +func (x *SetMemoAttachmentsRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *SetMemoAttachmentsRequest) GetAttachments() []*Attachment { + if x != nil { + return x.Attachments + } + return nil +} + +type ListMemoAttachmentsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The resource name of the memo. + // Format: memos/{memo} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Optional. The maximum number of attachments to return. + PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Optional. A page token for pagination. + PageToken string `protobuf:"bytes,3,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListMemoAttachmentsRequest) Reset() { + *x = ListMemoAttachmentsRequest{} + mi := &file_api_v1_memo_service_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListMemoAttachmentsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListMemoAttachmentsRequest) ProtoMessage() {} + +func (x *ListMemoAttachmentsRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListMemoAttachmentsRequest.ProtoReflect.Descriptor instead. +func (*ListMemoAttachmentsRequest) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{12} +} + +func (x *ListMemoAttachmentsRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ListMemoAttachmentsRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListMemoAttachmentsRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +type ListMemoAttachmentsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The list of attachments. + Attachments []*Attachment `protobuf:"bytes,1,rep,name=attachments,proto3" json:"attachments,omitempty"` + // A token for the next page of results. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + // The total count of attachments. + TotalSize int32 `protobuf:"varint,3,opt,name=total_size,json=totalSize,proto3" json:"total_size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListMemoAttachmentsResponse) Reset() { + *x = ListMemoAttachmentsResponse{} + mi := &file_api_v1_memo_service_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListMemoAttachmentsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListMemoAttachmentsResponse) ProtoMessage() {} + +func (x *ListMemoAttachmentsResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListMemoAttachmentsResponse.ProtoReflect.Descriptor instead. +func (*ListMemoAttachmentsResponse) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{13} +} + +func (x *ListMemoAttachmentsResponse) GetAttachments() []*Attachment { + if x != nil { + return x.Attachments + } + return nil +} + +func (x *ListMemoAttachmentsResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +func (x *ListMemoAttachmentsResponse) GetTotalSize() int32 { + if x != nil { + return x.TotalSize + } + return 0 +} + +type MemoRelation struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The memo in the relation. + Memo *MemoRelation_Memo `protobuf:"bytes,1,opt,name=memo,proto3" json:"memo,omitempty"` + // The related memo. + RelatedMemo *MemoRelation_Memo `protobuf:"bytes,2,opt,name=related_memo,json=relatedMemo,proto3" json:"related_memo,omitempty"` + Type MemoRelation_Type `protobuf:"varint,3,opt,name=type,proto3,enum=memos.api.v1.MemoRelation_Type" json:"type,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MemoRelation) Reset() { + *x = MemoRelation{} + mi := &file_api_v1_memo_service_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MemoRelation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MemoRelation) ProtoMessage() {} + +func (x *MemoRelation) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MemoRelation.ProtoReflect.Descriptor instead. +func (*MemoRelation) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{14} +} + +func (x *MemoRelation) GetMemo() *MemoRelation_Memo { + if x != nil { + return x.Memo + } + return nil +} + +func (x *MemoRelation) GetRelatedMemo() *MemoRelation_Memo { + if x != nil { + return x.RelatedMemo + } + return nil +} + +func (x *MemoRelation) GetType() MemoRelation_Type { + if x != nil { + return x.Type + } + return MemoRelation_TYPE_UNSPECIFIED +} + +type SetMemoRelationsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The resource name of the memo. + // Format: memos/{memo} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Required. The relations to set for the memo. + Relations []*MemoRelation `protobuf:"bytes,2,rep,name=relations,proto3" json:"relations,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SetMemoRelationsRequest) Reset() { + *x = SetMemoRelationsRequest{} + mi := &file_api_v1_memo_service_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SetMemoRelationsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SetMemoRelationsRequest) ProtoMessage() {} + +func (x *SetMemoRelationsRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SetMemoRelationsRequest.ProtoReflect.Descriptor instead. +func (*SetMemoRelationsRequest) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{15} +} + +func (x *SetMemoRelationsRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *SetMemoRelationsRequest) GetRelations() []*MemoRelation { + if x != nil { + return x.Relations + } + return nil +} + +type ListMemoRelationsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The resource name of the memo. + // Format: memos/{memo} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Optional. The maximum number of relations to return. + PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Optional. A page token for pagination. + PageToken string `protobuf:"bytes,3,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListMemoRelationsRequest) Reset() { + *x = ListMemoRelationsRequest{} + mi := &file_api_v1_memo_service_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListMemoRelationsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListMemoRelationsRequest) ProtoMessage() {} + +func (x *ListMemoRelationsRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListMemoRelationsRequest.ProtoReflect.Descriptor instead. +func (*ListMemoRelationsRequest) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{16} +} + +func (x *ListMemoRelationsRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ListMemoRelationsRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListMemoRelationsRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +type ListMemoRelationsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The list of relations. + Relations []*MemoRelation `protobuf:"bytes,1,rep,name=relations,proto3" json:"relations,omitempty"` + // A token for the next page of results. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + // The total count of relations. + TotalSize int32 `protobuf:"varint,3,opt,name=total_size,json=totalSize,proto3" json:"total_size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListMemoRelationsResponse) Reset() { + *x = ListMemoRelationsResponse{} + mi := &file_api_v1_memo_service_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListMemoRelationsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListMemoRelationsResponse) ProtoMessage() {} + +func (x *ListMemoRelationsResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListMemoRelationsResponse.ProtoReflect.Descriptor instead. +func (*ListMemoRelationsResponse) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{17} +} + +func (x *ListMemoRelationsResponse) GetRelations() []*MemoRelation { + if x != nil { + return x.Relations + } + return nil +} + +func (x *ListMemoRelationsResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +func (x *ListMemoRelationsResponse) GetTotalSize() int32 { + if x != nil { + return x.TotalSize + } + return 0 +} + +type CreateMemoCommentRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The resource name of the memo. + // Format: memos/{memo} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Required. The comment to create. + Comment *Memo `protobuf:"bytes,2,opt,name=comment,proto3" json:"comment,omitempty"` + // Optional. The comment ID to use. + CommentId string `protobuf:"bytes,3,opt,name=comment_id,json=commentId,proto3" json:"comment_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateMemoCommentRequest) Reset() { + *x = CreateMemoCommentRequest{} + mi := &file_api_v1_memo_service_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateMemoCommentRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateMemoCommentRequest) ProtoMessage() {} + +func (x *CreateMemoCommentRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateMemoCommentRequest.ProtoReflect.Descriptor instead. +func (*CreateMemoCommentRequest) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{18} +} + +func (x *CreateMemoCommentRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CreateMemoCommentRequest) GetComment() *Memo { + if x != nil { + return x.Comment + } + return nil +} + +func (x *CreateMemoCommentRequest) GetCommentId() string { + if x != nil { + return x.CommentId + } + return "" +} + +type ListMemoCommentsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The resource name of the memo. + // Format: memos/{memo} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Optional. The maximum number of comments to return. + PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Optional. A page token for pagination. + PageToken string `protobuf:"bytes,3,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + // Optional. The order to sort results by. + OrderBy string `protobuf:"bytes,4,opt,name=order_by,json=orderBy,proto3" json:"order_by,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListMemoCommentsRequest) Reset() { + *x = ListMemoCommentsRequest{} + mi := &file_api_v1_memo_service_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListMemoCommentsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListMemoCommentsRequest) ProtoMessage() {} + +func (x *ListMemoCommentsRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListMemoCommentsRequest.ProtoReflect.Descriptor instead. +func (*ListMemoCommentsRequest) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{19} +} + +func (x *ListMemoCommentsRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ListMemoCommentsRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListMemoCommentsRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +func (x *ListMemoCommentsRequest) GetOrderBy() string { + if x != nil { + return x.OrderBy + } + return "" +} + +type ListMemoCommentsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The list of comment memos. + Memos []*Memo `protobuf:"bytes,1,rep,name=memos,proto3" json:"memos,omitempty"` + // A token for the next page of results. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + // The total count of comments. + TotalSize int32 `protobuf:"varint,3,opt,name=total_size,json=totalSize,proto3" json:"total_size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListMemoCommentsResponse) Reset() { + *x = ListMemoCommentsResponse{} + mi := &file_api_v1_memo_service_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListMemoCommentsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListMemoCommentsResponse) ProtoMessage() {} + +func (x *ListMemoCommentsResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListMemoCommentsResponse.ProtoReflect.Descriptor instead. +func (*ListMemoCommentsResponse) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{20} +} + +func (x *ListMemoCommentsResponse) GetMemos() []*Memo { + if x != nil { + return x.Memos + } + return nil +} + +func (x *ListMemoCommentsResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +func (x *ListMemoCommentsResponse) GetTotalSize() int32 { + if x != nil { + return x.TotalSize + } + return 0 +} + +type ListMemoReactionsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The resource name of the memo. + // Format: memos/{memo} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Optional. The maximum number of reactions to return. + PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Optional. A page token for pagination. + PageToken string `protobuf:"bytes,3,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListMemoReactionsRequest) Reset() { + *x = ListMemoReactionsRequest{} + mi := &file_api_v1_memo_service_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListMemoReactionsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListMemoReactionsRequest) ProtoMessage() {} + +func (x *ListMemoReactionsRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListMemoReactionsRequest.ProtoReflect.Descriptor instead. +func (*ListMemoReactionsRequest) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{21} +} + +func (x *ListMemoReactionsRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *ListMemoReactionsRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListMemoReactionsRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +type ListMemoReactionsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The list of reactions. + Reactions []*Reaction `protobuf:"bytes,1,rep,name=reactions,proto3" json:"reactions,omitempty"` + // A token for the next page of results. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + // The total count of reactions. + TotalSize int32 `protobuf:"varint,3,opt,name=total_size,json=totalSize,proto3" json:"total_size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListMemoReactionsResponse) Reset() { + *x = ListMemoReactionsResponse{} + mi := &file_api_v1_memo_service_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListMemoReactionsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListMemoReactionsResponse) ProtoMessage() {} + +func (x *ListMemoReactionsResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListMemoReactionsResponse.ProtoReflect.Descriptor instead. +func (*ListMemoReactionsResponse) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{22} +} + +func (x *ListMemoReactionsResponse) GetReactions() []*Reaction { + if x != nil { + return x.Reactions + } + return nil +} + +func (x *ListMemoReactionsResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +func (x *ListMemoReactionsResponse) GetTotalSize() int32 { + if x != nil { + return x.TotalSize + } + return 0 +} + +type UpsertMemoReactionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The resource name of the memo. + // Format: memos/{memo} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Required. The reaction to upsert. + Reaction *Reaction `protobuf:"bytes,2,opt,name=reaction,proto3" json:"reaction,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpsertMemoReactionRequest) Reset() { + *x = UpsertMemoReactionRequest{} + mi := &file_api_v1_memo_service_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpsertMemoReactionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpsertMemoReactionRequest) ProtoMessage() {} + +func (x *UpsertMemoReactionRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpsertMemoReactionRequest.ProtoReflect.Descriptor instead. +func (*UpsertMemoReactionRequest) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{23} +} + +func (x *UpsertMemoReactionRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *UpsertMemoReactionRequest) GetReaction() *Reaction { + if x != nil { + return x.Reaction + } + return nil +} + +type DeleteMemoReactionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The resource name of the reaction to delete. + // Format: reactions/{reaction} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteMemoReactionRequest) Reset() { + *x = DeleteMemoReactionRequest{} + mi := &file_api_v1_memo_service_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteMemoReactionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteMemoReactionRequest) ProtoMessage() {} + +func (x *DeleteMemoReactionRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteMemoReactionRequest.ProtoReflect.Descriptor instead. +func (*DeleteMemoReactionRequest) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{24} +} + +func (x *DeleteMemoReactionRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type ExportMemosRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional. Format for the export (currently only "json" is supported) + Format string `protobuf:"bytes,1,opt,name=format,proto3" json:"format,omitempty"` + // Optional. Filter to apply to memos for export + // Uses the same filter format as ListMemosRequest + Filter string `protobuf:"bytes,2,opt,name=filter,proto3" json:"filter,omitempty"` + // Optional. Whether to exclude archived memos from export + // Default: false (include archived memos) + ExcludeArchived bool `protobuf:"varint,3,opt,name=exclude_archived,json=excludeArchived,proto3" json:"exclude_archived,omitempty"` + // Optional. Whether to include attachments in the export + // Default: true + IncludeAttachments bool `protobuf:"varint,4,opt,name=include_attachments,json=includeAttachments,proto3" json:"include_attachments,omitempty"` + // Optional. Whether to include memo relations in the export + // Default: true + IncludeRelations bool `protobuf:"varint,5,opt,name=include_relations,json=includeRelations,proto3" json:"include_relations,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExportMemosRequest) Reset() { + *x = ExportMemosRequest{} + mi := &file_api_v1_memo_service_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExportMemosRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExportMemosRequest) ProtoMessage() {} + +func (x *ExportMemosRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExportMemosRequest.ProtoReflect.Descriptor instead. +func (*ExportMemosRequest) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{25} +} + +func (x *ExportMemosRequest) GetFormat() string { + if x != nil { + return x.Format + } + return "" +} + +func (x *ExportMemosRequest) GetFilter() string { + if x != nil { + return x.Filter + } + return "" +} + +func (x *ExportMemosRequest) GetExcludeArchived() bool { + if x != nil { + return x.ExcludeArchived + } + return false +} + +func (x *ExportMemosRequest) GetIncludeAttachments() bool { + if x != nil { + return x.IncludeAttachments + } + return false +} + +func (x *ExportMemosRequest) GetIncludeRelations() bool { + if x != nil { + return x.IncludeRelations + } + return false +} + +type ExportMemosResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The exported data as bytes + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + // The format of the exported data + Format string `protobuf:"bytes,2,opt,name=format,proto3" json:"format,omitempty"` + // Suggested filename for the export + Filename string `protobuf:"bytes,3,opt,name=filename,proto3" json:"filename,omitempty"` + // Number of memos exported + MemoCount int32 `protobuf:"varint,4,opt,name=memo_count,json=memoCount,proto3" json:"memo_count,omitempty"` + // Size of the export data in bytes + SizeBytes int64 `protobuf:"varint,5,opt,name=size_bytes,json=sizeBytes,proto3" json:"size_bytes,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ExportMemosResponse) Reset() { + *x = ExportMemosResponse{} + mi := &file_api_v1_memo_service_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ExportMemosResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ExportMemosResponse) ProtoMessage() {} + +func (x *ExportMemosResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ExportMemosResponse.ProtoReflect.Descriptor instead. +func (*ExportMemosResponse) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{26} +} + +func (x *ExportMemosResponse) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +func (x *ExportMemosResponse) GetFormat() string { + if x != nil { + return x.Format + } + return "" +} + +func (x *ExportMemosResponse) GetFilename() string { + if x != nil { + return x.Filename + } + return "" +} + +func (x *ExportMemosResponse) GetMemoCount() int32 { + if x != nil { + return x.MemoCount + } + return 0 +} + +func (x *ExportMemosResponse) GetSizeBytes() int64 { + if x != nil { + return x.SizeBytes + } + return 0 +} + +type ImportMemosRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The data to import (JSON format) + Data []byte `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"` + // Optional. Format of the import data (currently only "json" is supported) + Format string `protobuf:"bytes,2,opt,name=format,proto3" json:"format,omitempty"` + // Optional. Whether to overwrite existing memos with the same UID + // Default: false (skip existing memos) + OverwriteExisting bool `protobuf:"varint,3,opt,name=overwrite_existing,json=overwriteExisting,proto3" json:"overwrite_existing,omitempty"` + // Optional. Whether to validate only (dry run mode) + // If true, the import will be validated but no data will be created + ValidateOnly bool `protobuf:"varint,4,opt,name=validate_only,json=validateOnly,proto3" json:"validate_only,omitempty"` + // Optional. Whether to preserve original timestamps + // Default: true + PreserveTimestamps bool `protobuf:"varint,5,opt,name=preserve_timestamps,json=preserveTimestamps,proto3" json:"preserve_timestamps,omitempty"` + // Optional. Whether to skip importing attachments + // Default: false (import attachments if present) + SkipAttachments bool `protobuf:"varint,6,opt,name=skip_attachments,json=skipAttachments,proto3" json:"skip_attachments,omitempty"` + // Optional. Whether to skip importing memo relations + // Default: false (import relations if present) + SkipRelations bool `protobuf:"varint,7,opt,name=skip_relations,json=skipRelations,proto3" json:"skip_relations,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ImportMemosRequest) Reset() { + *x = ImportMemosRequest{} + mi := &file_api_v1_memo_service_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ImportMemosRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ImportMemosRequest) ProtoMessage() {} + +func (x *ImportMemosRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ImportMemosRequest.ProtoReflect.Descriptor instead. +func (*ImportMemosRequest) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{27} +} + +func (x *ImportMemosRequest) GetData() []byte { + if x != nil { + return x.Data + } + return nil +} + +func (x *ImportMemosRequest) GetFormat() string { + if x != nil { + return x.Format + } + return "" +} + +func (x *ImportMemosRequest) GetOverwriteExisting() bool { + if x != nil { + return x.OverwriteExisting + } + return false +} + +func (x *ImportMemosRequest) GetValidateOnly() bool { + if x != nil { + return x.ValidateOnly + } + return false +} + +func (x *ImportMemosRequest) GetPreserveTimestamps() bool { + if x != nil { + return x.PreserveTimestamps + } + return false +} + +func (x *ImportMemosRequest) GetSkipAttachments() bool { + if x != nil { + return x.SkipAttachments + } + return false +} + +func (x *ImportMemosRequest) GetSkipRelations() bool { + if x != nil { + return x.SkipRelations + } + return false +} + +type ImportMemosResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Number of memos successfully imported + ImportedCount int32 `protobuf:"varint,1,opt,name=imported_count,json=importedCount,proto3" json:"imported_count,omitempty"` + // Number of memos skipped (due to errors or existing UIDs) + SkippedCount int32 `protobuf:"varint,2,opt,name=skipped_count,json=skippedCount,proto3" json:"skipped_count,omitempty"` + // Number of memos that failed validation (in validate_only mode) + ValidationErrors int32 `protobuf:"varint,3,opt,name=validation_errors,json=validationErrors,proto3" json:"validation_errors,omitempty"` + // List of error messages for failed imports + Errors []string `protobuf:"bytes,4,rep,name=errors,proto3" json:"errors,omitempty"` + // List of warning messages for potential issues + Warnings []string `protobuf:"bytes,5,rep,name=warnings,proto3" json:"warnings,omitempty"` + // Summary of the import operation + Summary *ImportSummary `protobuf:"bytes,6,opt,name=summary,proto3" json:"summary,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ImportMemosResponse) Reset() { + *x = ImportMemosResponse{} + mi := &file_api_v1_memo_service_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ImportMemosResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ImportMemosResponse) ProtoMessage() {} + +func (x *ImportMemosResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ImportMemosResponse.ProtoReflect.Descriptor instead. +func (*ImportMemosResponse) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{28} +} + +func (x *ImportMemosResponse) GetImportedCount() int32 { + if x != nil { + return x.ImportedCount + } + return 0 +} + +func (x *ImportMemosResponse) GetSkippedCount() int32 { + if x != nil { + return x.SkippedCount + } + return 0 +} + +func (x *ImportMemosResponse) GetValidationErrors() int32 { + if x != nil { + return x.ValidationErrors + } + return 0 +} + +func (x *ImportMemosResponse) GetErrors() []string { + if x != nil { + return x.Errors + } + return nil +} + +func (x *ImportMemosResponse) GetWarnings() []string { + if x != nil { + return x.Warnings + } + return nil +} + +func (x *ImportMemosResponse) GetSummary() *ImportSummary { + if x != nil { + return x.Summary + } + return nil +} + +type ImportSummary struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Total number of memos in the import data + TotalMemos int32 `protobuf:"varint,1,opt,name=total_memos,json=totalMemos,proto3" json:"total_memos,omitempty"` + // Number of new memos created + CreatedCount int32 `protobuf:"varint,2,opt,name=created_count,json=createdCount,proto3" json:"created_count,omitempty"` + // Number of existing memos updated + UpdatedCount int32 `protobuf:"varint,3,opt,name=updated_count,json=updatedCount,proto3" json:"updated_count,omitempty"` + // Number of attachments imported + AttachmentsImported int32 `protobuf:"varint,4,opt,name=attachments_imported,json=attachmentsImported,proto3" json:"attachments_imported,omitempty"` + // Number of relations imported + RelationsImported int32 `protobuf:"varint,5,opt,name=relations_imported,json=relationsImported,proto3" json:"relations_imported,omitempty"` + // Import duration in milliseconds + DurationMs int64 `protobuf:"varint,6,opt,name=duration_ms,json=durationMs,proto3" json:"duration_ms,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ImportSummary) Reset() { + *x = ImportSummary{} + mi := &file_api_v1_memo_service_proto_msgTypes[29] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ImportSummary) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ImportSummary) ProtoMessage() {} + +func (x *ImportSummary) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[29] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ImportSummary.ProtoReflect.Descriptor instead. +func (*ImportSummary) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{29} +} + +func (x *ImportSummary) GetTotalMemos() int32 { + if x != nil { + return x.TotalMemos + } + return 0 +} + +func (x *ImportSummary) GetCreatedCount() int32 { + if x != nil { + return x.CreatedCount + } + return 0 +} + +func (x *ImportSummary) GetUpdatedCount() int32 { + if x != nil { + return x.UpdatedCount + } + return 0 +} + +func (x *ImportSummary) GetAttachmentsImported() int32 { + if x != nil { + return x.AttachmentsImported + } + return 0 +} + +func (x *ImportSummary) GetRelationsImported() int32 { + if x != nil { + return x.RelationsImported + } + return 0 +} + +func (x *ImportSummary) GetDurationMs() int64 { + if x != nil { + return x.DurationMs + } + return 0 +} + +// Computed properties of a memo. +type Memo_Property struct { + state protoimpl.MessageState `protogen:"open.v1"` + HasLink bool `protobuf:"varint,1,opt,name=has_link,json=hasLink,proto3" json:"has_link,omitempty"` + HasTaskList bool `protobuf:"varint,2,opt,name=has_task_list,json=hasTaskList,proto3" json:"has_task_list,omitempty"` + HasCode bool `protobuf:"varint,3,opt,name=has_code,json=hasCode,proto3" json:"has_code,omitempty"` + HasIncompleteTasks bool `protobuf:"varint,4,opt,name=has_incomplete_tasks,json=hasIncompleteTasks,proto3" json:"has_incomplete_tasks,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Memo_Property) Reset() { + *x = Memo_Property{} + mi := &file_api_v1_memo_service_proto_msgTypes[30] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Memo_Property) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Memo_Property) ProtoMessage() {} + +func (x *Memo_Property) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[30] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Memo_Property.ProtoReflect.Descriptor instead. +func (*Memo_Property) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{1, 0} +} + +func (x *Memo_Property) GetHasLink() bool { + if x != nil { + return x.HasLink + } + return false +} + +func (x *Memo_Property) GetHasTaskList() bool { + if x != nil { + return x.HasTaskList + } + return false +} + +func (x *Memo_Property) GetHasCode() bool { + if x != nil { + return x.HasCode + } + return false +} + +func (x *Memo_Property) GetHasIncompleteTasks() bool { + if x != nil { + return x.HasIncompleteTasks + } + return false +} + +// Memo reference in relations. +type MemoRelation_Memo struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The resource name of the memo. + // Format: memos/{memo} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Output only. The snippet of the memo content. Plain text only. + Snippet string `protobuf:"bytes,2,opt,name=snippet,proto3" json:"snippet,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MemoRelation_Memo) Reset() { + *x = MemoRelation_Memo{} + mi := &file_api_v1_memo_service_proto_msgTypes[31] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MemoRelation_Memo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MemoRelation_Memo) ProtoMessage() {} + +func (x *MemoRelation_Memo) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_memo_service_proto_msgTypes[31] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MemoRelation_Memo.ProtoReflect.Descriptor instead. +func (*MemoRelation_Memo) Descriptor() ([]byte, []int) { + return file_api_v1_memo_service_proto_rawDescGZIP(), []int{14, 0} +} + +func (x *MemoRelation_Memo) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *MemoRelation_Memo) GetSnippet() string { + if x != nil { + return x.Snippet + } + return "" +} + +var File_api_v1_memo_service_proto protoreflect.FileDescriptor + +const file_api_v1_memo_service_proto_rawDesc = "" + + "\n" + + "\x19api/v1/memo_service.proto\x12\fmemos.api.v1\x1a\x1fapi/v1/attachment_service.proto\x1a\x13api/v1/common.proto\x1a\x1dapi/v1/markdown_service.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xce\x02\n" + + "\bReaction\x12\x1a\n" + + "\x04name\x18\x01 \x01(\tB\x06\xe0A\x03\xe0A\bR\x04name\x123\n" + + "\acreator\x18\x02 \x01(\tB\x19\xe0A\x03\xfaA\x13\n" + + "\x11memos.api.v1/UserR\acreator\x128\n" + + "\n" + + "content_id\x18\x03 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" + + "\x11memos.api.v1/MemoR\tcontentId\x12(\n" + + "\rreaction_type\x18\x04 \x01(\tB\x03\xe0A\x02R\freactionType\x12@\n" + + "\vcreate_time\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\n" + + "createTime:K\xeaAH\n" + + "\x15memos.api.v1/Reaction\x12\x14reactions/{reaction}\x1a\x04name*\treactions2\breaction\"\x87\t\n" + + "\x04Memo\x12\x17\n" + + "\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12.\n" + + "\x05state\x18\x02 \x01(\x0e2\x13.memos.api.v1.StateB\x03\xe0A\x02R\x05state\x123\n" + + "\acreator\x18\x03 \x01(\tB\x19\xe0A\x03\xfaA\x13\n" + + "\x11memos.api.v1/UserR\acreator\x12@\n" + + "\vcreate_time\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\n" + + "createTime\x12@\n" + + "\vupdate_time\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\n" + + "updateTime\x12B\n" + + "\fdisplay_time\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x01R\vdisplayTime\x12\x1d\n" + + "\acontent\x18\a \x01(\tB\x03\xe0A\x02R\acontent\x12-\n" + + "\x05nodes\x18\b \x03(\v2\x12.memos.api.v1.NodeB\x03\xe0A\x03R\x05nodes\x12=\n" + + "\n" + + "visibility\x18\t \x01(\x0e2\x18.memos.api.v1.VisibilityB\x03\xe0A\x02R\n" + + "visibility\x12\x17\n" + + "\x04tags\x18\n" + + " \x03(\tB\x03\xe0A\x03R\x04tags\x12\x1b\n" + + "\x06pinned\x18\v \x01(\bB\x03\xe0A\x01R\x06pinned\x12?\n" + + "\vattachments\x18\f \x03(\v2\x18.memos.api.v1.AttachmentB\x03\xe0A\x01R\vattachments\x12=\n" + + "\trelations\x18\r \x03(\v2\x1a.memos.api.v1.MemoRelationB\x03\xe0A\x01R\trelations\x129\n" + + "\treactions\x18\x0e \x03(\v2\x16.memos.api.v1.ReactionB\x03\xe0A\x03R\treactions\x12<\n" + + "\bproperty\x18\x0f \x01(\v2\x1b.memos.api.v1.Memo.PropertyB\x03\xe0A\x03R\bproperty\x126\n" + + "\x06parent\x18\x10 \x01(\tB\x19\xe0A\x03\xfaA\x13\n" + + "\x11memos.api.v1/MemoH\x00R\x06parent\x88\x01\x01\x12\x1d\n" + + "\asnippet\x18\x11 \x01(\tB\x03\xe0A\x03R\asnippet\x12<\n" + + "\blocation\x18\x12 \x01(\v2\x16.memos.api.v1.LocationB\x03\xe0A\x01H\x01R\blocation\x88\x01\x01\x1a\x96\x01\n" + + "\bProperty\x12\x19\n" + + "\bhas_link\x18\x01 \x01(\bR\ahasLink\x12\"\n" + + "\rhas_task_list\x18\x02 \x01(\bR\vhasTaskList\x12\x19\n" + + "\bhas_code\x18\x03 \x01(\bR\ahasCode\x120\n" + + "\x14has_incomplete_tasks\x18\x04 \x01(\bR\x12hasIncompleteTasks:7\xeaA4\n" + + "\x11memos.api.v1/Memo\x12\fmemos/{memo}\x1a\x04name*\x05memos2\x04memoB\t\n" + + "\a_parentB\v\n" + + "\t_location\"u\n" + + "\bLocation\x12%\n" + + "\vplaceholder\x18\x01 \x01(\tB\x03\xe0A\x01R\vplaceholder\x12\x1f\n" + + "\blatitude\x18\x02 \x01(\x01B\x03\xe0A\x01R\blatitude\x12!\n" + + "\tlongitude\x18\x03 \x01(\x01B\x03\xe0A\x01R\tlongitude\"\xac\x01\n" + + "\x11CreateMemoRequest\x12+\n" + + "\x04memo\x18\x01 \x01(\v2\x12.memos.api.v1.MemoB\x03\xe0A\x02R\x04memo\x12\x1c\n" + + "\amemo_id\x18\x02 \x01(\tB\x03\xe0A\x01R\x06memoId\x12(\n" + + "\rvalidate_only\x18\x03 \x01(\bB\x03\xe0A\x01R\fvalidateOnly\x12\"\n" + + "\n" + + "request_id\x18\x04 \x01(\tB\x03\xe0A\x01R\trequestId\"\xbf\x02\n" + + "\x10ListMemosRequest\x121\n" + + "\x06parent\x18\x01 \x01(\tB\x19\xe0A\x01\xfaA\x13\n" + + "\x11memos.api.v1/UserR\x06parent\x12 \n" + + "\tpage_size\x18\x02 \x01(\x05B\x03\xe0A\x01R\bpageSize\x12\"\n" + + "\n" + + "page_token\x18\x03 \x01(\tB\x03\xe0A\x01R\tpageToken\x12.\n" + + "\x05state\x18\x04 \x01(\x0e2\x13.memos.api.v1.StateB\x03\xe0A\x01R\x05state\x12\x1e\n" + + "\border_by\x18\x05 \x01(\tB\x03\xe0A\x01R\aorderBy\x12\x1b\n" + + "\x06filter\x18\x06 \x01(\tB\x03\xe0A\x01R\x06filter\x12&\n" + + "\fshow_deleted\x18\a \x01(\bB\x03\xe0A\x01R\vshowDeleted\x12\x1d\n" + + "\n" + + "old_filter\x18\b \x01(\tR\toldFilter\"\x84\x01\n" + + "\x11ListMemosResponse\x12(\n" + + "\x05memos\x18\x01 \x03(\v2\x12.memos.api.v1.MemoR\x05memos\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12\x1d\n" + + "\n" + + "total_size\x18\x03 \x01(\x05R\ttotalSize\"}\n" + + "\x0eGetMemoRequest\x12-\n" + + "\x04name\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" + + "\x11memos.api.v1/MemoR\x04name\x12<\n" + + "\tread_mask\x18\x02 \x01(\v2\x1a.google.protobuf.FieldMaskB\x03\xe0A\x01R\breadMask\"\xac\x01\n" + + "\x11UpdateMemoRequest\x12+\n" + + "\x04memo\x18\x01 \x01(\v2\x12.memos.api.v1.MemoB\x03\xe0A\x02R\x04memo\x12@\n" + + "\vupdate_mask\x18\x02 \x01(\v2\x1a.google.protobuf.FieldMaskB\x03\xe0A\x02R\n" + + "updateMask\x12(\n" + + "\rallow_missing\x18\x03 \x01(\bB\x03\xe0A\x01R\fallowMissing\"]\n" + + "\x11DeleteMemoRequest\x12-\n" + + "\x04name\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" + + "\x11memos.api.v1/MemoR\x04name\x12\x19\n" + + "\x05force\x18\x02 \x01(\bB\x03\xe0A\x01R\x05force\"\x85\x01\n" + + "\x14RenameMemoTagRequest\x121\n" + + "\x06parent\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" + + "\x11memos.api.v1/MemoR\x06parent\x12\x1c\n" + + "\aold_tag\x18\x02 \x01(\tB\x03\xe0A\x02R\x06oldTag\x12\x1c\n" + + "\anew_tag\x18\x03 \x01(\tB\x03\xe0A\x02R\x06newTag\"\x97\x01\n" + + "\x14DeleteMemoTagRequest\x121\n" + + "\x06parent\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" + + "\x11memos.api.v1/MemoR\x06parent\x12\x15\n" + + "\x03tag\x18\x02 \x01(\tB\x03\xe0A\x02R\x03tag\x125\n" + + "\x14delete_related_memos\x18\x03 \x01(\bB\x03\xe0A\x01R\x12deleteRelatedMemos\"\x8b\x01\n" + + "\x19SetMemoAttachmentsRequest\x12-\n" + + "\x04name\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" + + "\x11memos.api.v1/MemoR\x04name\x12?\n" + + "\vattachments\x18\x02 \x03(\v2\x18.memos.api.v1.AttachmentB\x03\xe0A\x02R\vattachments\"\x91\x01\n" + + "\x1aListMemoAttachmentsRequest\x12-\n" + + "\x04name\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" + + "\x11memos.api.v1/MemoR\x04name\x12 \n" + + "\tpage_size\x18\x02 \x01(\x05B\x03\xe0A\x01R\bpageSize\x12\"\n" + + "\n" + + "page_token\x18\x03 \x01(\tB\x03\xe0A\x01R\tpageToken\"\xa0\x01\n" + + "\x1bListMemoAttachmentsResponse\x12:\n" + + "\vattachments\x18\x01 \x03(\v2\x18.memos.api.v1.AttachmentR\vattachments\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12\x1d\n" + + "\n" + + "total_size\x18\x03 \x01(\x05R\ttotalSize\"\xdb\x02\n" + + "\fMemoRelation\x128\n" + + "\x04memo\x18\x01 \x01(\v2\x1f.memos.api.v1.MemoRelation.MemoB\x03\xe0A\x02R\x04memo\x12G\n" + + "\frelated_memo\x18\x02 \x01(\v2\x1f.memos.api.v1.MemoRelation.MemoB\x03\xe0A\x02R\vrelatedMemo\x128\n" + + "\x04type\x18\x03 \x01(\x0e2\x1f.memos.api.v1.MemoRelation.TypeB\x03\xe0A\x02R\x04type\x1aT\n" + + "\x04Memo\x12-\n" + + "\x04name\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" + + "\x11memos.api.v1/MemoR\x04name\x12\x1d\n" + + "\asnippet\x18\x02 \x01(\tB\x03\xe0A\x03R\asnippet\"8\n" + + "\x04Type\x12\x14\n" + + "\x10TYPE_UNSPECIFIED\x10\x00\x12\r\n" + + "\tREFERENCE\x10\x01\x12\v\n" + + "\aCOMMENT\x10\x02\"\x87\x01\n" + + "\x17SetMemoRelationsRequest\x12-\n" + + "\x04name\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" + + "\x11memos.api.v1/MemoR\x04name\x12=\n" + + "\trelations\x18\x02 \x03(\v2\x1a.memos.api.v1.MemoRelationB\x03\xe0A\x02R\trelations\"\x8f\x01\n" + + "\x18ListMemoRelationsRequest\x12-\n" + + "\x04name\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" + + "\x11memos.api.v1/MemoR\x04name\x12 \n" + + "\tpage_size\x18\x02 \x01(\x05B\x03\xe0A\x01R\bpageSize\x12\"\n" + + "\n" + + "page_token\x18\x03 \x01(\tB\x03\xe0A\x01R\tpageToken\"\x9c\x01\n" + + "\x19ListMemoRelationsResponse\x128\n" + + "\trelations\x18\x01 \x03(\v2\x1a.memos.api.v1.MemoRelationR\trelations\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12\x1d\n" + + "\n" + + "total_size\x18\x03 \x01(\x05R\ttotalSize\"\xa0\x01\n" + + "\x18CreateMemoCommentRequest\x12-\n" + + "\x04name\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" + + "\x11memos.api.v1/MemoR\x04name\x121\n" + + "\acomment\x18\x02 \x01(\v2\x12.memos.api.v1.MemoB\x03\xe0A\x02R\acomment\x12\"\n" + + "\n" + + "comment_id\x18\x03 \x01(\tB\x03\xe0A\x01R\tcommentId\"\xae\x01\n" + + "\x17ListMemoCommentsRequest\x12-\n" + + "\x04name\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" + + "\x11memos.api.v1/MemoR\x04name\x12 \n" + + "\tpage_size\x18\x02 \x01(\x05B\x03\xe0A\x01R\bpageSize\x12\"\n" + + "\n" + + "page_token\x18\x03 \x01(\tB\x03\xe0A\x01R\tpageToken\x12\x1e\n" + + "\border_by\x18\x04 \x01(\tB\x03\xe0A\x01R\aorderBy\"\x8b\x01\n" + + "\x18ListMemoCommentsResponse\x12(\n" + + "\x05memos\x18\x01 \x03(\v2\x12.memos.api.v1.MemoR\x05memos\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12\x1d\n" + + "\n" + + "total_size\x18\x03 \x01(\x05R\ttotalSize\"\x8f\x01\n" + + "\x18ListMemoReactionsRequest\x12-\n" + + "\x04name\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" + + "\x11memos.api.v1/MemoR\x04name\x12 \n" + + "\tpage_size\x18\x02 \x01(\x05B\x03\xe0A\x01R\bpageSize\x12\"\n" + + "\n" + + "page_token\x18\x03 \x01(\tB\x03\xe0A\x01R\tpageToken\"\x98\x01\n" + + "\x19ListMemoReactionsResponse\x124\n" + + "\treactions\x18\x01 \x03(\v2\x16.memos.api.v1.ReactionR\treactions\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12\x1d\n" + + "\n" + + "total_size\x18\x03 \x01(\x05R\ttotalSize\"\x83\x01\n" + + "\x19UpsertMemoReactionRequest\x12-\n" + + "\x04name\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" + + "\x11memos.api.v1/MemoR\x04name\x127\n" + + "\breaction\x18\x02 \x01(\v2\x16.memos.api.v1.ReactionB\x03\xe0A\x02R\breaction\"N\n" + + "\x19DeleteMemoReactionRequest\x121\n" + + "\x04name\x18\x01 \x01(\tB\x1d\xe0A\x02\xfaA\x17\n" + + "\x15memos.api.v1/ReactionR\x04name\"\xe6\x01\n" + + "\x12ExportMemosRequest\x12\x1b\n" + + "\x06format\x18\x01 \x01(\tB\x03\xe0A\x01R\x06format\x12\x1b\n" + + "\x06filter\x18\x02 \x01(\tB\x03\xe0A\x01R\x06filter\x12.\n" + + "\x10exclude_archived\x18\x03 \x01(\bB\x03\xe0A\x01R\x0fexcludeArchived\x124\n" + + "\x13include_attachments\x18\x04 \x01(\bB\x03\xe0A\x01R\x12includeAttachments\x120\n" + + "\x11include_relations\x18\x05 \x01(\bB\x03\xe0A\x01R\x10includeRelations\"\x9b\x01\n" + + "\x13ExportMemosResponse\x12\x12\n" + + "\x04data\x18\x01 \x01(\fR\x04data\x12\x16\n" + + "\x06format\x18\x02 \x01(\tR\x06format\x12\x1a\n" + + "\bfilename\x18\x03 \x01(\tR\bfilename\x12\x1d\n" + + "\n" + + "memo_count\x18\x04 \x01(\x05R\tmemoCount\x12\x1d\n" + + "\n" + + "size_bytes\x18\x05 \x01(\x03R\tsizeBytes\"\xba\x02\n" + + "\x12ImportMemosRequest\x12\x17\n" + + "\x04data\x18\x01 \x01(\fB\x03\xe0A\x02R\x04data\x12\x1b\n" + + "\x06format\x18\x02 \x01(\tB\x03\xe0A\x01R\x06format\x122\n" + + "\x12overwrite_existing\x18\x03 \x01(\bB\x03\xe0A\x01R\x11overwriteExisting\x12(\n" + + "\rvalidate_only\x18\x04 \x01(\bB\x03\xe0A\x01R\fvalidateOnly\x124\n" + + "\x13preserve_timestamps\x18\x05 \x01(\bB\x03\xe0A\x01R\x12preserveTimestamps\x12.\n" + + "\x10skip_attachments\x18\x06 \x01(\bB\x03\xe0A\x01R\x0fskipAttachments\x12*\n" + + "\x0eskip_relations\x18\a \x01(\bB\x03\xe0A\x01R\rskipRelations\"\xf9\x01\n" + + "\x13ImportMemosResponse\x12%\n" + + "\x0eimported_count\x18\x01 \x01(\x05R\rimportedCount\x12#\n" + + "\rskipped_count\x18\x02 \x01(\x05R\fskippedCount\x12+\n" + + "\x11validation_errors\x18\x03 \x01(\x05R\x10validationErrors\x12\x16\n" + + "\x06errors\x18\x04 \x03(\tR\x06errors\x12\x1a\n" + + "\bwarnings\x18\x05 \x03(\tR\bwarnings\x125\n" + + "\asummary\x18\x06 \x01(\v2\x1b.memos.api.v1.ImportSummaryR\asummary\"\xfd\x01\n" + + "\rImportSummary\x12\x1f\n" + + "\vtotal_memos\x18\x01 \x01(\x05R\n" + + "totalMemos\x12#\n" + + "\rcreated_count\x18\x02 \x01(\x05R\fcreatedCount\x12#\n" + + "\rupdated_count\x18\x03 \x01(\x05R\fupdatedCount\x121\n" + + "\x14attachments_imported\x18\x04 \x01(\x05R\x13attachmentsImported\x12-\n" + + "\x12relations_imported\x18\x05 \x01(\x05R\x11relationsImported\x12\x1f\n" + + "\vduration_ms\x18\x06 \x01(\x03R\n" + + "durationMs*P\n" + + "\n" + + "Visibility\x12\x1a\n" + + "\x16VISIBILITY_UNSPECIFIED\x10\x00\x12\v\n" + + "\aPRIVATE\x10\x01\x12\r\n" + + "\tPROTECTED\x10\x02\x12\n" + + "\n" + + "\x06PUBLIC\x10\x032\x81\x13\n" + + "\vMemoService\x12e\n" + + "\n" + + "CreateMemo\x12\x1f.memos.api.v1.CreateMemoRequest\x1a\x12.memos.api.v1.Memo\"\"\xdaA\x04memo\x82\xd3\xe4\x93\x02\x15:\x04memo\"\r/api/v1/memos\x12\x91\x01\n" + + "\tListMemos\x12\x1e.memos.api.v1.ListMemosRequest\x1a\x1f.memos.api.v1.ListMemosResponse\"C\xdaA\x00\xdaA\x06parent\x82\xd3\xe4\x93\x021Z \x12\x1e/api/v1/{parent=users/*}/memos\x12\r/api/v1/memos\x12b\n" + + "\aGetMemo\x12\x1c.memos.api.v1.GetMemoRequest\x1a\x12.memos.api.v1.Memo\"%\xdaA\x04name\x82\xd3\xe4\x93\x02\x18\x12\x16/api/v1/{name=memos/*}\x12\x7f\n" + + "\n" + + "UpdateMemo\x12\x1f.memos.api.v1.UpdateMemoRequest\x1a\x12.memos.api.v1.Memo\"<\xdaA\x10memo,update_mask\x82\xd3\xe4\x93\x02#:\x04memo2\x1b/api/v1/{memo.name=memos/*}\x12l\n" + + "\n" + + "DeleteMemo\x12\x1f.memos.api.v1.DeleteMemoRequest\x1a\x16.google.protobuf.Empty\"%\xdaA\x04name\x82\xd3\xe4\x93\x02\x18*\x16/api/v1/{name=memos/*}\x12\x95\x01\n" + + "\rRenameMemoTag\x12\".memos.api.v1.RenameMemoTagRequest\x1a\x16.google.protobuf.Empty\"H\xdaA\x16parent,old_tag,new_tag\x82\xd3\xe4\x93\x02):\x01*2$/api/v1/{parent=memos/*}/tags:rename\x12\x85\x01\n" + + "\rDeleteMemoTag\x12\".memos.api.v1.DeleteMemoTagRequest\x1a\x16.google.protobuf.Empty\"8\xdaA\n" + + "parent,tag\x82\xd3\xe4\x93\x02%*#/api/v1/{parent=memos/*}/tags/{tag}\x12\x8b\x01\n" + + "\x12SetMemoAttachments\x12'.memos.api.v1.SetMemoAttachmentsRequest\x1a\x16.google.protobuf.Empty\"4\xdaA\x04name\x82\xd3\xe4\x93\x02':\x01*2\"/api/v1/{name=memos/*}/attachments\x12\x9d\x01\n" + + "\x13ListMemoAttachments\x12(.memos.api.v1.ListMemoAttachmentsRequest\x1a).memos.api.v1.ListMemoAttachmentsResponse\"1\xdaA\x04name\x82\xd3\xe4\x93\x02$\x12\"/api/v1/{name=memos/*}/attachments\x12\x85\x01\n" + + "\x10SetMemoRelations\x12%.memos.api.v1.SetMemoRelationsRequest\x1a\x16.google.protobuf.Empty\"2\xdaA\x04name\x82\xd3\xe4\x93\x02%:\x01*2 /api/v1/{name=memos/*}/relations\x12\x95\x01\n" + + "\x11ListMemoRelations\x12&.memos.api.v1.ListMemoRelationsRequest\x1a'.memos.api.v1.ListMemoRelationsResponse\"/\xdaA\x04name\x82\xd3\xe4\x93\x02\"\x12 /api/v1/{name=memos/*}/relations\x12\x90\x01\n" + + "\x11CreateMemoComment\x12&.memos.api.v1.CreateMemoCommentRequest\x1a\x12.memos.api.v1.Memo\"?\xdaA\fname,comment\x82\xd3\xe4\x93\x02*:\acomment\"\x1f/api/v1/{name=memos/*}/comments\x12\x91\x01\n" + + "\x10ListMemoComments\x12%.memos.api.v1.ListMemoCommentsRequest\x1a&.memos.api.v1.ListMemoCommentsResponse\".\xdaA\x04name\x82\xd3\xe4\x93\x02!\x12\x1f/api/v1/{name=memos/*}/comments\x12\x95\x01\n" + + "\x11ListMemoReactions\x12&.memos.api.v1.ListMemoReactionsRequest\x1a'.memos.api.v1.ListMemoReactionsResponse\"/\xdaA\x04name\x82\xd3\xe4\x93\x02\"\x12 /api/v1/{name=memos/*}/reactions\x12\x89\x01\n" + + "\x12UpsertMemoReaction\x12'.memos.api.v1.UpsertMemoReactionRequest\x1a\x16.memos.api.v1.Reaction\"2\xdaA\x04name\x82\xd3\xe4\x93\x02%:\x01*\" /api/v1/{name=memos/*}/reactions\x12\x80\x01\n" + + "\x12DeleteMemoReaction\x12'.memos.api.v1.DeleteMemoReactionRequest\x1a\x16.google.protobuf.Empty\")\xdaA\x04name\x82\xd3\xe4\x93\x02\x1c*\x1a/api/v1/{name=reactions/*}\x12s\n" + + "\vExportMemos\x12 .memos.api.v1.ExportMemosRequest\x1a!.memos.api.v1.ExportMemosResponse\"\x1f\x82\xd3\xe4\x93\x02\x19:\x01*\"\x14/api/v1/memos:export\x12s\n" + + "\vImportMemos\x12 .memos.api.v1.ImportMemosRequest\x1a!.memos.api.v1.ImportMemosResponse\"\x1f\x82\xd3\xe4\x93\x02\x19:\x01*\"\x14/api/v1/memos:importB\xa8\x01\n" + + "\x10com.memos.api.v1B\x10MemoServiceProtoP\x01Z0github.com/usememos/memos/proto/gen/api/v1;apiv1\xa2\x02\x03MAX\xaa\x02\fMemos.Api.V1\xca\x02\fMemos\\Api\\V1\xe2\x02\x18Memos\\Api\\V1\\GPBMetadata\xea\x02\x0eMemos::Api::V1b\x06proto3" + +var ( + file_api_v1_memo_service_proto_rawDescOnce sync.Once + file_api_v1_memo_service_proto_rawDescData []byte +) + +func file_api_v1_memo_service_proto_rawDescGZIP() []byte { + file_api_v1_memo_service_proto_rawDescOnce.Do(func() { + file_api_v1_memo_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_v1_memo_service_proto_rawDesc), len(file_api_v1_memo_service_proto_rawDesc))) + }) + return file_api_v1_memo_service_proto_rawDescData +} + +var file_api_v1_memo_service_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_api_v1_memo_service_proto_msgTypes = make([]protoimpl.MessageInfo, 32) +var file_api_v1_memo_service_proto_goTypes = []any{ + (Visibility)(0), // 0: memos.api.v1.Visibility + (MemoRelation_Type)(0), // 1: memos.api.v1.MemoRelation.Type + (*Reaction)(nil), // 2: memos.api.v1.Reaction + (*Memo)(nil), // 3: memos.api.v1.Memo + (*Location)(nil), // 4: memos.api.v1.Location + (*CreateMemoRequest)(nil), // 5: memos.api.v1.CreateMemoRequest + (*ListMemosRequest)(nil), // 6: memos.api.v1.ListMemosRequest + (*ListMemosResponse)(nil), // 7: memos.api.v1.ListMemosResponse + (*GetMemoRequest)(nil), // 8: memos.api.v1.GetMemoRequest + (*UpdateMemoRequest)(nil), // 9: memos.api.v1.UpdateMemoRequest + (*DeleteMemoRequest)(nil), // 10: memos.api.v1.DeleteMemoRequest + (*RenameMemoTagRequest)(nil), // 11: memos.api.v1.RenameMemoTagRequest + (*DeleteMemoTagRequest)(nil), // 12: memos.api.v1.DeleteMemoTagRequest + (*SetMemoAttachmentsRequest)(nil), // 13: memos.api.v1.SetMemoAttachmentsRequest + (*ListMemoAttachmentsRequest)(nil), // 14: memos.api.v1.ListMemoAttachmentsRequest + (*ListMemoAttachmentsResponse)(nil), // 15: memos.api.v1.ListMemoAttachmentsResponse + (*MemoRelation)(nil), // 16: memos.api.v1.MemoRelation + (*SetMemoRelationsRequest)(nil), // 17: memos.api.v1.SetMemoRelationsRequest + (*ListMemoRelationsRequest)(nil), // 18: memos.api.v1.ListMemoRelationsRequest + (*ListMemoRelationsResponse)(nil), // 19: memos.api.v1.ListMemoRelationsResponse + (*CreateMemoCommentRequest)(nil), // 20: memos.api.v1.CreateMemoCommentRequest + (*ListMemoCommentsRequest)(nil), // 21: memos.api.v1.ListMemoCommentsRequest + (*ListMemoCommentsResponse)(nil), // 22: memos.api.v1.ListMemoCommentsResponse + (*ListMemoReactionsRequest)(nil), // 23: memos.api.v1.ListMemoReactionsRequest + (*ListMemoReactionsResponse)(nil), // 24: memos.api.v1.ListMemoReactionsResponse + (*UpsertMemoReactionRequest)(nil), // 25: memos.api.v1.UpsertMemoReactionRequest + (*DeleteMemoReactionRequest)(nil), // 26: memos.api.v1.DeleteMemoReactionRequest + (*ExportMemosRequest)(nil), // 27: memos.api.v1.ExportMemosRequest + (*ExportMemosResponse)(nil), // 28: memos.api.v1.ExportMemosResponse + (*ImportMemosRequest)(nil), // 29: memos.api.v1.ImportMemosRequest + (*ImportMemosResponse)(nil), // 30: memos.api.v1.ImportMemosResponse + (*ImportSummary)(nil), // 31: memos.api.v1.ImportSummary + (*Memo_Property)(nil), // 32: memos.api.v1.Memo.Property + (*MemoRelation_Memo)(nil), // 33: memos.api.v1.MemoRelation.Memo + (*timestamppb.Timestamp)(nil), // 34: google.protobuf.Timestamp + (State)(0), // 35: memos.api.v1.State + (*Node)(nil), // 36: memos.api.v1.Node + (*Attachment)(nil), // 37: memos.api.v1.Attachment + (*fieldmaskpb.FieldMask)(nil), // 38: google.protobuf.FieldMask + (*emptypb.Empty)(nil), // 39: google.protobuf.Empty +} +var file_api_v1_memo_service_proto_depIdxs = []int32{ + 34, // 0: memos.api.v1.Reaction.create_time:type_name -> google.protobuf.Timestamp + 35, // 1: memos.api.v1.Memo.state:type_name -> memos.api.v1.State + 34, // 2: memos.api.v1.Memo.create_time:type_name -> google.protobuf.Timestamp + 34, // 3: memos.api.v1.Memo.update_time:type_name -> google.protobuf.Timestamp + 34, // 4: memos.api.v1.Memo.display_time:type_name -> google.protobuf.Timestamp + 36, // 5: memos.api.v1.Memo.nodes:type_name -> memos.api.v1.Node + 0, // 6: memos.api.v1.Memo.visibility:type_name -> memos.api.v1.Visibility + 37, // 7: memos.api.v1.Memo.attachments:type_name -> memos.api.v1.Attachment + 16, // 8: memos.api.v1.Memo.relations:type_name -> memos.api.v1.MemoRelation + 2, // 9: memos.api.v1.Memo.reactions:type_name -> memos.api.v1.Reaction + 32, // 10: memos.api.v1.Memo.property:type_name -> memos.api.v1.Memo.Property + 4, // 11: memos.api.v1.Memo.location:type_name -> memos.api.v1.Location + 3, // 12: memos.api.v1.CreateMemoRequest.memo:type_name -> memos.api.v1.Memo + 35, // 13: memos.api.v1.ListMemosRequest.state:type_name -> memos.api.v1.State + 3, // 14: memos.api.v1.ListMemosResponse.memos:type_name -> memos.api.v1.Memo + 38, // 15: memos.api.v1.GetMemoRequest.read_mask:type_name -> google.protobuf.FieldMask + 3, // 16: memos.api.v1.UpdateMemoRequest.memo:type_name -> memos.api.v1.Memo + 38, // 17: memos.api.v1.UpdateMemoRequest.update_mask:type_name -> google.protobuf.FieldMask + 37, // 18: memos.api.v1.SetMemoAttachmentsRequest.attachments:type_name -> memos.api.v1.Attachment + 37, // 19: memos.api.v1.ListMemoAttachmentsResponse.attachments:type_name -> memos.api.v1.Attachment + 33, // 20: memos.api.v1.MemoRelation.memo:type_name -> memos.api.v1.MemoRelation.Memo + 33, // 21: memos.api.v1.MemoRelation.related_memo:type_name -> memos.api.v1.MemoRelation.Memo + 1, // 22: memos.api.v1.MemoRelation.type:type_name -> memos.api.v1.MemoRelation.Type + 16, // 23: memos.api.v1.SetMemoRelationsRequest.relations:type_name -> memos.api.v1.MemoRelation + 16, // 24: memos.api.v1.ListMemoRelationsResponse.relations:type_name -> memos.api.v1.MemoRelation + 3, // 25: memos.api.v1.CreateMemoCommentRequest.comment:type_name -> memos.api.v1.Memo + 3, // 26: memos.api.v1.ListMemoCommentsResponse.memos:type_name -> memos.api.v1.Memo + 2, // 27: memos.api.v1.ListMemoReactionsResponse.reactions:type_name -> memos.api.v1.Reaction + 2, // 28: memos.api.v1.UpsertMemoReactionRequest.reaction:type_name -> memos.api.v1.Reaction + 31, // 29: memos.api.v1.ImportMemosResponse.summary:type_name -> memos.api.v1.ImportSummary + 5, // 30: memos.api.v1.MemoService.CreateMemo:input_type -> memos.api.v1.CreateMemoRequest + 6, // 31: memos.api.v1.MemoService.ListMemos:input_type -> memos.api.v1.ListMemosRequest + 8, // 32: memos.api.v1.MemoService.GetMemo:input_type -> memos.api.v1.GetMemoRequest + 9, // 33: memos.api.v1.MemoService.UpdateMemo:input_type -> memos.api.v1.UpdateMemoRequest + 10, // 34: memos.api.v1.MemoService.DeleteMemo:input_type -> memos.api.v1.DeleteMemoRequest + 11, // 35: memos.api.v1.MemoService.RenameMemoTag:input_type -> memos.api.v1.RenameMemoTagRequest + 12, // 36: memos.api.v1.MemoService.DeleteMemoTag:input_type -> memos.api.v1.DeleteMemoTagRequest + 13, // 37: memos.api.v1.MemoService.SetMemoAttachments:input_type -> memos.api.v1.SetMemoAttachmentsRequest + 14, // 38: memos.api.v1.MemoService.ListMemoAttachments:input_type -> memos.api.v1.ListMemoAttachmentsRequest + 17, // 39: memos.api.v1.MemoService.SetMemoRelations:input_type -> memos.api.v1.SetMemoRelationsRequest + 18, // 40: memos.api.v1.MemoService.ListMemoRelations:input_type -> memos.api.v1.ListMemoRelationsRequest + 20, // 41: memos.api.v1.MemoService.CreateMemoComment:input_type -> memos.api.v1.CreateMemoCommentRequest + 21, // 42: memos.api.v1.MemoService.ListMemoComments:input_type -> memos.api.v1.ListMemoCommentsRequest + 23, // 43: memos.api.v1.MemoService.ListMemoReactions:input_type -> memos.api.v1.ListMemoReactionsRequest + 25, // 44: memos.api.v1.MemoService.UpsertMemoReaction:input_type -> memos.api.v1.UpsertMemoReactionRequest + 26, // 45: memos.api.v1.MemoService.DeleteMemoReaction:input_type -> memos.api.v1.DeleteMemoReactionRequest + 27, // 46: memos.api.v1.MemoService.ExportMemos:input_type -> memos.api.v1.ExportMemosRequest + 29, // 47: memos.api.v1.MemoService.ImportMemos:input_type -> memos.api.v1.ImportMemosRequest + 3, // 48: memos.api.v1.MemoService.CreateMemo:output_type -> memos.api.v1.Memo + 7, // 49: memos.api.v1.MemoService.ListMemos:output_type -> memos.api.v1.ListMemosResponse + 3, // 50: memos.api.v1.MemoService.GetMemo:output_type -> memos.api.v1.Memo + 3, // 51: memos.api.v1.MemoService.UpdateMemo:output_type -> memos.api.v1.Memo + 39, // 52: memos.api.v1.MemoService.DeleteMemo:output_type -> google.protobuf.Empty + 39, // 53: memos.api.v1.MemoService.RenameMemoTag:output_type -> google.protobuf.Empty + 39, // 54: memos.api.v1.MemoService.DeleteMemoTag:output_type -> google.protobuf.Empty + 39, // 55: memos.api.v1.MemoService.SetMemoAttachments:output_type -> google.protobuf.Empty + 15, // 56: memos.api.v1.MemoService.ListMemoAttachments:output_type -> memos.api.v1.ListMemoAttachmentsResponse + 39, // 57: memos.api.v1.MemoService.SetMemoRelations:output_type -> google.protobuf.Empty + 19, // 58: memos.api.v1.MemoService.ListMemoRelations:output_type -> memos.api.v1.ListMemoRelationsResponse + 3, // 59: memos.api.v1.MemoService.CreateMemoComment:output_type -> memos.api.v1.Memo + 22, // 60: memos.api.v1.MemoService.ListMemoComments:output_type -> memos.api.v1.ListMemoCommentsResponse + 24, // 61: memos.api.v1.MemoService.ListMemoReactions:output_type -> memos.api.v1.ListMemoReactionsResponse + 2, // 62: memos.api.v1.MemoService.UpsertMemoReaction:output_type -> memos.api.v1.Reaction + 39, // 63: memos.api.v1.MemoService.DeleteMemoReaction:output_type -> google.protobuf.Empty + 28, // 64: memos.api.v1.MemoService.ExportMemos:output_type -> memos.api.v1.ExportMemosResponse + 30, // 65: memos.api.v1.MemoService.ImportMemos:output_type -> memos.api.v1.ImportMemosResponse + 48, // [48:66] is the sub-list for method output_type + 30, // [30:48] is the sub-list for method input_type + 30, // [30:30] is the sub-list for extension type_name + 30, // [30:30] is the sub-list for extension extendee + 0, // [0:30] is the sub-list for field type_name +} + +func init() { file_api_v1_memo_service_proto_init() } +func file_api_v1_memo_service_proto_init() { + if File_api_v1_memo_service_proto != nil { + return + } + file_api_v1_attachment_service_proto_init() + file_api_v1_common_proto_init() + file_api_v1_markdown_service_proto_init() + file_api_v1_memo_service_proto_msgTypes[1].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_memo_service_proto_rawDesc), len(file_api_v1_memo_service_proto_rawDesc)), + NumEnums: 2, + NumMessages: 32, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_api_v1_memo_service_proto_goTypes, + DependencyIndexes: file_api_v1_memo_service_proto_depIdxs, + EnumInfos: file_api_v1_memo_service_proto_enumTypes, + MessageInfos: file_api_v1_memo_service_proto_msgTypes, + }.Build() + File_api_v1_memo_service_proto = out.File + file_api_v1_memo_service_proto_goTypes = nil + file_api_v1_memo_service_proto_depIdxs = nil +} diff --git a/proto/gen/api/v1/memo_service.pb.gw.go b/proto/gen/api/v1/memo_service.pb.gw.go new file mode 100644 index 0000000..7ab5ec1 --- /dev/null +++ b/proto/gen/api/v1/memo_service.pb.gw.go @@ -0,0 +1,1761 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: api/v1/memo_service.proto + +/* +Package apiv1 is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package apiv1 + +import ( + "context" + "errors" + "io" + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// Suppress "imported and not used" errors +var ( + _ codes.Code + _ io.Reader + _ status.Status + _ = errors.New + _ = runtime.String + _ = utilities.NewDoubleArray + _ = metadata.Join +) + +var filter_MemoService_CreateMemo_0 = &utilities.DoubleArray{Encoding: map[string]int{"memo": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} + +func request_MemoService_CreateMemo_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateMemoRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Memo); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_CreateMemo_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.CreateMemo(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_MemoService_CreateMemo_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateMemoRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Memo); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_CreateMemo_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.CreateMemo(ctx, &protoReq) + return msg, metadata, err +} + +var filter_MemoService_ListMemos_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + +func request_MemoService_ListMemos_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListMemosRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_ListMemos_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.ListMemos(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_MemoService_ListMemos_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListMemosRequest + metadata runtime.ServerMetadata + ) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_ListMemos_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ListMemos(ctx, &protoReq) + return msg, metadata, err +} + +var filter_MemoService_ListMemos_1 = &utilities.DoubleArray{Encoding: map[string]int{"parent": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} + +func request_MemoService_ListMemos_1(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListMemosRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_ListMemos_1); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.ListMemos(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_MemoService_ListMemos_1(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListMemosRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_ListMemos_1); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ListMemos(ctx, &protoReq) + return msg, metadata, err +} + +var filter_MemoService_GetMemo_0 = &utilities.DoubleArray{Encoding: map[string]int{"name": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} + +func request_MemoService_GetMemo_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetMemoRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_GetMemo_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.GetMemo(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_MemoService_GetMemo_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetMemoRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_GetMemo_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.GetMemo(ctx, &protoReq) + return msg, metadata, err +} + +var filter_MemoService_UpdateMemo_0 = &utilities.DoubleArray{Encoding: map[string]int{"memo": 0, "name": 1}, Base: []int{1, 2, 1, 0, 0}, Check: []int{0, 1, 2, 3, 2}} + +func request_MemoService_UpdateMemo_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateMemoRequest + metadata runtime.ServerMetadata + err error + ) + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Memo); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 { + if fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Memo); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } else { + protoReq.UpdateMask = fieldMask + } + } + val, ok := pathParams["memo.name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "memo.name") + } + err = runtime.PopulateFieldFromPath(&protoReq, "memo.name", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "memo.name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_UpdateMemo_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.UpdateMemo(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_MemoService_UpdateMemo_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateMemoRequest + metadata runtime.ServerMetadata + err error + ) + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Memo); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 { + if fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Memo); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } else { + protoReq.UpdateMask = fieldMask + } + } + val, ok := pathParams["memo.name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "memo.name") + } + err = runtime.PopulateFieldFromPath(&protoReq, "memo.name", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "memo.name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_UpdateMemo_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.UpdateMemo(ctx, &protoReq) + return msg, metadata, err +} + +var filter_MemoService_DeleteMemo_0 = &utilities.DoubleArray{Encoding: map[string]int{"name": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} + +func request_MemoService_DeleteMemo_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteMemoRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_DeleteMemo_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.DeleteMemo(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_MemoService_DeleteMemo_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteMemoRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_DeleteMemo_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.DeleteMemo(ctx, &protoReq) + return msg, metadata, err +} + +func request_MemoService_RenameMemoTag_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq RenameMemoTagRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } + msg, err := client.RenameMemoTag(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_MemoService_RenameMemoTag_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq RenameMemoTagRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } + msg, err := server.RenameMemoTag(ctx, &protoReq) + return msg, metadata, err +} + +var filter_MemoService_DeleteMemoTag_0 = &utilities.DoubleArray{Encoding: map[string]int{"parent": 0, "tag": 1}, Base: []int{1, 1, 2, 0, 0}, Check: []int{0, 1, 1, 2, 3}} + +func request_MemoService_DeleteMemoTag_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteMemoTagRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } + val, ok = pathParams["tag"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "tag") + } + protoReq.Tag, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "tag", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_DeleteMemoTag_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.DeleteMemoTag(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_MemoService_DeleteMemoTag_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteMemoTagRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } + val, ok = pathParams["tag"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "tag") + } + protoReq.Tag, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "tag", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_DeleteMemoTag_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.DeleteMemoTag(ctx, &protoReq) + return msg, metadata, err +} + +func request_MemoService_SetMemoAttachments_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq SetMemoAttachmentsRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := client.SetMemoAttachments(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_MemoService_SetMemoAttachments_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq SetMemoAttachmentsRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := server.SetMemoAttachments(ctx, &protoReq) + return msg, metadata, err +} + +var filter_MemoService_ListMemoAttachments_0 = &utilities.DoubleArray{Encoding: map[string]int{"name": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} + +func request_MemoService_ListMemoAttachments_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListMemoAttachmentsRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_ListMemoAttachments_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.ListMemoAttachments(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_MemoService_ListMemoAttachments_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListMemoAttachmentsRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_ListMemoAttachments_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ListMemoAttachments(ctx, &protoReq) + return msg, metadata, err +} + +func request_MemoService_SetMemoRelations_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq SetMemoRelationsRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := client.SetMemoRelations(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_MemoService_SetMemoRelations_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq SetMemoRelationsRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := server.SetMemoRelations(ctx, &protoReq) + return msg, metadata, err +} + +var filter_MemoService_ListMemoRelations_0 = &utilities.DoubleArray{Encoding: map[string]int{"name": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} + +func request_MemoService_ListMemoRelations_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListMemoRelationsRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_ListMemoRelations_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.ListMemoRelations(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_MemoService_ListMemoRelations_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListMemoRelationsRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_ListMemoRelations_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ListMemoRelations(ctx, &protoReq) + return msg, metadata, err +} + +var filter_MemoService_CreateMemoComment_0 = &utilities.DoubleArray{Encoding: map[string]int{"comment": 0, "name": 1}, Base: []int{1, 1, 2, 0, 0}, Check: []int{0, 1, 1, 2, 3}} + +func request_MemoService_CreateMemoComment_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateMemoCommentRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Comment); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_CreateMemoComment_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.CreateMemoComment(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_MemoService_CreateMemoComment_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateMemoCommentRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Comment); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_CreateMemoComment_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.CreateMemoComment(ctx, &protoReq) + return msg, metadata, err +} + +var filter_MemoService_ListMemoComments_0 = &utilities.DoubleArray{Encoding: map[string]int{"name": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} + +func request_MemoService_ListMemoComments_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListMemoCommentsRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_ListMemoComments_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.ListMemoComments(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_MemoService_ListMemoComments_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListMemoCommentsRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_ListMemoComments_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ListMemoComments(ctx, &protoReq) + return msg, metadata, err +} + +var filter_MemoService_ListMemoReactions_0 = &utilities.DoubleArray{Encoding: map[string]int{"name": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} + +func request_MemoService_ListMemoReactions_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListMemoReactionsRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_ListMemoReactions_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.ListMemoReactions(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_MemoService_ListMemoReactions_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListMemoReactionsRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_MemoService_ListMemoReactions_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ListMemoReactions(ctx, &protoReq) + return msg, metadata, err +} + +func request_MemoService_UpsertMemoReaction_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpsertMemoReactionRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := client.UpsertMemoReaction(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_MemoService_UpsertMemoReaction_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpsertMemoReactionRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := server.UpsertMemoReaction(ctx, &protoReq) + return msg, metadata, err +} + +func request_MemoService_DeleteMemoReaction_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteMemoReactionRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := client.DeleteMemoReaction(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_MemoService_DeleteMemoReaction_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteMemoReactionRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := server.DeleteMemoReaction(ctx, &protoReq) + return msg, metadata, err +} + +func request_MemoService_ExportMemos_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ExportMemosRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.ExportMemos(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_MemoService_ExportMemos_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ExportMemosRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ExportMemos(ctx, &protoReq) + return msg, metadata, err +} + +func request_MemoService_ImportMemos_0(ctx context.Context, marshaler runtime.Marshaler, client MemoServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ImportMemosRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.ImportMemos(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_MemoService_ImportMemos_0(ctx context.Context, marshaler runtime.Marshaler, server MemoServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ImportMemosRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ImportMemos(ctx, &protoReq) + return msg, metadata, err +} + +// RegisterMemoServiceHandlerServer registers the http handlers for service MemoService to "mux". +// UnaryRPC :call MemoServiceServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterMemoServiceHandlerFromEndpoint instead. +// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call. +func RegisterMemoServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server MemoServiceServer) error { + mux.Handle(http.MethodPost, pattern_MemoService_CreateMemo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.MemoService/CreateMemo", runtime.WithHTTPPathPattern("/api/v1/memos")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_MemoService_CreateMemo_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_CreateMemo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_MemoService_ListMemos_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.MemoService/ListMemos", runtime.WithHTTPPathPattern("/api/v1/memos")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_MemoService_ListMemos_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_ListMemos_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_MemoService_ListMemos_1, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.MemoService/ListMemos", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/memos")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_MemoService_ListMemos_1(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_ListMemos_1(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_MemoService_GetMemo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.MemoService/GetMemo", runtime.WithHTTPPathPattern("/api/v1/{name=memos/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_MemoService_GetMemo_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_GetMemo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPatch, pattern_MemoService_UpdateMemo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.MemoService/UpdateMemo", runtime.WithHTTPPathPattern("/api/v1/{memo.name=memos/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_MemoService_UpdateMemo_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_UpdateMemo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_MemoService_DeleteMemo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.MemoService/DeleteMemo", runtime.WithHTTPPathPattern("/api/v1/{name=memos/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_MemoService_DeleteMemo_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_DeleteMemo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPatch, pattern_MemoService_RenameMemoTag_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.MemoService/RenameMemoTag", runtime.WithHTTPPathPattern("/api/v1/{parent=memos/*}/tags:rename")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_MemoService_RenameMemoTag_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_RenameMemoTag_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_MemoService_DeleteMemoTag_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.MemoService/DeleteMemoTag", runtime.WithHTTPPathPattern("/api/v1/{parent=memos/*}/tags/{tag}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_MemoService_DeleteMemoTag_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_DeleteMemoTag_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPatch, pattern_MemoService_SetMemoAttachments_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.MemoService/SetMemoAttachments", runtime.WithHTTPPathPattern("/api/v1/{name=memos/*}/attachments")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_MemoService_SetMemoAttachments_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_SetMemoAttachments_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_MemoService_ListMemoAttachments_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.MemoService/ListMemoAttachments", runtime.WithHTTPPathPattern("/api/v1/{name=memos/*}/attachments")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_MemoService_ListMemoAttachments_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_ListMemoAttachments_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPatch, pattern_MemoService_SetMemoRelations_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.MemoService/SetMemoRelations", runtime.WithHTTPPathPattern("/api/v1/{name=memos/*}/relations")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_MemoService_SetMemoRelations_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_SetMemoRelations_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_MemoService_ListMemoRelations_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.MemoService/ListMemoRelations", runtime.WithHTTPPathPattern("/api/v1/{name=memos/*}/relations")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_MemoService_ListMemoRelations_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_ListMemoRelations_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_MemoService_CreateMemoComment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.MemoService/CreateMemoComment", runtime.WithHTTPPathPattern("/api/v1/{name=memos/*}/comments")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_MemoService_CreateMemoComment_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_CreateMemoComment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_MemoService_ListMemoComments_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.MemoService/ListMemoComments", runtime.WithHTTPPathPattern("/api/v1/{name=memos/*}/comments")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_MemoService_ListMemoComments_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_ListMemoComments_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_MemoService_ListMemoReactions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.MemoService/ListMemoReactions", runtime.WithHTTPPathPattern("/api/v1/{name=memos/*}/reactions")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_MemoService_ListMemoReactions_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_ListMemoReactions_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_MemoService_UpsertMemoReaction_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.MemoService/UpsertMemoReaction", runtime.WithHTTPPathPattern("/api/v1/{name=memos/*}/reactions")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_MemoService_UpsertMemoReaction_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_UpsertMemoReaction_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_MemoService_DeleteMemoReaction_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.MemoService/DeleteMemoReaction", runtime.WithHTTPPathPattern("/api/v1/{name=reactions/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_MemoService_DeleteMemoReaction_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_DeleteMemoReaction_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_MemoService_ExportMemos_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.MemoService/ExportMemos", runtime.WithHTTPPathPattern("/api/v1/memos:export")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_MemoService_ExportMemos_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_ExportMemos_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_MemoService_ImportMemos_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.MemoService/ImportMemos", runtime.WithHTTPPathPattern("/api/v1/memos:import")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_MemoService_ImportMemos_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_ImportMemos_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + + return nil +} + +// RegisterMemoServiceHandlerFromEndpoint is same as RegisterMemoServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterMemoServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.NewClient(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + return RegisterMemoServiceHandler(ctx, mux, conn) +} + +// RegisterMemoServiceHandler registers the http handlers for service MemoService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterMemoServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterMemoServiceHandlerClient(ctx, mux, NewMemoServiceClient(conn)) +} + +// RegisterMemoServiceHandlerClient registers the http handlers for service MemoService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "MemoServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "MemoServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "MemoServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares. +func RegisterMemoServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client MemoServiceClient) error { + mux.Handle(http.MethodPost, pattern_MemoService_CreateMemo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.MemoService/CreateMemo", runtime.WithHTTPPathPattern("/api/v1/memos")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_MemoService_CreateMemo_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_CreateMemo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_MemoService_ListMemos_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.MemoService/ListMemos", runtime.WithHTTPPathPattern("/api/v1/memos")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_MemoService_ListMemos_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_ListMemos_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_MemoService_ListMemos_1, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.MemoService/ListMemos", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/memos")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_MemoService_ListMemos_1(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_ListMemos_1(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_MemoService_GetMemo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.MemoService/GetMemo", runtime.WithHTTPPathPattern("/api/v1/{name=memos/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_MemoService_GetMemo_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_GetMemo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPatch, pattern_MemoService_UpdateMemo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.MemoService/UpdateMemo", runtime.WithHTTPPathPattern("/api/v1/{memo.name=memos/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_MemoService_UpdateMemo_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_UpdateMemo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_MemoService_DeleteMemo_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.MemoService/DeleteMemo", runtime.WithHTTPPathPattern("/api/v1/{name=memos/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_MemoService_DeleteMemo_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_DeleteMemo_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPatch, pattern_MemoService_RenameMemoTag_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.MemoService/RenameMemoTag", runtime.WithHTTPPathPattern("/api/v1/{parent=memos/*}/tags:rename")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_MemoService_RenameMemoTag_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_RenameMemoTag_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_MemoService_DeleteMemoTag_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.MemoService/DeleteMemoTag", runtime.WithHTTPPathPattern("/api/v1/{parent=memos/*}/tags/{tag}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_MemoService_DeleteMemoTag_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_DeleteMemoTag_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPatch, pattern_MemoService_SetMemoAttachments_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.MemoService/SetMemoAttachments", runtime.WithHTTPPathPattern("/api/v1/{name=memos/*}/attachments")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_MemoService_SetMemoAttachments_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_SetMemoAttachments_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_MemoService_ListMemoAttachments_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.MemoService/ListMemoAttachments", runtime.WithHTTPPathPattern("/api/v1/{name=memos/*}/attachments")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_MemoService_ListMemoAttachments_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_ListMemoAttachments_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPatch, pattern_MemoService_SetMemoRelations_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.MemoService/SetMemoRelations", runtime.WithHTTPPathPattern("/api/v1/{name=memos/*}/relations")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_MemoService_SetMemoRelations_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_SetMemoRelations_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_MemoService_ListMemoRelations_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.MemoService/ListMemoRelations", runtime.WithHTTPPathPattern("/api/v1/{name=memos/*}/relations")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_MemoService_ListMemoRelations_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_ListMemoRelations_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_MemoService_CreateMemoComment_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.MemoService/CreateMemoComment", runtime.WithHTTPPathPattern("/api/v1/{name=memos/*}/comments")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_MemoService_CreateMemoComment_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_CreateMemoComment_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_MemoService_ListMemoComments_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.MemoService/ListMemoComments", runtime.WithHTTPPathPattern("/api/v1/{name=memos/*}/comments")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_MemoService_ListMemoComments_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_ListMemoComments_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_MemoService_ListMemoReactions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.MemoService/ListMemoReactions", runtime.WithHTTPPathPattern("/api/v1/{name=memos/*}/reactions")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_MemoService_ListMemoReactions_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_ListMemoReactions_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_MemoService_UpsertMemoReaction_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.MemoService/UpsertMemoReaction", runtime.WithHTTPPathPattern("/api/v1/{name=memos/*}/reactions")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_MemoService_UpsertMemoReaction_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_UpsertMemoReaction_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_MemoService_DeleteMemoReaction_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.MemoService/DeleteMemoReaction", runtime.WithHTTPPathPattern("/api/v1/{name=reactions/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_MemoService_DeleteMemoReaction_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_DeleteMemoReaction_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_MemoService_ExportMemos_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.MemoService/ExportMemos", runtime.WithHTTPPathPattern("/api/v1/memos:export")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_MemoService_ExportMemos_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_ExportMemos_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_MemoService_ImportMemos_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.MemoService/ImportMemos", runtime.WithHTTPPathPattern("/api/v1/memos:import")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_MemoService_ImportMemos_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_MemoService_ImportMemos_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + return nil +} + +var ( + pattern_MemoService_CreateMemo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "memos"}, "")) + pattern_MemoService_ListMemos_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "memos"}, "")) + pattern_MemoService_ListMemos_1 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "memos"}, "")) + pattern_MemoService_GetMemo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "memos", "name"}, "")) + pattern_MemoService_UpdateMemo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "memos", "memo.name"}, "")) + pattern_MemoService_DeleteMemo_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "memos", "name"}, "")) + pattern_MemoService_RenameMemoTag_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "memos", "parent", "tags"}, "rename")) + pattern_MemoService_DeleteMemoTag_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4, 1, 0, 4, 1, 5, 5}, []string{"api", "v1", "memos", "parent", "tags", "tag"}, "")) + pattern_MemoService_SetMemoAttachments_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "memos", "name", "attachments"}, "")) + pattern_MemoService_ListMemoAttachments_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "memos", "name", "attachments"}, "")) + pattern_MemoService_SetMemoRelations_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "memos", "name", "relations"}, "")) + pattern_MemoService_ListMemoRelations_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "memos", "name", "relations"}, "")) + pattern_MemoService_CreateMemoComment_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "memos", "name", "comments"}, "")) + pattern_MemoService_ListMemoComments_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "memos", "name", "comments"}, "")) + pattern_MemoService_ListMemoReactions_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "memos", "name", "reactions"}, "")) + pattern_MemoService_UpsertMemoReaction_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "memos", "name", "reactions"}, "")) + pattern_MemoService_DeleteMemoReaction_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "reactions", "name"}, "")) + pattern_MemoService_ExportMemos_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "memos"}, "export")) + pattern_MemoService_ImportMemos_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "memos"}, "import")) +) + +var ( + forward_MemoService_CreateMemo_0 = runtime.ForwardResponseMessage + forward_MemoService_ListMemos_0 = runtime.ForwardResponseMessage + forward_MemoService_ListMemos_1 = runtime.ForwardResponseMessage + forward_MemoService_GetMemo_0 = runtime.ForwardResponseMessage + forward_MemoService_UpdateMemo_0 = runtime.ForwardResponseMessage + forward_MemoService_DeleteMemo_0 = runtime.ForwardResponseMessage + forward_MemoService_RenameMemoTag_0 = runtime.ForwardResponseMessage + forward_MemoService_DeleteMemoTag_0 = runtime.ForwardResponseMessage + forward_MemoService_SetMemoAttachments_0 = runtime.ForwardResponseMessage + forward_MemoService_ListMemoAttachments_0 = runtime.ForwardResponseMessage + forward_MemoService_SetMemoRelations_0 = runtime.ForwardResponseMessage + forward_MemoService_ListMemoRelations_0 = runtime.ForwardResponseMessage + forward_MemoService_CreateMemoComment_0 = runtime.ForwardResponseMessage + forward_MemoService_ListMemoComments_0 = runtime.ForwardResponseMessage + forward_MemoService_ListMemoReactions_0 = runtime.ForwardResponseMessage + forward_MemoService_UpsertMemoReaction_0 = runtime.ForwardResponseMessage + forward_MemoService_DeleteMemoReaction_0 = runtime.ForwardResponseMessage + forward_MemoService_ExportMemos_0 = runtime.ForwardResponseMessage + forward_MemoService_ImportMemos_0 = runtime.ForwardResponseMessage +) diff --git a/proto/gen/api/v1/memo_service_grpc.pb.go b/proto/gen/api/v1/memo_service_grpc.pb.go new file mode 100644 index 0000000..1e0ff7c --- /dev/null +++ b/proto/gen/api/v1/memo_service_grpc.pb.go @@ -0,0 +1,804 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc (unknown) +// source: api/v1/memo_service.proto + +package apiv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + MemoService_CreateMemo_FullMethodName = "/memos.api.v1.MemoService/CreateMemo" + MemoService_ListMemos_FullMethodName = "/memos.api.v1.MemoService/ListMemos" + MemoService_GetMemo_FullMethodName = "/memos.api.v1.MemoService/GetMemo" + MemoService_UpdateMemo_FullMethodName = "/memos.api.v1.MemoService/UpdateMemo" + MemoService_DeleteMemo_FullMethodName = "/memos.api.v1.MemoService/DeleteMemo" + MemoService_RenameMemoTag_FullMethodName = "/memos.api.v1.MemoService/RenameMemoTag" + MemoService_DeleteMemoTag_FullMethodName = "/memos.api.v1.MemoService/DeleteMemoTag" + MemoService_SetMemoAttachments_FullMethodName = "/memos.api.v1.MemoService/SetMemoAttachments" + MemoService_ListMemoAttachments_FullMethodName = "/memos.api.v1.MemoService/ListMemoAttachments" + MemoService_SetMemoRelations_FullMethodName = "/memos.api.v1.MemoService/SetMemoRelations" + MemoService_ListMemoRelations_FullMethodName = "/memos.api.v1.MemoService/ListMemoRelations" + MemoService_CreateMemoComment_FullMethodName = "/memos.api.v1.MemoService/CreateMemoComment" + MemoService_ListMemoComments_FullMethodName = "/memos.api.v1.MemoService/ListMemoComments" + MemoService_ListMemoReactions_FullMethodName = "/memos.api.v1.MemoService/ListMemoReactions" + MemoService_UpsertMemoReaction_FullMethodName = "/memos.api.v1.MemoService/UpsertMemoReaction" + MemoService_DeleteMemoReaction_FullMethodName = "/memos.api.v1.MemoService/DeleteMemoReaction" + MemoService_ExportMemos_FullMethodName = "/memos.api.v1.MemoService/ExportMemos" + MemoService_ImportMemos_FullMethodName = "/memos.api.v1.MemoService/ImportMemos" +) + +// MemoServiceClient is the client API for MemoService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type MemoServiceClient interface { + // CreateMemo creates a memo. + CreateMemo(ctx context.Context, in *CreateMemoRequest, opts ...grpc.CallOption) (*Memo, error) + // ListMemos lists memos with pagination and filter. + ListMemos(ctx context.Context, in *ListMemosRequest, opts ...grpc.CallOption) (*ListMemosResponse, error) + // GetMemo gets a memo. + GetMemo(ctx context.Context, in *GetMemoRequest, opts ...grpc.CallOption) (*Memo, error) + // UpdateMemo updates a memo. + UpdateMemo(ctx context.Context, in *UpdateMemoRequest, opts ...grpc.CallOption) (*Memo, error) + // DeleteMemo deletes a memo. + DeleteMemo(ctx context.Context, in *DeleteMemoRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + // RenameMemoTag renames a tag for a memo. + RenameMemoTag(ctx context.Context, in *RenameMemoTagRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + // DeleteMemoTag deletes a tag for a memo. + DeleteMemoTag(ctx context.Context, in *DeleteMemoTagRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + // SetMemoAttachments sets attachments for a memo. + SetMemoAttachments(ctx context.Context, in *SetMemoAttachmentsRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + // ListMemoAttachments lists attachments for a memo. + ListMemoAttachments(ctx context.Context, in *ListMemoAttachmentsRequest, opts ...grpc.CallOption) (*ListMemoAttachmentsResponse, error) + // SetMemoRelations sets relations for a memo. + SetMemoRelations(ctx context.Context, in *SetMemoRelationsRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + // ListMemoRelations lists relations for a memo. + ListMemoRelations(ctx context.Context, in *ListMemoRelationsRequest, opts ...grpc.CallOption) (*ListMemoRelationsResponse, error) + // CreateMemoComment creates a comment for a memo. + CreateMemoComment(ctx context.Context, in *CreateMemoCommentRequest, opts ...grpc.CallOption) (*Memo, error) + // ListMemoComments lists comments for a memo. + ListMemoComments(ctx context.Context, in *ListMemoCommentsRequest, opts ...grpc.CallOption) (*ListMemoCommentsResponse, error) + // ListMemoReactions lists reactions for a memo. + ListMemoReactions(ctx context.Context, in *ListMemoReactionsRequest, opts ...grpc.CallOption) (*ListMemoReactionsResponse, error) + // UpsertMemoReaction upserts a reaction for a memo. + UpsertMemoReaction(ctx context.Context, in *UpsertMemoReactionRequest, opts ...grpc.CallOption) (*Reaction, error) + // DeleteMemoReaction deletes a reaction for a memo. + DeleteMemoReaction(ctx context.Context, in *DeleteMemoReactionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + // ExportMemos exports memos for the current user + ExportMemos(ctx context.Context, in *ExportMemosRequest, opts ...grpc.CallOption) (*ExportMemosResponse, error) + // ImportMemos imports memos from provided data + ImportMemos(ctx context.Context, in *ImportMemosRequest, opts ...grpc.CallOption) (*ImportMemosResponse, error) +} + +type memoServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewMemoServiceClient(cc grpc.ClientConnInterface) MemoServiceClient { + return &memoServiceClient{cc} +} + +func (c *memoServiceClient) CreateMemo(ctx context.Context, in *CreateMemoRequest, opts ...grpc.CallOption) (*Memo, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Memo) + err := c.cc.Invoke(ctx, MemoService_CreateMemo_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *memoServiceClient) ListMemos(ctx context.Context, in *ListMemosRequest, opts ...grpc.CallOption) (*ListMemosResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListMemosResponse) + err := c.cc.Invoke(ctx, MemoService_ListMemos_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *memoServiceClient) GetMemo(ctx context.Context, in *GetMemoRequest, opts ...grpc.CallOption) (*Memo, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Memo) + err := c.cc.Invoke(ctx, MemoService_GetMemo_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *memoServiceClient) UpdateMemo(ctx context.Context, in *UpdateMemoRequest, opts ...grpc.CallOption) (*Memo, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Memo) + err := c.cc.Invoke(ctx, MemoService_UpdateMemo_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *memoServiceClient) DeleteMemo(ctx context.Context, in *DeleteMemoRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, MemoService_DeleteMemo_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *memoServiceClient) RenameMemoTag(ctx context.Context, in *RenameMemoTagRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, MemoService_RenameMemoTag_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *memoServiceClient) DeleteMemoTag(ctx context.Context, in *DeleteMemoTagRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, MemoService_DeleteMemoTag_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *memoServiceClient) SetMemoAttachments(ctx context.Context, in *SetMemoAttachmentsRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, MemoService_SetMemoAttachments_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *memoServiceClient) ListMemoAttachments(ctx context.Context, in *ListMemoAttachmentsRequest, opts ...grpc.CallOption) (*ListMemoAttachmentsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListMemoAttachmentsResponse) + err := c.cc.Invoke(ctx, MemoService_ListMemoAttachments_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *memoServiceClient) SetMemoRelations(ctx context.Context, in *SetMemoRelationsRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, MemoService_SetMemoRelations_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *memoServiceClient) ListMemoRelations(ctx context.Context, in *ListMemoRelationsRequest, opts ...grpc.CallOption) (*ListMemoRelationsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListMemoRelationsResponse) + err := c.cc.Invoke(ctx, MemoService_ListMemoRelations_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *memoServiceClient) CreateMemoComment(ctx context.Context, in *CreateMemoCommentRequest, opts ...grpc.CallOption) (*Memo, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Memo) + err := c.cc.Invoke(ctx, MemoService_CreateMemoComment_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *memoServiceClient) ListMemoComments(ctx context.Context, in *ListMemoCommentsRequest, opts ...grpc.CallOption) (*ListMemoCommentsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListMemoCommentsResponse) + err := c.cc.Invoke(ctx, MemoService_ListMemoComments_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *memoServiceClient) ListMemoReactions(ctx context.Context, in *ListMemoReactionsRequest, opts ...grpc.CallOption) (*ListMemoReactionsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListMemoReactionsResponse) + err := c.cc.Invoke(ctx, MemoService_ListMemoReactions_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *memoServiceClient) UpsertMemoReaction(ctx context.Context, in *UpsertMemoReactionRequest, opts ...grpc.CallOption) (*Reaction, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Reaction) + err := c.cc.Invoke(ctx, MemoService_UpsertMemoReaction_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *memoServiceClient) DeleteMemoReaction(ctx context.Context, in *DeleteMemoReactionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, MemoService_DeleteMemoReaction_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *memoServiceClient) ExportMemos(ctx context.Context, in *ExportMemosRequest, opts ...grpc.CallOption) (*ExportMemosResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ExportMemosResponse) + err := c.cc.Invoke(ctx, MemoService_ExportMemos_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *memoServiceClient) ImportMemos(ctx context.Context, in *ImportMemosRequest, opts ...grpc.CallOption) (*ImportMemosResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ImportMemosResponse) + err := c.cc.Invoke(ctx, MemoService_ImportMemos_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// MemoServiceServer is the server API for MemoService service. +// All implementations must embed UnimplementedMemoServiceServer +// for forward compatibility. +type MemoServiceServer interface { + // CreateMemo creates a memo. + CreateMemo(context.Context, *CreateMemoRequest) (*Memo, error) + // ListMemos lists memos with pagination and filter. + ListMemos(context.Context, *ListMemosRequest) (*ListMemosResponse, error) + // GetMemo gets a memo. + GetMemo(context.Context, *GetMemoRequest) (*Memo, error) + // UpdateMemo updates a memo. + UpdateMemo(context.Context, *UpdateMemoRequest) (*Memo, error) + // DeleteMemo deletes a memo. + DeleteMemo(context.Context, *DeleteMemoRequest) (*emptypb.Empty, error) + // RenameMemoTag renames a tag for a memo. + RenameMemoTag(context.Context, *RenameMemoTagRequest) (*emptypb.Empty, error) + // DeleteMemoTag deletes a tag for a memo. + DeleteMemoTag(context.Context, *DeleteMemoTagRequest) (*emptypb.Empty, error) + // SetMemoAttachments sets attachments for a memo. + SetMemoAttachments(context.Context, *SetMemoAttachmentsRequest) (*emptypb.Empty, error) + // ListMemoAttachments lists attachments for a memo. + ListMemoAttachments(context.Context, *ListMemoAttachmentsRequest) (*ListMemoAttachmentsResponse, error) + // SetMemoRelations sets relations for a memo. + SetMemoRelations(context.Context, *SetMemoRelationsRequest) (*emptypb.Empty, error) + // ListMemoRelations lists relations for a memo. + ListMemoRelations(context.Context, *ListMemoRelationsRequest) (*ListMemoRelationsResponse, error) + // CreateMemoComment creates a comment for a memo. + CreateMemoComment(context.Context, *CreateMemoCommentRequest) (*Memo, error) + // ListMemoComments lists comments for a memo. + ListMemoComments(context.Context, *ListMemoCommentsRequest) (*ListMemoCommentsResponse, error) + // ListMemoReactions lists reactions for a memo. + ListMemoReactions(context.Context, *ListMemoReactionsRequest) (*ListMemoReactionsResponse, error) + // UpsertMemoReaction upserts a reaction for a memo. + UpsertMemoReaction(context.Context, *UpsertMemoReactionRequest) (*Reaction, error) + // DeleteMemoReaction deletes a reaction for a memo. + DeleteMemoReaction(context.Context, *DeleteMemoReactionRequest) (*emptypb.Empty, error) + // ExportMemos exports memos for the current user + ExportMemos(context.Context, *ExportMemosRequest) (*ExportMemosResponse, error) + // ImportMemos imports memos from provided data + ImportMemos(context.Context, *ImportMemosRequest) (*ImportMemosResponse, error) + mustEmbedUnimplementedMemoServiceServer() +} + +// UnimplementedMemoServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedMemoServiceServer struct{} + +func (UnimplementedMemoServiceServer) CreateMemo(context.Context, *CreateMemoRequest) (*Memo, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateMemo not implemented") +} +func (UnimplementedMemoServiceServer) ListMemos(context.Context, *ListMemosRequest) (*ListMemosResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListMemos not implemented") +} +func (UnimplementedMemoServiceServer) GetMemo(context.Context, *GetMemoRequest) (*Memo, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetMemo not implemented") +} +func (UnimplementedMemoServiceServer) UpdateMemo(context.Context, *UpdateMemoRequest) (*Memo, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateMemo not implemented") +} +func (UnimplementedMemoServiceServer) DeleteMemo(context.Context, *DeleteMemoRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteMemo not implemented") +} +func (UnimplementedMemoServiceServer) RenameMemoTag(context.Context, *RenameMemoTagRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method RenameMemoTag not implemented") +} +func (UnimplementedMemoServiceServer) DeleteMemoTag(context.Context, *DeleteMemoTagRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteMemoTag not implemented") +} +func (UnimplementedMemoServiceServer) SetMemoAttachments(context.Context, *SetMemoAttachmentsRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method SetMemoAttachments not implemented") +} +func (UnimplementedMemoServiceServer) ListMemoAttachments(context.Context, *ListMemoAttachmentsRequest) (*ListMemoAttachmentsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListMemoAttachments not implemented") +} +func (UnimplementedMemoServiceServer) SetMemoRelations(context.Context, *SetMemoRelationsRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method SetMemoRelations not implemented") +} +func (UnimplementedMemoServiceServer) ListMemoRelations(context.Context, *ListMemoRelationsRequest) (*ListMemoRelationsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListMemoRelations not implemented") +} +func (UnimplementedMemoServiceServer) CreateMemoComment(context.Context, *CreateMemoCommentRequest) (*Memo, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateMemoComment not implemented") +} +func (UnimplementedMemoServiceServer) ListMemoComments(context.Context, *ListMemoCommentsRequest) (*ListMemoCommentsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListMemoComments not implemented") +} +func (UnimplementedMemoServiceServer) ListMemoReactions(context.Context, *ListMemoReactionsRequest) (*ListMemoReactionsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListMemoReactions not implemented") +} +func (UnimplementedMemoServiceServer) UpsertMemoReaction(context.Context, *UpsertMemoReactionRequest) (*Reaction, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpsertMemoReaction not implemented") +} +func (UnimplementedMemoServiceServer) DeleteMemoReaction(context.Context, *DeleteMemoReactionRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteMemoReaction not implemented") +} +func (UnimplementedMemoServiceServer) ExportMemos(context.Context, *ExportMemosRequest) (*ExportMemosResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ExportMemos not implemented") +} +func (UnimplementedMemoServiceServer) ImportMemos(context.Context, *ImportMemosRequest) (*ImportMemosResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ImportMemos not implemented") +} +func (UnimplementedMemoServiceServer) mustEmbedUnimplementedMemoServiceServer() {} +func (UnimplementedMemoServiceServer) testEmbeddedByValue() {} + +// UnsafeMemoServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to MemoServiceServer will +// result in compilation errors. +type UnsafeMemoServiceServer interface { + mustEmbedUnimplementedMemoServiceServer() +} + +func RegisterMemoServiceServer(s grpc.ServiceRegistrar, srv MemoServiceServer) { + // If the following call pancis, it indicates UnimplementedMemoServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&MemoService_ServiceDesc, srv) +} + +func _MemoService_CreateMemo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateMemoRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MemoServiceServer).CreateMemo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MemoService_CreateMemo_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MemoServiceServer).CreateMemo(ctx, req.(*CreateMemoRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _MemoService_ListMemos_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListMemosRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MemoServiceServer).ListMemos(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MemoService_ListMemos_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MemoServiceServer).ListMemos(ctx, req.(*ListMemosRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _MemoService_GetMemo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetMemoRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MemoServiceServer).GetMemo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MemoService_GetMemo_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MemoServiceServer).GetMemo(ctx, req.(*GetMemoRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _MemoService_UpdateMemo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateMemoRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MemoServiceServer).UpdateMemo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MemoService_UpdateMemo_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MemoServiceServer).UpdateMemo(ctx, req.(*UpdateMemoRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _MemoService_DeleteMemo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteMemoRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MemoServiceServer).DeleteMemo(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MemoService_DeleteMemo_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MemoServiceServer).DeleteMemo(ctx, req.(*DeleteMemoRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _MemoService_RenameMemoTag_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RenameMemoTagRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MemoServiceServer).RenameMemoTag(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MemoService_RenameMemoTag_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MemoServiceServer).RenameMemoTag(ctx, req.(*RenameMemoTagRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _MemoService_DeleteMemoTag_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteMemoTagRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MemoServiceServer).DeleteMemoTag(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MemoService_DeleteMemoTag_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MemoServiceServer).DeleteMemoTag(ctx, req.(*DeleteMemoTagRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _MemoService_SetMemoAttachments_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SetMemoAttachmentsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MemoServiceServer).SetMemoAttachments(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MemoService_SetMemoAttachments_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MemoServiceServer).SetMemoAttachments(ctx, req.(*SetMemoAttachmentsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _MemoService_ListMemoAttachments_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListMemoAttachmentsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MemoServiceServer).ListMemoAttachments(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MemoService_ListMemoAttachments_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MemoServiceServer).ListMemoAttachments(ctx, req.(*ListMemoAttachmentsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _MemoService_SetMemoRelations_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SetMemoRelationsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MemoServiceServer).SetMemoRelations(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MemoService_SetMemoRelations_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MemoServiceServer).SetMemoRelations(ctx, req.(*SetMemoRelationsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _MemoService_ListMemoRelations_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListMemoRelationsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MemoServiceServer).ListMemoRelations(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MemoService_ListMemoRelations_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MemoServiceServer).ListMemoRelations(ctx, req.(*ListMemoRelationsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _MemoService_CreateMemoComment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateMemoCommentRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MemoServiceServer).CreateMemoComment(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MemoService_CreateMemoComment_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MemoServiceServer).CreateMemoComment(ctx, req.(*CreateMemoCommentRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _MemoService_ListMemoComments_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListMemoCommentsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MemoServiceServer).ListMemoComments(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MemoService_ListMemoComments_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MemoServiceServer).ListMemoComments(ctx, req.(*ListMemoCommentsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _MemoService_ListMemoReactions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListMemoReactionsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MemoServiceServer).ListMemoReactions(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MemoService_ListMemoReactions_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MemoServiceServer).ListMemoReactions(ctx, req.(*ListMemoReactionsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _MemoService_UpsertMemoReaction_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpsertMemoReactionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MemoServiceServer).UpsertMemoReaction(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MemoService_UpsertMemoReaction_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MemoServiceServer).UpsertMemoReaction(ctx, req.(*UpsertMemoReactionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _MemoService_DeleteMemoReaction_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteMemoReactionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MemoServiceServer).DeleteMemoReaction(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MemoService_DeleteMemoReaction_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MemoServiceServer).DeleteMemoReaction(ctx, req.(*DeleteMemoReactionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _MemoService_ExportMemos_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ExportMemosRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MemoServiceServer).ExportMemos(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MemoService_ExportMemos_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MemoServiceServer).ExportMemos(ctx, req.(*ExportMemosRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _MemoService_ImportMemos_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ImportMemosRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(MemoServiceServer).ImportMemos(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: MemoService_ImportMemos_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(MemoServiceServer).ImportMemos(ctx, req.(*ImportMemosRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// MemoService_ServiceDesc is the grpc.ServiceDesc for MemoService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var MemoService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "memos.api.v1.MemoService", + HandlerType: (*MemoServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CreateMemo", + Handler: _MemoService_CreateMemo_Handler, + }, + { + MethodName: "ListMemos", + Handler: _MemoService_ListMemos_Handler, + }, + { + MethodName: "GetMemo", + Handler: _MemoService_GetMemo_Handler, + }, + { + MethodName: "UpdateMemo", + Handler: _MemoService_UpdateMemo_Handler, + }, + { + MethodName: "DeleteMemo", + Handler: _MemoService_DeleteMemo_Handler, + }, + { + MethodName: "RenameMemoTag", + Handler: _MemoService_RenameMemoTag_Handler, + }, + { + MethodName: "DeleteMemoTag", + Handler: _MemoService_DeleteMemoTag_Handler, + }, + { + MethodName: "SetMemoAttachments", + Handler: _MemoService_SetMemoAttachments_Handler, + }, + { + MethodName: "ListMemoAttachments", + Handler: _MemoService_ListMemoAttachments_Handler, + }, + { + MethodName: "SetMemoRelations", + Handler: _MemoService_SetMemoRelations_Handler, + }, + { + MethodName: "ListMemoRelations", + Handler: _MemoService_ListMemoRelations_Handler, + }, + { + MethodName: "CreateMemoComment", + Handler: _MemoService_CreateMemoComment_Handler, + }, + { + MethodName: "ListMemoComments", + Handler: _MemoService_ListMemoComments_Handler, + }, + { + MethodName: "ListMemoReactions", + Handler: _MemoService_ListMemoReactions_Handler, + }, + { + MethodName: "UpsertMemoReaction", + Handler: _MemoService_UpsertMemoReaction_Handler, + }, + { + MethodName: "DeleteMemoReaction", + Handler: _MemoService_DeleteMemoReaction_Handler, + }, + { + MethodName: "ExportMemos", + Handler: _MemoService_ExportMemos_Handler, + }, + { + MethodName: "ImportMemos", + Handler: _MemoService_ImportMemos_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "api/v1/memo_service.proto", +} diff --git a/proto/gen/api/v1/shortcut_service.pb.go b/proto/gen/api/v1/shortcut_service.pb.go new file mode 100644 index 0000000..7c75683 --- /dev/null +++ b/proto/gen/api/v1/shortcut_service.pb.go @@ -0,0 +1,496 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: api/v1/shortcut_service.proto + +package apiv1 + +import ( + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + fieldmaskpb "google.golang.org/protobuf/types/known/fieldmaskpb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Shortcut struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The resource name of the shortcut. + // Format: users/{user}/shortcuts/{shortcut} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // The title of the shortcut. + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + // The filter expression for the shortcut. + Filter string `protobuf:"bytes,3,opt,name=filter,proto3" json:"filter,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Shortcut) Reset() { + *x = Shortcut{} + mi := &file_api_v1_shortcut_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Shortcut) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Shortcut) ProtoMessage() {} + +func (x *Shortcut) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_shortcut_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Shortcut.ProtoReflect.Descriptor instead. +func (*Shortcut) Descriptor() ([]byte, []int) { + return file_api_v1_shortcut_service_proto_rawDescGZIP(), []int{0} +} + +func (x *Shortcut) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Shortcut) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *Shortcut) GetFilter() string { + if x != nil { + return x.Filter + } + return "" +} + +type ListShortcutsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The parent resource where shortcuts are listed. + // Format: users/{user} + Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListShortcutsRequest) Reset() { + *x = ListShortcutsRequest{} + mi := &file_api_v1_shortcut_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListShortcutsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListShortcutsRequest) ProtoMessage() {} + +func (x *ListShortcutsRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_shortcut_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListShortcutsRequest.ProtoReflect.Descriptor instead. +func (*ListShortcutsRequest) Descriptor() ([]byte, []int) { + return file_api_v1_shortcut_service_proto_rawDescGZIP(), []int{1} +} + +func (x *ListShortcutsRequest) GetParent() string { + if x != nil { + return x.Parent + } + return "" +} + +type ListShortcutsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The list of shortcuts. + Shortcuts []*Shortcut `protobuf:"bytes,1,rep,name=shortcuts,proto3" json:"shortcuts,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListShortcutsResponse) Reset() { + *x = ListShortcutsResponse{} + mi := &file_api_v1_shortcut_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListShortcutsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListShortcutsResponse) ProtoMessage() {} + +func (x *ListShortcutsResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_shortcut_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListShortcutsResponse.ProtoReflect.Descriptor instead. +func (*ListShortcutsResponse) Descriptor() ([]byte, []int) { + return file_api_v1_shortcut_service_proto_rawDescGZIP(), []int{2} +} + +func (x *ListShortcutsResponse) GetShortcuts() []*Shortcut { + if x != nil { + return x.Shortcuts + } + return nil +} + +type GetShortcutRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The resource name of the shortcut to retrieve. + // Format: users/{user}/shortcuts/{shortcut} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetShortcutRequest) Reset() { + *x = GetShortcutRequest{} + mi := &file_api_v1_shortcut_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetShortcutRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetShortcutRequest) ProtoMessage() {} + +func (x *GetShortcutRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_shortcut_service_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetShortcutRequest.ProtoReflect.Descriptor instead. +func (*GetShortcutRequest) Descriptor() ([]byte, []int) { + return file_api_v1_shortcut_service_proto_rawDescGZIP(), []int{3} +} + +func (x *GetShortcutRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type CreateShortcutRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The parent resource where this shortcut will be created. + // Format: users/{user} + Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"` + // Required. The shortcut to create. + Shortcut *Shortcut `protobuf:"bytes,2,opt,name=shortcut,proto3" json:"shortcut,omitempty"` + // Optional. If set, validate the request, but do not actually create the shortcut. + ValidateOnly bool `protobuf:"varint,3,opt,name=validate_only,json=validateOnly,proto3" json:"validate_only,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateShortcutRequest) Reset() { + *x = CreateShortcutRequest{} + mi := &file_api_v1_shortcut_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateShortcutRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateShortcutRequest) ProtoMessage() {} + +func (x *CreateShortcutRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_shortcut_service_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateShortcutRequest.ProtoReflect.Descriptor instead. +func (*CreateShortcutRequest) Descriptor() ([]byte, []int) { + return file_api_v1_shortcut_service_proto_rawDescGZIP(), []int{4} +} + +func (x *CreateShortcutRequest) GetParent() string { + if x != nil { + return x.Parent + } + return "" +} + +func (x *CreateShortcutRequest) GetShortcut() *Shortcut { + if x != nil { + return x.Shortcut + } + return nil +} + +func (x *CreateShortcutRequest) GetValidateOnly() bool { + if x != nil { + return x.ValidateOnly + } + return false +} + +type UpdateShortcutRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The shortcut resource which replaces the resource on the server. + Shortcut *Shortcut `protobuf:"bytes,1,opt,name=shortcut,proto3" json:"shortcut,omitempty"` + // Optional. The list of fields to update. + UpdateMask *fieldmaskpb.FieldMask `protobuf:"bytes,2,opt,name=update_mask,json=updateMask,proto3" json:"update_mask,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateShortcutRequest) Reset() { + *x = UpdateShortcutRequest{} + mi := &file_api_v1_shortcut_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateShortcutRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateShortcutRequest) ProtoMessage() {} + +func (x *UpdateShortcutRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_shortcut_service_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateShortcutRequest.ProtoReflect.Descriptor instead. +func (*UpdateShortcutRequest) Descriptor() ([]byte, []int) { + return file_api_v1_shortcut_service_proto_rawDescGZIP(), []int{5} +} + +func (x *UpdateShortcutRequest) GetShortcut() *Shortcut { + if x != nil { + return x.Shortcut + } + return nil +} + +func (x *UpdateShortcutRequest) GetUpdateMask() *fieldmaskpb.FieldMask { + if x != nil { + return x.UpdateMask + } + return nil +} + +type DeleteShortcutRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The resource name of the shortcut to delete. + // Format: users/{user}/shortcuts/{shortcut} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteShortcutRequest) Reset() { + *x = DeleteShortcutRequest{} + mi := &file_api_v1_shortcut_service_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteShortcutRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteShortcutRequest) ProtoMessage() {} + +func (x *DeleteShortcutRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_shortcut_service_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteShortcutRequest.ProtoReflect.Descriptor instead. +func (*DeleteShortcutRequest) Descriptor() ([]byte, []int) { + return file_api_v1_shortcut_service_proto_rawDescGZIP(), []int{6} +} + +func (x *DeleteShortcutRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +var File_api_v1_shortcut_service_proto protoreflect.FileDescriptor + +const file_api_v1_shortcut_service_proto_rawDesc = "" + + "\n" + + "\x1dapi/v1/shortcut_service.proto\x12\fmemos.api.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\"\xaf\x01\n" + + "\bShortcut\x12\x17\n" + + "\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12\x19\n" + + "\x05title\x18\x02 \x01(\tB\x03\xe0A\x02R\x05title\x12\x1b\n" + + "\x06filter\x18\x03 \x01(\tB\x03\xe0A\x01R\x06filter:R\xeaAO\n" + + "\x15memos.api.v1/Shortcut\x12!users/{user}/shortcuts/{shortcut}*\tshortcuts2\bshortcut\"M\n" + + "\x14ListShortcutsRequest\x125\n" + + "\x06parent\x18\x01 \x01(\tB\x1d\xe0A\x02\xfaA\x17\x12\x15memos.api.v1/ShortcutR\x06parent\"M\n" + + "\x15ListShortcutsResponse\x124\n" + + "\tshortcuts\x18\x01 \x03(\v2\x16.memos.api.v1.ShortcutR\tshortcuts\"G\n" + + "\x12GetShortcutRequest\x121\n" + + "\x04name\x18\x01 \x01(\tB\x1d\xe0A\x02\xfaA\x17\n" + + "\x15memos.api.v1/ShortcutR\x04name\"\xb1\x01\n" + + "\x15CreateShortcutRequest\x125\n" + + "\x06parent\x18\x01 \x01(\tB\x1d\xe0A\x02\xfaA\x17\x12\x15memos.api.v1/ShortcutR\x06parent\x127\n" + + "\bshortcut\x18\x02 \x01(\v2\x16.memos.api.v1.ShortcutB\x03\xe0A\x02R\bshortcut\x12(\n" + + "\rvalidate_only\x18\x03 \x01(\bB\x03\xe0A\x01R\fvalidateOnly\"\x92\x01\n" + + "\x15UpdateShortcutRequest\x127\n" + + "\bshortcut\x18\x01 \x01(\v2\x16.memos.api.v1.ShortcutB\x03\xe0A\x02R\bshortcut\x12@\n" + + "\vupdate_mask\x18\x02 \x01(\v2\x1a.google.protobuf.FieldMaskB\x03\xe0A\x01R\n" + + "updateMask\"J\n" + + "\x15DeleteShortcutRequest\x121\n" + + "\x04name\x18\x01 \x01(\tB\x1d\xe0A\x02\xfaA\x17\n" + + "\x15memos.api.v1/ShortcutR\x04name2\xde\x05\n" + + "\x0fShortcutService\x12\x8d\x01\n" + + "\rListShortcuts\x12\".memos.api.v1.ListShortcutsRequest\x1a#.memos.api.v1.ListShortcutsResponse\"3\xdaA\x06parent\x82\xd3\xe4\x93\x02$\x12\"/api/v1/{parent=users/*}/shortcuts\x12z\n" + + "\vGetShortcut\x12 .memos.api.v1.GetShortcutRequest\x1a\x16.memos.api.v1.Shortcut\"1\xdaA\x04name\x82\xd3\xe4\x93\x02$\x12\"/api/v1/{name=users/*/shortcuts/*}\x12\x95\x01\n" + + "\x0eCreateShortcut\x12#.memos.api.v1.CreateShortcutRequest\x1a\x16.memos.api.v1.Shortcut\"F\xdaA\x0fparent,shortcut\x82\xd3\xe4\x93\x02.:\bshortcut\"\"/api/v1/{parent=users/*}/shortcuts\x12\xa3\x01\n" + + "\x0eUpdateShortcut\x12#.memos.api.v1.UpdateShortcutRequest\x1a\x16.memos.api.v1.Shortcut\"T\xdaA\x14shortcut,update_mask\x82\xd3\xe4\x93\x027:\bshortcut2+/api/v1/{shortcut.name=users/*/shortcuts/*}\x12\x80\x01\n" + + "\x0eDeleteShortcut\x12#.memos.api.v1.DeleteShortcutRequest\x1a\x16.google.protobuf.Empty\"1\xdaA\x04name\x82\xd3\xe4\x93\x02$*\"/api/v1/{name=users/*/shortcuts/*}B\xac\x01\n" + + "\x10com.memos.api.v1B\x14ShortcutServiceProtoP\x01Z0github.com/usememos/memos/proto/gen/api/v1;apiv1\xa2\x02\x03MAX\xaa\x02\fMemos.Api.V1\xca\x02\fMemos\\Api\\V1\xe2\x02\x18Memos\\Api\\V1\\GPBMetadata\xea\x02\x0eMemos::Api::V1b\x06proto3" + +var ( + file_api_v1_shortcut_service_proto_rawDescOnce sync.Once + file_api_v1_shortcut_service_proto_rawDescData []byte +) + +func file_api_v1_shortcut_service_proto_rawDescGZIP() []byte { + file_api_v1_shortcut_service_proto_rawDescOnce.Do(func() { + file_api_v1_shortcut_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_v1_shortcut_service_proto_rawDesc), len(file_api_v1_shortcut_service_proto_rawDesc))) + }) + return file_api_v1_shortcut_service_proto_rawDescData +} + +var file_api_v1_shortcut_service_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_api_v1_shortcut_service_proto_goTypes = []any{ + (*Shortcut)(nil), // 0: memos.api.v1.Shortcut + (*ListShortcutsRequest)(nil), // 1: memos.api.v1.ListShortcutsRequest + (*ListShortcutsResponse)(nil), // 2: memos.api.v1.ListShortcutsResponse + (*GetShortcutRequest)(nil), // 3: memos.api.v1.GetShortcutRequest + (*CreateShortcutRequest)(nil), // 4: memos.api.v1.CreateShortcutRequest + (*UpdateShortcutRequest)(nil), // 5: memos.api.v1.UpdateShortcutRequest + (*DeleteShortcutRequest)(nil), // 6: memos.api.v1.DeleteShortcutRequest + (*fieldmaskpb.FieldMask)(nil), // 7: google.protobuf.FieldMask + (*emptypb.Empty)(nil), // 8: google.protobuf.Empty +} +var file_api_v1_shortcut_service_proto_depIdxs = []int32{ + 0, // 0: memos.api.v1.ListShortcutsResponse.shortcuts:type_name -> memos.api.v1.Shortcut + 0, // 1: memos.api.v1.CreateShortcutRequest.shortcut:type_name -> memos.api.v1.Shortcut + 0, // 2: memos.api.v1.UpdateShortcutRequest.shortcut:type_name -> memos.api.v1.Shortcut + 7, // 3: memos.api.v1.UpdateShortcutRequest.update_mask:type_name -> google.protobuf.FieldMask + 1, // 4: memos.api.v1.ShortcutService.ListShortcuts:input_type -> memos.api.v1.ListShortcutsRequest + 3, // 5: memos.api.v1.ShortcutService.GetShortcut:input_type -> memos.api.v1.GetShortcutRequest + 4, // 6: memos.api.v1.ShortcutService.CreateShortcut:input_type -> memos.api.v1.CreateShortcutRequest + 5, // 7: memos.api.v1.ShortcutService.UpdateShortcut:input_type -> memos.api.v1.UpdateShortcutRequest + 6, // 8: memos.api.v1.ShortcutService.DeleteShortcut:input_type -> memos.api.v1.DeleteShortcutRequest + 2, // 9: memos.api.v1.ShortcutService.ListShortcuts:output_type -> memos.api.v1.ListShortcutsResponse + 0, // 10: memos.api.v1.ShortcutService.GetShortcut:output_type -> memos.api.v1.Shortcut + 0, // 11: memos.api.v1.ShortcutService.CreateShortcut:output_type -> memos.api.v1.Shortcut + 0, // 12: memos.api.v1.ShortcutService.UpdateShortcut:output_type -> memos.api.v1.Shortcut + 8, // 13: memos.api.v1.ShortcutService.DeleteShortcut:output_type -> google.protobuf.Empty + 9, // [9:14] is the sub-list for method output_type + 4, // [4:9] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_api_v1_shortcut_service_proto_init() } +func file_api_v1_shortcut_service_proto_init() { + if File_api_v1_shortcut_service_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_shortcut_service_proto_rawDesc), len(file_api_v1_shortcut_service_proto_rawDesc)), + NumEnums: 0, + NumMessages: 7, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_api_v1_shortcut_service_proto_goTypes, + DependencyIndexes: file_api_v1_shortcut_service_proto_depIdxs, + MessageInfos: file_api_v1_shortcut_service_proto_msgTypes, + }.Build() + File_api_v1_shortcut_service_proto = out.File + file_api_v1_shortcut_service_proto_goTypes = nil + file_api_v1_shortcut_service_proto_depIdxs = nil +} diff --git a/proto/gen/api/v1/shortcut_service.pb.gw.go b/proto/gen/api/v1/shortcut_service.pb.gw.go new file mode 100644 index 0000000..0e15ae2 --- /dev/null +++ b/proto/gen/api/v1/shortcut_service.pb.gw.go @@ -0,0 +1,543 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: api/v1/shortcut_service.proto + +/* +Package apiv1 is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package apiv1 + +import ( + "context" + "errors" + "io" + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// Suppress "imported and not used" errors +var ( + _ codes.Code + _ io.Reader + _ status.Status + _ = errors.New + _ = runtime.String + _ = utilities.NewDoubleArray + _ = metadata.Join +) + +func request_ShortcutService_ListShortcuts_0(ctx context.Context, marshaler runtime.Marshaler, client ShortcutServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListShortcutsRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } + msg, err := client.ListShortcuts(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_ShortcutService_ListShortcuts_0(ctx context.Context, marshaler runtime.Marshaler, server ShortcutServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListShortcutsRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } + msg, err := server.ListShortcuts(ctx, &protoReq) + return msg, metadata, err +} + +func request_ShortcutService_GetShortcut_0(ctx context.Context, marshaler runtime.Marshaler, client ShortcutServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetShortcutRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := client.GetShortcut(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_ShortcutService_GetShortcut_0(ctx context.Context, marshaler runtime.Marshaler, server ShortcutServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetShortcutRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := server.GetShortcut(ctx, &protoReq) + return msg, metadata, err +} + +var filter_ShortcutService_CreateShortcut_0 = &utilities.DoubleArray{Encoding: map[string]int{"shortcut": 0, "parent": 1}, Base: []int{1, 1, 2, 0, 0}, Check: []int{0, 1, 1, 2, 3}} + +func request_ShortcutService_CreateShortcut_0(ctx context.Context, marshaler runtime.Marshaler, client ShortcutServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateShortcutRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Shortcut); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ShortcutService_CreateShortcut_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.CreateShortcut(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_ShortcutService_CreateShortcut_0(ctx context.Context, marshaler runtime.Marshaler, server ShortcutServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateShortcutRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Shortcut); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ShortcutService_CreateShortcut_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.CreateShortcut(ctx, &protoReq) + return msg, metadata, err +} + +var filter_ShortcutService_UpdateShortcut_0 = &utilities.DoubleArray{Encoding: map[string]int{"shortcut": 0, "name": 1}, Base: []int{1, 2, 1, 0, 0}, Check: []int{0, 1, 2, 3, 2}} + +func request_ShortcutService_UpdateShortcut_0(ctx context.Context, marshaler runtime.Marshaler, client ShortcutServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateShortcutRequest + metadata runtime.ServerMetadata + err error + ) + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Shortcut); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 { + if fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Shortcut); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } else { + protoReq.UpdateMask = fieldMask + } + } + val, ok := pathParams["shortcut.name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "shortcut.name") + } + err = runtime.PopulateFieldFromPath(&protoReq, "shortcut.name", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "shortcut.name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ShortcutService_UpdateShortcut_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.UpdateShortcut(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_ShortcutService_UpdateShortcut_0(ctx context.Context, marshaler runtime.Marshaler, server ShortcutServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateShortcutRequest + metadata runtime.ServerMetadata + err error + ) + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Shortcut); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 { + if fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Shortcut); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } else { + protoReq.UpdateMask = fieldMask + } + } + val, ok := pathParams["shortcut.name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "shortcut.name") + } + err = runtime.PopulateFieldFromPath(&protoReq, "shortcut.name", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "shortcut.name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_ShortcutService_UpdateShortcut_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.UpdateShortcut(ctx, &protoReq) + return msg, metadata, err +} + +func request_ShortcutService_DeleteShortcut_0(ctx context.Context, marshaler runtime.Marshaler, client ShortcutServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteShortcutRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := client.DeleteShortcut(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_ShortcutService_DeleteShortcut_0(ctx context.Context, marshaler runtime.Marshaler, server ShortcutServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteShortcutRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := server.DeleteShortcut(ctx, &protoReq) + return msg, metadata, err +} + +// RegisterShortcutServiceHandlerServer registers the http handlers for service ShortcutService to "mux". +// UnaryRPC :call ShortcutServiceServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterShortcutServiceHandlerFromEndpoint instead. +// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call. +func RegisterShortcutServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server ShortcutServiceServer) error { + mux.Handle(http.MethodGet, pattern_ShortcutService_ListShortcuts_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.ShortcutService/ListShortcuts", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/shortcuts")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ShortcutService_ListShortcuts_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ShortcutService_ListShortcuts_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_ShortcutService_GetShortcut_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.ShortcutService/GetShortcut", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/shortcuts/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ShortcutService_GetShortcut_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ShortcutService_GetShortcut_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_ShortcutService_CreateShortcut_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.ShortcutService/CreateShortcut", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/shortcuts")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ShortcutService_CreateShortcut_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ShortcutService_CreateShortcut_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPatch, pattern_ShortcutService_UpdateShortcut_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.ShortcutService/UpdateShortcut", runtime.WithHTTPPathPattern("/api/v1/{shortcut.name=users/*/shortcuts/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ShortcutService_UpdateShortcut_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ShortcutService_UpdateShortcut_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_ShortcutService_DeleteShortcut_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.ShortcutService/DeleteShortcut", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/shortcuts/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_ShortcutService_DeleteShortcut_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ShortcutService_DeleteShortcut_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + + return nil +} + +// RegisterShortcutServiceHandlerFromEndpoint is same as RegisterShortcutServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterShortcutServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.NewClient(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + return RegisterShortcutServiceHandler(ctx, mux, conn) +} + +// RegisterShortcutServiceHandler registers the http handlers for service ShortcutService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterShortcutServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterShortcutServiceHandlerClient(ctx, mux, NewShortcutServiceClient(conn)) +} + +// RegisterShortcutServiceHandlerClient registers the http handlers for service ShortcutService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "ShortcutServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "ShortcutServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "ShortcutServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares. +func RegisterShortcutServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client ShortcutServiceClient) error { + mux.Handle(http.MethodGet, pattern_ShortcutService_ListShortcuts_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.ShortcutService/ListShortcuts", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/shortcuts")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ShortcutService_ListShortcuts_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ShortcutService_ListShortcuts_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_ShortcutService_GetShortcut_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.ShortcutService/GetShortcut", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/shortcuts/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ShortcutService_GetShortcut_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ShortcutService_GetShortcut_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_ShortcutService_CreateShortcut_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.ShortcutService/CreateShortcut", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/shortcuts")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ShortcutService_CreateShortcut_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ShortcutService_CreateShortcut_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPatch, pattern_ShortcutService_UpdateShortcut_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.ShortcutService/UpdateShortcut", runtime.WithHTTPPathPattern("/api/v1/{shortcut.name=users/*/shortcuts/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ShortcutService_UpdateShortcut_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ShortcutService_UpdateShortcut_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_ShortcutService_DeleteShortcut_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.ShortcutService/DeleteShortcut", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/shortcuts/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_ShortcutService_DeleteShortcut_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_ShortcutService_DeleteShortcut_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + return nil +} + +var ( + pattern_ShortcutService_ListShortcuts_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "shortcuts"}, "")) + pattern_ShortcutService_GetShortcut_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "shortcuts", "name"}, "")) + pattern_ShortcutService_CreateShortcut_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "shortcuts"}, "")) + pattern_ShortcutService_UpdateShortcut_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "shortcuts", "shortcut.name"}, "")) + pattern_ShortcutService_DeleteShortcut_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "shortcuts", "name"}, "")) +) + +var ( + forward_ShortcutService_ListShortcuts_0 = runtime.ForwardResponseMessage + forward_ShortcutService_GetShortcut_0 = runtime.ForwardResponseMessage + forward_ShortcutService_CreateShortcut_0 = runtime.ForwardResponseMessage + forward_ShortcutService_UpdateShortcut_0 = runtime.ForwardResponseMessage + forward_ShortcutService_DeleteShortcut_0 = runtime.ForwardResponseMessage +) diff --git a/proto/gen/api/v1/shortcut_service_grpc.pb.go b/proto/gen/api/v1/shortcut_service_grpc.pb.go new file mode 100644 index 0000000..3a3f5b8 --- /dev/null +++ b/proto/gen/api/v1/shortcut_service_grpc.pb.go @@ -0,0 +1,284 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc (unknown) +// source: api/v1/shortcut_service.proto + +package apiv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + ShortcutService_ListShortcuts_FullMethodName = "/memos.api.v1.ShortcutService/ListShortcuts" + ShortcutService_GetShortcut_FullMethodName = "/memos.api.v1.ShortcutService/GetShortcut" + ShortcutService_CreateShortcut_FullMethodName = "/memos.api.v1.ShortcutService/CreateShortcut" + ShortcutService_UpdateShortcut_FullMethodName = "/memos.api.v1.ShortcutService/UpdateShortcut" + ShortcutService_DeleteShortcut_FullMethodName = "/memos.api.v1.ShortcutService/DeleteShortcut" +) + +// ShortcutServiceClient is the client API for ShortcutService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type ShortcutServiceClient interface { + // ListShortcuts returns a list of shortcuts for a user. + ListShortcuts(ctx context.Context, in *ListShortcutsRequest, opts ...grpc.CallOption) (*ListShortcutsResponse, error) + // GetShortcut gets a shortcut by name. + GetShortcut(ctx context.Context, in *GetShortcutRequest, opts ...grpc.CallOption) (*Shortcut, error) + // CreateShortcut creates a new shortcut for a user. + CreateShortcut(ctx context.Context, in *CreateShortcutRequest, opts ...grpc.CallOption) (*Shortcut, error) + // UpdateShortcut updates a shortcut for a user. + UpdateShortcut(ctx context.Context, in *UpdateShortcutRequest, opts ...grpc.CallOption) (*Shortcut, error) + // DeleteShortcut deletes a shortcut for a user. + DeleteShortcut(ctx context.Context, in *DeleteShortcutRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +type shortcutServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewShortcutServiceClient(cc grpc.ClientConnInterface) ShortcutServiceClient { + return &shortcutServiceClient{cc} +} + +func (c *shortcutServiceClient) ListShortcuts(ctx context.Context, in *ListShortcutsRequest, opts ...grpc.CallOption) (*ListShortcutsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListShortcutsResponse) + err := c.cc.Invoke(ctx, ShortcutService_ListShortcuts_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *shortcutServiceClient) GetShortcut(ctx context.Context, in *GetShortcutRequest, opts ...grpc.CallOption) (*Shortcut, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Shortcut) + err := c.cc.Invoke(ctx, ShortcutService_GetShortcut_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *shortcutServiceClient) CreateShortcut(ctx context.Context, in *CreateShortcutRequest, opts ...grpc.CallOption) (*Shortcut, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Shortcut) + err := c.cc.Invoke(ctx, ShortcutService_CreateShortcut_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *shortcutServiceClient) UpdateShortcut(ctx context.Context, in *UpdateShortcutRequest, opts ...grpc.CallOption) (*Shortcut, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Shortcut) + err := c.cc.Invoke(ctx, ShortcutService_UpdateShortcut_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *shortcutServiceClient) DeleteShortcut(ctx context.Context, in *DeleteShortcutRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, ShortcutService_DeleteShortcut_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// ShortcutServiceServer is the server API for ShortcutService service. +// All implementations must embed UnimplementedShortcutServiceServer +// for forward compatibility. +type ShortcutServiceServer interface { + // ListShortcuts returns a list of shortcuts for a user. + ListShortcuts(context.Context, *ListShortcutsRequest) (*ListShortcutsResponse, error) + // GetShortcut gets a shortcut by name. + GetShortcut(context.Context, *GetShortcutRequest) (*Shortcut, error) + // CreateShortcut creates a new shortcut for a user. + CreateShortcut(context.Context, *CreateShortcutRequest) (*Shortcut, error) + // UpdateShortcut updates a shortcut for a user. + UpdateShortcut(context.Context, *UpdateShortcutRequest) (*Shortcut, error) + // DeleteShortcut deletes a shortcut for a user. + DeleteShortcut(context.Context, *DeleteShortcutRequest) (*emptypb.Empty, error) + mustEmbedUnimplementedShortcutServiceServer() +} + +// UnimplementedShortcutServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedShortcutServiceServer struct{} + +func (UnimplementedShortcutServiceServer) ListShortcuts(context.Context, *ListShortcutsRequest) (*ListShortcutsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListShortcuts not implemented") +} +func (UnimplementedShortcutServiceServer) GetShortcut(context.Context, *GetShortcutRequest) (*Shortcut, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetShortcut not implemented") +} +func (UnimplementedShortcutServiceServer) CreateShortcut(context.Context, *CreateShortcutRequest) (*Shortcut, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateShortcut not implemented") +} +func (UnimplementedShortcutServiceServer) UpdateShortcut(context.Context, *UpdateShortcutRequest) (*Shortcut, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateShortcut not implemented") +} +func (UnimplementedShortcutServiceServer) DeleteShortcut(context.Context, *DeleteShortcutRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteShortcut not implemented") +} +func (UnimplementedShortcutServiceServer) mustEmbedUnimplementedShortcutServiceServer() {} +func (UnimplementedShortcutServiceServer) testEmbeddedByValue() {} + +// UnsafeShortcutServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to ShortcutServiceServer will +// result in compilation errors. +type UnsafeShortcutServiceServer interface { + mustEmbedUnimplementedShortcutServiceServer() +} + +func RegisterShortcutServiceServer(s grpc.ServiceRegistrar, srv ShortcutServiceServer) { + // If the following call pancis, it indicates UnimplementedShortcutServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&ShortcutService_ServiceDesc, srv) +} + +func _ShortcutService_ListShortcuts_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListShortcutsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ShortcutServiceServer).ListShortcuts(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ShortcutService_ListShortcuts_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ShortcutServiceServer).ListShortcuts(ctx, req.(*ListShortcutsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ShortcutService_GetShortcut_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetShortcutRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ShortcutServiceServer).GetShortcut(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ShortcutService_GetShortcut_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ShortcutServiceServer).GetShortcut(ctx, req.(*GetShortcutRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ShortcutService_CreateShortcut_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateShortcutRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ShortcutServiceServer).CreateShortcut(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ShortcutService_CreateShortcut_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ShortcutServiceServer).CreateShortcut(ctx, req.(*CreateShortcutRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ShortcutService_UpdateShortcut_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateShortcutRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ShortcutServiceServer).UpdateShortcut(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ShortcutService_UpdateShortcut_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ShortcutServiceServer).UpdateShortcut(ctx, req.(*UpdateShortcutRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ShortcutService_DeleteShortcut_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteShortcutRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ShortcutServiceServer).DeleteShortcut(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: ShortcutService_DeleteShortcut_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ShortcutServiceServer).DeleteShortcut(ctx, req.(*DeleteShortcutRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// ShortcutService_ServiceDesc is the grpc.ServiceDesc for ShortcutService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var ShortcutService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "memos.api.v1.ShortcutService", + HandlerType: (*ShortcutServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ListShortcuts", + Handler: _ShortcutService_ListShortcuts_Handler, + }, + { + MethodName: "GetShortcut", + Handler: _ShortcutService_GetShortcut_Handler, + }, + { + MethodName: "CreateShortcut", + Handler: _ShortcutService_CreateShortcut_Handler, + }, + { + MethodName: "UpdateShortcut", + Handler: _ShortcutService_UpdateShortcut_Handler, + }, + { + MethodName: "DeleteShortcut", + Handler: _ShortcutService_DeleteShortcut_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "api/v1/shortcut_service.proto", +} diff --git a/proto/gen/api/v1/user_service.pb.go b/proto/gen/api/v1/user_service.pb.go new file mode 100644 index 0000000..5bf6833 --- /dev/null +++ b/proto/gen/api/v1/user_service.pb.go @@ -0,0 +1,2262 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: api/v1/user_service.proto + +package apiv1 + +import ( + _ "google.golang.org/genproto/googleapis/api/annotations" + httpbody "google.golang.org/genproto/googleapis/api/httpbody" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + fieldmaskpb "google.golang.org/protobuf/types/known/fieldmaskpb" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// User role enumeration. +type User_Role int32 + +const ( + // Unspecified role. + User_ROLE_UNSPECIFIED User_Role = 0 + // Host role with full system access. + User_HOST User_Role = 1 + // Admin role with administrative privileges. + User_ADMIN User_Role = 2 + // Regular user role. + User_USER User_Role = 3 +) + +// Enum value maps for User_Role. +var ( + User_Role_name = map[int32]string{ + 0: "ROLE_UNSPECIFIED", + 1: "HOST", + 2: "ADMIN", + 3: "USER", + } + User_Role_value = map[string]int32{ + "ROLE_UNSPECIFIED": 0, + "HOST": 1, + "ADMIN": 2, + "USER": 3, + } +) + +func (x User_Role) Enum() *User_Role { + p := new(User_Role) + *p = x + return p +} + +func (x User_Role) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (User_Role) Descriptor() protoreflect.EnumDescriptor { + return file_api_v1_user_service_proto_enumTypes[0].Descriptor() +} + +func (User_Role) Type() protoreflect.EnumType { + return &file_api_v1_user_service_proto_enumTypes[0] +} + +func (x User_Role) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use User_Role.Descriptor instead. +func (User_Role) EnumDescriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{0, 0} +} + +type User struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The resource name of the user. + // Format: users/{user} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // The role of the user. + Role User_Role `protobuf:"varint,2,opt,name=role,proto3,enum=memos.api.v1.User_Role" json:"role,omitempty"` + // Required. The unique username for login. + Username string `protobuf:"bytes,3,opt,name=username,proto3" json:"username,omitempty"` + // Optional. The email address of the user. + Email string `protobuf:"bytes,4,opt,name=email,proto3" json:"email,omitempty"` + // Optional. The display name of the user. + DisplayName string `protobuf:"bytes,5,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + // Optional. The avatar URL of the user. + AvatarUrl string `protobuf:"bytes,6,opt,name=avatar_url,json=avatarUrl,proto3" json:"avatar_url,omitempty"` + // Optional. The description of the user. + Description string `protobuf:"bytes,7,opt,name=description,proto3" json:"description,omitempty"` + // Input only. The password for the user. + Password string `protobuf:"bytes,8,opt,name=password,proto3" json:"password,omitempty"` + // The state of the user. + State State `protobuf:"varint,9,opt,name=state,proto3,enum=memos.api.v1.State" json:"state,omitempty"` + // Output only. The creation timestamp. + CreateTime *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` + // Output only. The last update timestamp. + UpdateTime *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=update_time,json=updateTime,proto3" json:"update_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *User) Reset() { + *x = User{} + mi := &file_api_v1_user_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *User) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*User) ProtoMessage() {} + +func (x *User) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_user_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use User.ProtoReflect.Descriptor instead. +func (*User) Descriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{0} +} + +func (x *User) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *User) GetRole() User_Role { + if x != nil { + return x.Role + } + return User_ROLE_UNSPECIFIED +} + +func (x *User) GetUsername() string { + if x != nil { + return x.Username + } + return "" +} + +func (x *User) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *User) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + +func (x *User) GetAvatarUrl() string { + if x != nil { + return x.AvatarUrl + } + return "" +} + +func (x *User) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *User) GetPassword() string { + if x != nil { + return x.Password + } + return "" +} + +func (x *User) GetState() State { + if x != nil { + return x.State + } + return State_STATE_UNSPECIFIED +} + +func (x *User) GetCreateTime() *timestamppb.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *User) GetUpdateTime() *timestamppb.Timestamp { + if x != nil { + return x.UpdateTime + } + return nil +} + +type ListUsersRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional. The maximum number of users to return. + // The service may return fewer than this value. + // If unspecified, at most 50 users will be returned. + // The maximum value is 1000; values above 1000 will be coerced to 1000. + PageSize int32 `protobuf:"varint,1,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Optional. A page token, received from a previous `ListUsers` call. + // Provide this to retrieve the subsequent page. + PageToken string `protobuf:"bytes,2,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + // Optional. Filter to apply to the list results. + // Example: "state=ACTIVE" or "role=USER" or "email:@example.com" + // Supported operators: =, !=, <, <=, >, >=, : + // Supported fields: username, email, role, state, create_time, update_time + Filter string `protobuf:"bytes,3,opt,name=filter,proto3" json:"filter,omitempty"` + // Optional. The order to sort results by. + // Example: "create_time desc" or "username asc" + OrderBy string `protobuf:"bytes,4,opt,name=order_by,json=orderBy,proto3" json:"order_by,omitempty"` + // Optional. If true, show deleted users in the response. + ShowDeleted bool `protobuf:"varint,5,opt,name=show_deleted,json=showDeleted,proto3" json:"show_deleted,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListUsersRequest) Reset() { + *x = ListUsersRequest{} + mi := &file_api_v1_user_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListUsersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListUsersRequest) ProtoMessage() {} + +func (x *ListUsersRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_user_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListUsersRequest.ProtoReflect.Descriptor instead. +func (*ListUsersRequest) Descriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{1} +} + +func (x *ListUsersRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListUsersRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +func (x *ListUsersRequest) GetFilter() string { + if x != nil { + return x.Filter + } + return "" +} + +func (x *ListUsersRequest) GetOrderBy() string { + if x != nil { + return x.OrderBy + } + return "" +} + +func (x *ListUsersRequest) GetShowDeleted() bool { + if x != nil { + return x.ShowDeleted + } + return false +} + +type ListUsersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The list of users. + Users []*User `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"` + // A token that can be sent as `page_token` to retrieve the next page. + // If this field is omitted, there are no subsequent pages. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + // The total count of users (may be approximate). + TotalSize int32 `protobuf:"varint,3,opt,name=total_size,json=totalSize,proto3" json:"total_size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListUsersResponse) Reset() { + *x = ListUsersResponse{} + mi := &file_api_v1_user_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListUsersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListUsersResponse) ProtoMessage() {} + +func (x *ListUsersResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_user_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListUsersResponse.ProtoReflect.Descriptor instead. +func (*ListUsersResponse) Descriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{2} +} + +func (x *ListUsersResponse) GetUsers() []*User { + if x != nil { + return x.Users + } + return nil +} + +func (x *ListUsersResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +func (x *ListUsersResponse) GetTotalSize() int32 { + if x != nil { + return x.TotalSize + } + return 0 +} + +type GetUserRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The resource name of the user. + // Format: users/{user} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Optional. The fields to return in the response. + // If not specified, all fields are returned. + ReadMask *fieldmaskpb.FieldMask `protobuf:"bytes,2,opt,name=read_mask,json=readMask,proto3" json:"read_mask,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetUserRequest) Reset() { + *x = GetUserRequest{} + mi := &file_api_v1_user_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetUserRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetUserRequest) ProtoMessage() {} + +func (x *GetUserRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_user_service_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetUserRequest.ProtoReflect.Descriptor instead. +func (*GetUserRequest) Descriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{3} +} + +func (x *GetUserRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *GetUserRequest) GetReadMask() *fieldmaskpb.FieldMask { + if x != nil { + return x.ReadMask + } + return nil +} + +type CreateUserRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The user to create. + User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + // Optional. The user ID to use for this user. + // If empty, a unique ID will be generated. + // Must match the pattern [a-z0-9-]+ + UserId string `protobuf:"bytes,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + // Optional. If set, validate the request but don't actually create the user. + ValidateOnly bool `protobuf:"varint,3,opt,name=validate_only,json=validateOnly,proto3" json:"validate_only,omitempty"` + // Optional. An idempotency token that can be used to ensure that multiple + // requests to create a user have the same result. + RequestId string `protobuf:"bytes,4,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateUserRequest) Reset() { + *x = CreateUserRequest{} + mi := &file_api_v1_user_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateUserRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateUserRequest) ProtoMessage() {} + +func (x *CreateUserRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_user_service_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateUserRequest.ProtoReflect.Descriptor instead. +func (*CreateUserRequest) Descriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{4} +} + +func (x *CreateUserRequest) GetUser() *User { + if x != nil { + return x.User + } + return nil +} + +func (x *CreateUserRequest) GetUserId() string { + if x != nil { + return x.UserId + } + return "" +} + +func (x *CreateUserRequest) GetValidateOnly() bool { + if x != nil { + return x.ValidateOnly + } + return false +} + +func (x *CreateUserRequest) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +type UpdateUserRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The user to update. + User *User `protobuf:"bytes,1,opt,name=user,proto3" json:"user,omitempty"` + // Required. The list of fields to update. + UpdateMask *fieldmaskpb.FieldMask `protobuf:"bytes,2,opt,name=update_mask,json=updateMask,proto3" json:"update_mask,omitempty"` + // Optional. If set to true, allows updating sensitive fields. + AllowMissing bool `protobuf:"varint,3,opt,name=allow_missing,json=allowMissing,proto3" json:"allow_missing,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateUserRequest) Reset() { + *x = UpdateUserRequest{} + mi := &file_api_v1_user_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateUserRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateUserRequest) ProtoMessage() {} + +func (x *UpdateUserRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_user_service_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateUserRequest.ProtoReflect.Descriptor instead. +func (*UpdateUserRequest) Descriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{5} +} + +func (x *UpdateUserRequest) GetUser() *User { + if x != nil { + return x.User + } + return nil +} + +func (x *UpdateUserRequest) GetUpdateMask() *fieldmaskpb.FieldMask { + if x != nil { + return x.UpdateMask + } + return nil +} + +func (x *UpdateUserRequest) GetAllowMissing() bool { + if x != nil { + return x.AllowMissing + } + return false +} + +type DeleteUserRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The resource name of the user to delete. + // Format: users/{user} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Optional. If set to true, the user will be deleted even if they have associated data. + Force bool `protobuf:"varint,2,opt,name=force,proto3" json:"force,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteUserRequest) Reset() { + *x = DeleteUserRequest{} + mi := &file_api_v1_user_service_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteUserRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteUserRequest) ProtoMessage() {} + +func (x *DeleteUserRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_user_service_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteUserRequest.ProtoReflect.Descriptor instead. +func (*DeleteUserRequest) Descriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{6} +} + +func (x *DeleteUserRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *DeleteUserRequest) GetForce() bool { + if x != nil { + return x.Force + } + return false +} + +type SearchUsersRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The search query. + Query string `protobuf:"bytes,1,opt,name=query,proto3" json:"query,omitempty"` + // Optional. The maximum number of users to return. + PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Optional. A page token for pagination. + PageToken string `protobuf:"bytes,3,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SearchUsersRequest) Reset() { + *x = SearchUsersRequest{} + mi := &file_api_v1_user_service_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SearchUsersRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SearchUsersRequest) ProtoMessage() {} + +func (x *SearchUsersRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_user_service_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SearchUsersRequest.ProtoReflect.Descriptor instead. +func (*SearchUsersRequest) Descriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{7} +} + +func (x *SearchUsersRequest) GetQuery() string { + if x != nil { + return x.Query + } + return "" +} + +func (x *SearchUsersRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *SearchUsersRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +type SearchUsersResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The list of users matching the search query. + Users []*User `protobuf:"bytes,1,rep,name=users,proto3" json:"users,omitempty"` + // A token for the next page of results. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + // The total count of matching users. + TotalSize int32 `protobuf:"varint,3,opt,name=total_size,json=totalSize,proto3" json:"total_size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SearchUsersResponse) Reset() { + *x = SearchUsersResponse{} + mi := &file_api_v1_user_service_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SearchUsersResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SearchUsersResponse) ProtoMessage() {} + +func (x *SearchUsersResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_user_service_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SearchUsersResponse.ProtoReflect.Descriptor instead. +func (*SearchUsersResponse) Descriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{8} +} + +func (x *SearchUsersResponse) GetUsers() []*User { + if x != nil { + return x.Users + } + return nil +} + +func (x *SearchUsersResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +func (x *SearchUsersResponse) GetTotalSize() int32 { + if x != nil { + return x.TotalSize + } + return 0 +} + +type GetUserAvatarRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The resource name of the user. + // Format: users/{user} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetUserAvatarRequest) Reset() { + *x = GetUserAvatarRequest{} + mi := &file_api_v1_user_service_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetUserAvatarRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetUserAvatarRequest) ProtoMessage() {} + +func (x *GetUserAvatarRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_user_service_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetUserAvatarRequest.ProtoReflect.Descriptor instead. +func (*GetUserAvatarRequest) Descriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{9} +} + +func (x *GetUserAvatarRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +// User statistics messages +type UserStats struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The resource name of the user whose stats these are. + // Format: users/{user} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // The timestamps when the memos were displayed. + MemoDisplayTimestamps []*timestamppb.Timestamp `protobuf:"bytes,2,rep,name=memo_display_timestamps,json=memoDisplayTimestamps,proto3" json:"memo_display_timestamps,omitempty"` + // The stats of memo types. + MemoTypeStats *UserStats_MemoTypeStats `protobuf:"bytes,3,opt,name=memo_type_stats,json=memoTypeStats,proto3" json:"memo_type_stats,omitempty"` + // The count of tags. + TagCount map[string]int32 `protobuf:"bytes,4,rep,name=tag_count,json=tagCount,proto3" json:"tag_count,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"` + // The pinned memos of the user. + PinnedMemos []string `protobuf:"bytes,5,rep,name=pinned_memos,json=pinnedMemos,proto3" json:"pinned_memos,omitempty"` + // Total memo count. + TotalMemoCount int32 `protobuf:"varint,6,opt,name=total_memo_count,json=totalMemoCount,proto3" json:"total_memo_count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserStats) Reset() { + *x = UserStats{} + mi := &file_api_v1_user_service_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserStats) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserStats) ProtoMessage() {} + +func (x *UserStats) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_user_service_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserStats.ProtoReflect.Descriptor instead. +func (*UserStats) Descriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{10} +} + +func (x *UserStats) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *UserStats) GetMemoDisplayTimestamps() []*timestamppb.Timestamp { + if x != nil { + return x.MemoDisplayTimestamps + } + return nil +} + +func (x *UserStats) GetMemoTypeStats() *UserStats_MemoTypeStats { + if x != nil { + return x.MemoTypeStats + } + return nil +} + +func (x *UserStats) GetTagCount() map[string]int32 { + if x != nil { + return x.TagCount + } + return nil +} + +func (x *UserStats) GetPinnedMemos() []string { + if x != nil { + return x.PinnedMemos + } + return nil +} + +func (x *UserStats) GetTotalMemoCount() int32 { + if x != nil { + return x.TotalMemoCount + } + return 0 +} + +type GetUserStatsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The resource name of the user. + // Format: users/{user} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetUserStatsRequest) Reset() { + *x = GetUserStatsRequest{} + mi := &file_api_v1_user_service_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetUserStatsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetUserStatsRequest) ProtoMessage() {} + +func (x *GetUserStatsRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_user_service_proto_msgTypes[11] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetUserStatsRequest.ProtoReflect.Descriptor instead. +func (*GetUserStatsRequest) Descriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{11} +} + +func (x *GetUserStatsRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +// User settings message +type UserSetting struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The resource name of the user whose setting this is. + // Format: users/{user} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // The preferred locale of the user. + Locale string `protobuf:"bytes,2,opt,name=locale,proto3" json:"locale,omitempty"` + // The preferred appearance of the user. + Appearance string `protobuf:"bytes,3,opt,name=appearance,proto3" json:"appearance,omitempty"` + // The default visibility of the memo. + MemoVisibility string `protobuf:"bytes,4,opt,name=memo_visibility,json=memoVisibility,proto3" json:"memo_visibility,omitempty"` + // The preferred theme of the user. + // This references a CSS file in the web/public/themes/ directory. + // If not set, the default theme will be used. + Theme string `protobuf:"bytes,5,opt,name=theme,proto3" json:"theme,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserSetting) Reset() { + *x = UserSetting{} + mi := &file_api_v1_user_service_proto_msgTypes[12] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserSetting) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserSetting) ProtoMessage() {} + +func (x *UserSetting) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_user_service_proto_msgTypes[12] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserSetting.ProtoReflect.Descriptor instead. +func (*UserSetting) Descriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{12} +} + +func (x *UserSetting) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *UserSetting) GetLocale() string { + if x != nil { + return x.Locale + } + return "" +} + +func (x *UserSetting) GetAppearance() string { + if x != nil { + return x.Appearance + } + return "" +} + +func (x *UserSetting) GetMemoVisibility() string { + if x != nil { + return x.MemoVisibility + } + return "" +} + +func (x *UserSetting) GetTheme() string { + if x != nil { + return x.Theme + } + return "" +} + +type GetUserSettingRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The resource name of the user. + // Format: users/{user} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetUserSettingRequest) Reset() { + *x = GetUserSettingRequest{} + mi := &file_api_v1_user_service_proto_msgTypes[13] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetUserSettingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetUserSettingRequest) ProtoMessage() {} + +func (x *GetUserSettingRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_user_service_proto_msgTypes[13] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetUserSettingRequest.ProtoReflect.Descriptor instead. +func (*GetUserSettingRequest) Descriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{13} +} + +func (x *GetUserSettingRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type UpdateUserSettingRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The user setting to update. + Setting *UserSetting `protobuf:"bytes,1,opt,name=setting,proto3" json:"setting,omitempty"` + // Required. The list of fields to update. + UpdateMask *fieldmaskpb.FieldMask `protobuf:"bytes,2,opt,name=update_mask,json=updateMask,proto3" json:"update_mask,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateUserSettingRequest) Reset() { + *x = UpdateUserSettingRequest{} + mi := &file_api_v1_user_service_proto_msgTypes[14] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateUserSettingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateUserSettingRequest) ProtoMessage() {} + +func (x *UpdateUserSettingRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_user_service_proto_msgTypes[14] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateUserSettingRequest.ProtoReflect.Descriptor instead. +func (*UpdateUserSettingRequest) Descriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{14} +} + +func (x *UpdateUserSettingRequest) GetSetting() *UserSetting { + if x != nil { + return x.Setting + } + return nil +} + +func (x *UpdateUserSettingRequest) GetUpdateMask() *fieldmaskpb.FieldMask { + if x != nil { + return x.UpdateMask + } + return nil +} + +// User access token message +type UserAccessToken struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The resource name of the access token. + // Format: users/{user}/accessTokens/{access_token} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Output only. The access token value. + AccessToken string `protobuf:"bytes,2,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"` + // The description of the access token. + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + // Output only. The issued timestamp. + IssuedAt *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=issued_at,json=issuedAt,proto3" json:"issued_at,omitempty"` + // Optional. The expiration timestamp. + ExpiresAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=expires_at,json=expiresAt,proto3" json:"expires_at,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserAccessToken) Reset() { + *x = UserAccessToken{} + mi := &file_api_v1_user_service_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserAccessToken) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserAccessToken) ProtoMessage() {} + +func (x *UserAccessToken) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_user_service_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserAccessToken.ProtoReflect.Descriptor instead. +func (*UserAccessToken) Descriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{15} +} + +func (x *UserAccessToken) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *UserAccessToken) GetAccessToken() string { + if x != nil { + return x.AccessToken + } + return "" +} + +func (x *UserAccessToken) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *UserAccessToken) GetIssuedAt() *timestamppb.Timestamp { + if x != nil { + return x.IssuedAt + } + return nil +} + +func (x *UserAccessToken) GetExpiresAt() *timestamppb.Timestamp { + if x != nil { + return x.ExpiresAt + } + return nil +} + +type ListUserAccessTokensRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The parent resource whose access tokens will be listed. + // Format: users/{user} + Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"` + // Optional. The maximum number of access tokens to return. + PageSize int32 `protobuf:"varint,2,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Optional. A page token for pagination. + PageToken string `protobuf:"bytes,3,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListUserAccessTokensRequest) Reset() { + *x = ListUserAccessTokensRequest{} + mi := &file_api_v1_user_service_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListUserAccessTokensRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListUserAccessTokensRequest) ProtoMessage() {} + +func (x *ListUserAccessTokensRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_user_service_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListUserAccessTokensRequest.ProtoReflect.Descriptor instead. +func (*ListUserAccessTokensRequest) Descriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{16} +} + +func (x *ListUserAccessTokensRequest) GetParent() string { + if x != nil { + return x.Parent + } + return "" +} + +func (x *ListUserAccessTokensRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListUserAccessTokensRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +type ListUserAccessTokensResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The list of access tokens. + AccessTokens []*UserAccessToken `protobuf:"bytes,1,rep,name=access_tokens,json=accessTokens,proto3" json:"access_tokens,omitempty"` + // A token for the next page of results. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + // The total count of access tokens. + TotalSize int32 `protobuf:"varint,3,opt,name=total_size,json=totalSize,proto3" json:"total_size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListUserAccessTokensResponse) Reset() { + *x = ListUserAccessTokensResponse{} + mi := &file_api_v1_user_service_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListUserAccessTokensResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListUserAccessTokensResponse) ProtoMessage() {} + +func (x *ListUserAccessTokensResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_user_service_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListUserAccessTokensResponse.ProtoReflect.Descriptor instead. +func (*ListUserAccessTokensResponse) Descriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{17} +} + +func (x *ListUserAccessTokensResponse) GetAccessTokens() []*UserAccessToken { + if x != nil { + return x.AccessTokens + } + return nil +} + +func (x *ListUserAccessTokensResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +func (x *ListUserAccessTokensResponse) GetTotalSize() int32 { + if x != nil { + return x.TotalSize + } + return 0 +} + +type CreateUserAccessTokenRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The parent resource where this access token will be created. + // Format: users/{user} + Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"` + // Required. The access token to create. + AccessToken *UserAccessToken `protobuf:"bytes,2,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"` + // Optional. The access token ID to use. + AccessTokenId string `protobuf:"bytes,3,opt,name=access_token_id,json=accessTokenId,proto3" json:"access_token_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateUserAccessTokenRequest) Reset() { + *x = CreateUserAccessTokenRequest{} + mi := &file_api_v1_user_service_proto_msgTypes[18] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateUserAccessTokenRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateUserAccessTokenRequest) ProtoMessage() {} + +func (x *CreateUserAccessTokenRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_user_service_proto_msgTypes[18] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateUserAccessTokenRequest.ProtoReflect.Descriptor instead. +func (*CreateUserAccessTokenRequest) Descriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{18} +} + +func (x *CreateUserAccessTokenRequest) GetParent() string { + if x != nil { + return x.Parent + } + return "" +} + +func (x *CreateUserAccessTokenRequest) GetAccessToken() *UserAccessToken { + if x != nil { + return x.AccessToken + } + return nil +} + +func (x *CreateUserAccessTokenRequest) GetAccessTokenId() string { + if x != nil { + return x.AccessTokenId + } + return "" +} + +type DeleteUserAccessTokenRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The resource name of the access token to delete. + // Format: users/{user}/accessTokens/{access_token} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteUserAccessTokenRequest) Reset() { + *x = DeleteUserAccessTokenRequest{} + mi := &file_api_v1_user_service_proto_msgTypes[19] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteUserAccessTokenRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteUserAccessTokenRequest) ProtoMessage() {} + +func (x *DeleteUserAccessTokenRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_user_service_proto_msgTypes[19] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteUserAccessTokenRequest.ProtoReflect.Descriptor instead. +func (*DeleteUserAccessTokenRequest) Descriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{19} +} + +func (x *DeleteUserAccessTokenRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type UserSession struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The resource name of the session. + // Format: users/{user}/sessions/{session} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // The session ID. + SessionId string `protobuf:"bytes,2,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + // The timestamp when the session was created. + CreateTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` + // The timestamp when the session was last accessed. + // Used for sliding expiration calculation (last_accessed_time + 2 weeks). + LastAccessedTime *timestamppb.Timestamp `protobuf:"bytes,4,opt,name=last_accessed_time,json=lastAccessedTime,proto3" json:"last_accessed_time,omitempty"` + // Client information associated with this session. + ClientInfo *UserSession_ClientInfo `protobuf:"bytes,5,opt,name=client_info,json=clientInfo,proto3" json:"client_info,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserSession) Reset() { + *x = UserSession{} + mi := &file_api_v1_user_service_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserSession) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserSession) ProtoMessage() {} + +func (x *UserSession) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_user_service_proto_msgTypes[20] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserSession.ProtoReflect.Descriptor instead. +func (*UserSession) Descriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{20} +} + +func (x *UserSession) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *UserSession) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *UserSession) GetCreateTime() *timestamppb.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *UserSession) GetLastAccessedTime() *timestamppb.Timestamp { + if x != nil { + return x.LastAccessedTime + } + return nil +} + +func (x *UserSession) GetClientInfo() *UserSession_ClientInfo { + if x != nil { + return x.ClientInfo + } + return nil +} + +type ListUserSessionsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The resource name of the parent. + // Format: users/{user} + Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListUserSessionsRequest) Reset() { + *x = ListUserSessionsRequest{} + mi := &file_api_v1_user_service_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListUserSessionsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListUserSessionsRequest) ProtoMessage() {} + +func (x *ListUserSessionsRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_user_service_proto_msgTypes[21] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListUserSessionsRequest.ProtoReflect.Descriptor instead. +func (*ListUserSessionsRequest) Descriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{21} +} + +func (x *ListUserSessionsRequest) GetParent() string { + if x != nil { + return x.Parent + } + return "" +} + +type ListUserSessionsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The list of user sessions. + Sessions []*UserSession `protobuf:"bytes,1,rep,name=sessions,proto3" json:"sessions,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListUserSessionsResponse) Reset() { + *x = ListUserSessionsResponse{} + mi := &file_api_v1_user_service_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListUserSessionsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListUserSessionsResponse) ProtoMessage() {} + +func (x *ListUserSessionsResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_user_service_proto_msgTypes[22] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListUserSessionsResponse.ProtoReflect.Descriptor instead. +func (*ListUserSessionsResponse) Descriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{22} +} + +func (x *ListUserSessionsResponse) GetSessions() []*UserSession { + if x != nil { + return x.Sessions + } + return nil +} + +type RevokeUserSessionRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The resource name of the session to revoke. + // Format: users/{user}/sessions/{session} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *RevokeUserSessionRequest) Reset() { + *x = RevokeUserSessionRequest{} + mi := &file_api_v1_user_service_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *RevokeUserSessionRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*RevokeUserSessionRequest) ProtoMessage() {} + +func (x *RevokeUserSessionRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_user_service_proto_msgTypes[23] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use RevokeUserSessionRequest.ProtoReflect.Descriptor instead. +func (*RevokeUserSessionRequest) Descriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{23} +} + +func (x *RevokeUserSessionRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type ListAllUserStatsRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Optional. The maximum number of user stats to return. + PageSize int32 `protobuf:"varint,1,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // Optional. A page token for pagination. + PageToken string `protobuf:"bytes,2,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListAllUserStatsRequest) Reset() { + *x = ListAllUserStatsRequest{} + mi := &file_api_v1_user_service_proto_msgTypes[24] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListAllUserStatsRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAllUserStatsRequest) ProtoMessage() {} + +func (x *ListAllUserStatsRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_user_service_proto_msgTypes[24] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListAllUserStatsRequest.ProtoReflect.Descriptor instead. +func (*ListAllUserStatsRequest) Descriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{24} +} + +func (x *ListAllUserStatsRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListAllUserStatsRequest) GetPageToken() string { + if x != nil { + return x.PageToken + } + return "" +} + +type ListAllUserStatsResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The list of user statistics. + UserStats []*UserStats `protobuf:"bytes,1,rep,name=user_stats,json=userStats,proto3" json:"user_stats,omitempty"` + // A token for the next page of results. + NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` + // The total count of user statistics. + TotalSize int32 `protobuf:"varint,3,opt,name=total_size,json=totalSize,proto3" json:"total_size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListAllUserStatsResponse) Reset() { + *x = ListAllUserStatsResponse{} + mi := &file_api_v1_user_service_proto_msgTypes[25] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListAllUserStatsResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListAllUserStatsResponse) ProtoMessage() {} + +func (x *ListAllUserStatsResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_user_service_proto_msgTypes[25] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListAllUserStatsResponse.ProtoReflect.Descriptor instead. +func (*ListAllUserStatsResponse) Descriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{25} +} + +func (x *ListAllUserStatsResponse) GetUserStats() []*UserStats { + if x != nil { + return x.UserStats + } + return nil +} + +func (x *ListAllUserStatsResponse) GetNextPageToken() string { + if x != nil { + return x.NextPageToken + } + return "" +} + +func (x *ListAllUserStatsResponse) GetTotalSize() int32 { + if x != nil { + return x.TotalSize + } + return 0 +} + +// Memo type statistics. +type UserStats_MemoTypeStats struct { + state protoimpl.MessageState `protogen:"open.v1"` + LinkCount int32 `protobuf:"varint,1,opt,name=link_count,json=linkCount,proto3" json:"link_count,omitempty"` + CodeCount int32 `protobuf:"varint,2,opt,name=code_count,json=codeCount,proto3" json:"code_count,omitempty"` + TodoCount int32 `protobuf:"varint,3,opt,name=todo_count,json=todoCount,proto3" json:"todo_count,omitempty"` + UndoCount int32 `protobuf:"varint,4,opt,name=undo_count,json=undoCount,proto3" json:"undo_count,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserStats_MemoTypeStats) Reset() { + *x = UserStats_MemoTypeStats{} + mi := &file_api_v1_user_service_proto_msgTypes[27] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserStats_MemoTypeStats) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserStats_MemoTypeStats) ProtoMessage() {} + +func (x *UserStats_MemoTypeStats) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_user_service_proto_msgTypes[27] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserStats_MemoTypeStats.ProtoReflect.Descriptor instead. +func (*UserStats_MemoTypeStats) Descriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{10, 1} +} + +func (x *UserStats_MemoTypeStats) GetLinkCount() int32 { + if x != nil { + return x.LinkCount + } + return 0 +} + +func (x *UserStats_MemoTypeStats) GetCodeCount() int32 { + if x != nil { + return x.CodeCount + } + return 0 +} + +func (x *UserStats_MemoTypeStats) GetTodoCount() int32 { + if x != nil { + return x.TodoCount + } + return 0 +} + +func (x *UserStats_MemoTypeStats) GetUndoCount() int32 { + if x != nil { + return x.UndoCount + } + return 0 +} + +type UserSession_ClientInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + // User agent string of the client. + UserAgent string `protobuf:"bytes,1,opt,name=user_agent,json=userAgent,proto3" json:"user_agent,omitempty"` + // IP address of the client. + IpAddress string `protobuf:"bytes,2,opt,name=ip_address,json=ipAddress,proto3" json:"ip_address,omitempty"` + // Optional. Device type (e.g., "mobile", "desktop", "tablet"). + DeviceType string `protobuf:"bytes,3,opt,name=device_type,json=deviceType,proto3" json:"device_type,omitempty"` + // Optional. Operating system (e.g., "iOS 17.0", "Windows 11"). + Os string `protobuf:"bytes,4,opt,name=os,proto3" json:"os,omitempty"` + // Optional. Browser name and version (e.g., "Chrome 119.0"). + Browser string `protobuf:"bytes,5,opt,name=browser,proto3" json:"browser,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserSession_ClientInfo) Reset() { + *x = UserSession_ClientInfo{} + mi := &file_api_v1_user_service_proto_msgTypes[28] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserSession_ClientInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserSession_ClientInfo) ProtoMessage() {} + +func (x *UserSession_ClientInfo) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_user_service_proto_msgTypes[28] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserSession_ClientInfo.ProtoReflect.Descriptor instead. +func (*UserSession_ClientInfo) Descriptor() ([]byte, []int) { + return file_api_v1_user_service_proto_rawDescGZIP(), []int{20, 0} +} + +func (x *UserSession_ClientInfo) GetUserAgent() string { + if x != nil { + return x.UserAgent + } + return "" +} + +func (x *UserSession_ClientInfo) GetIpAddress() string { + if x != nil { + return x.IpAddress + } + return "" +} + +func (x *UserSession_ClientInfo) GetDeviceType() string { + if x != nil { + return x.DeviceType + } + return "" +} + +func (x *UserSession_ClientInfo) GetOs() string { + if x != nil { + return x.Os + } + return "" +} + +func (x *UserSession_ClientInfo) GetBrowser() string { + if x != nil { + return x.Browser + } + return "" +} + +var File_api_v1_user_service_proto protoreflect.FileDescriptor + +const file_api_v1_user_service_proto_rawDesc = "" + + "\n" + + "\x19api/v1/user_service.proto\x12\fmemos.api.v1\x1a\x13api/v1/common.proto\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/httpbody.proto\x1a\x19google/api/resource.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xcb\x04\n" + + "\x04User\x12\x17\n" + + "\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x120\n" + + "\x04role\x18\x02 \x01(\x0e2\x17.memos.api.v1.User.RoleB\x03\xe0A\x02R\x04role\x12\x1f\n" + + "\busername\x18\x03 \x01(\tB\x03\xe0A\x02R\busername\x12\x19\n" + + "\x05email\x18\x04 \x01(\tB\x03\xe0A\x01R\x05email\x12&\n" + + "\fdisplay_name\x18\x05 \x01(\tB\x03\xe0A\x01R\vdisplayName\x12\"\n" + + "\n" + + "avatar_url\x18\x06 \x01(\tB\x03\xe0A\x01R\tavatarUrl\x12%\n" + + "\vdescription\x18\a \x01(\tB\x03\xe0A\x01R\vdescription\x12\x1f\n" + + "\bpassword\x18\b \x01(\tB\x03\xe0A\x04R\bpassword\x12.\n" + + "\x05state\x18\t \x01(\x0e2\x13.memos.api.v1.StateB\x03\xe0A\x02R\x05state\x12@\n" + + "\vcreate_time\x18\n" + + " \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\n" + + "createTime\x12@\n" + + "\vupdate_time\x18\v \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\n" + + "updateTime\";\n" + + "\x04Role\x12\x14\n" + + "\x10ROLE_UNSPECIFIED\x10\x00\x12\b\n" + + "\x04HOST\x10\x01\x12\t\n" + + "\x05ADMIN\x10\x02\x12\b\n" + + "\x04USER\x10\x03:7\xeaA4\n" + + "\x11memos.api.v1/User\x12\fusers/{user}\x1a\x04name*\x05users2\x04user\"\xbd\x01\n" + + "\x10ListUsersRequest\x12 \n" + + "\tpage_size\x18\x01 \x01(\x05B\x03\xe0A\x01R\bpageSize\x12\"\n" + + "\n" + + "page_token\x18\x02 \x01(\tB\x03\xe0A\x01R\tpageToken\x12\x1b\n" + + "\x06filter\x18\x03 \x01(\tB\x03\xe0A\x01R\x06filter\x12\x1e\n" + + "\border_by\x18\x04 \x01(\tB\x03\xe0A\x01R\aorderBy\x12&\n" + + "\fshow_deleted\x18\x05 \x01(\bB\x03\xe0A\x01R\vshowDeleted\"\x84\x01\n" + + "\x11ListUsersResponse\x12(\n" + + "\x05users\x18\x01 \x03(\v2\x12.memos.api.v1.UserR\x05users\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12\x1d\n" + + "\n" + + "total_size\x18\x03 \x01(\x05R\ttotalSize\"}\n" + + "\x0eGetUserRequest\x12-\n" + + "\x04name\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" + + "\x11memos.api.v1/UserR\x04name\x12<\n" + + "\tread_mask\x18\x02 \x01(\v2\x1a.google.protobuf.FieldMaskB\x03\xe0A\x01R\breadMask\"\xaf\x01\n" + + "\x11CreateUserRequest\x12.\n" + + "\x04user\x18\x01 \x01(\v2\x12.memos.api.v1.UserB\x06\xe0A\x02\xe0A\x04R\x04user\x12\x1c\n" + + "\auser_id\x18\x02 \x01(\tB\x03\xe0A\x01R\x06userId\x12(\n" + + "\rvalidate_only\x18\x03 \x01(\bB\x03\xe0A\x01R\fvalidateOnly\x12\"\n" + + "\n" + + "request_id\x18\x04 \x01(\tB\x03\xe0A\x01R\trequestId\"\xac\x01\n" + + "\x11UpdateUserRequest\x12+\n" + + "\x04user\x18\x01 \x01(\v2\x12.memos.api.v1.UserB\x03\xe0A\x02R\x04user\x12@\n" + + "\vupdate_mask\x18\x02 \x01(\v2\x1a.google.protobuf.FieldMaskB\x03\xe0A\x02R\n" + + "updateMask\x12(\n" + + "\rallow_missing\x18\x03 \x01(\bB\x03\xe0A\x01R\fallowMissing\"]\n" + + "\x11DeleteUserRequest\x12-\n" + + "\x04name\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" + + "\x11memos.api.v1/UserR\x04name\x12\x19\n" + + "\x05force\x18\x02 \x01(\bB\x03\xe0A\x01R\x05force\"u\n" + + "\x12SearchUsersRequest\x12\x19\n" + + "\x05query\x18\x01 \x01(\tB\x03\xe0A\x02R\x05query\x12 \n" + + "\tpage_size\x18\x02 \x01(\x05B\x03\xe0A\x01R\bpageSize\x12\"\n" + + "\n" + + "page_token\x18\x03 \x01(\tB\x03\xe0A\x01R\tpageToken\"\x86\x01\n" + + "\x13SearchUsersResponse\x12(\n" + + "\x05users\x18\x01 \x03(\v2\x12.memos.api.v1.UserR\x05users\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12\x1d\n" + + "\n" + + "total_size\x18\x03 \x01(\x05R\ttotalSize\"E\n" + + "\x14GetUserAvatarRequest\x12-\n" + + "\x04name\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" + + "\x11memos.api.v1/UserR\x04name\"\xe4\x04\n" + + "\tUserStats\x12\x17\n" + + "\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12R\n" + + "\x17memo_display_timestamps\x18\x02 \x03(\v2\x1a.google.protobuf.TimestampR\x15memoDisplayTimestamps\x12M\n" + + "\x0fmemo_type_stats\x18\x03 \x01(\v2%.memos.api.v1.UserStats.MemoTypeStatsR\rmemoTypeStats\x12B\n" + + "\ttag_count\x18\x04 \x03(\v2%.memos.api.v1.UserStats.TagCountEntryR\btagCount\x12!\n" + + "\fpinned_memos\x18\x05 \x03(\tR\vpinnedMemos\x12(\n" + + "\x10total_memo_count\x18\x06 \x01(\x05R\x0etotalMemoCount\x1a;\n" + + "\rTagCountEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + + "\x05value\x18\x02 \x01(\x05R\x05value:\x028\x01\x1a\x8b\x01\n" + + "\rMemoTypeStats\x12\x1d\n" + + "\n" + + "link_count\x18\x01 \x01(\x05R\tlinkCount\x12\x1d\n" + + "\n" + + "code_count\x18\x02 \x01(\x05R\tcodeCount\x12\x1d\n" + + "\n" + + "todo_count\x18\x03 \x01(\x05R\ttodoCount\x12\x1d\n" + + "\n" + + "undo_count\x18\x04 \x01(\x05R\tundoCount:?\xeaA<\n" + + "\x16memos.api.v1/UserStats\x12\fusers/{user}*\tuserStats2\tuserStats\"D\n" + + "\x13GetUserStatsRequest\x12-\n" + + "\x04name\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" + + "\x11memos.api.v1/UserR\x04name\"\xf9\x01\n" + + "\vUserSetting\x12\x17\n" + + "\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12\x1b\n" + + "\x06locale\x18\x02 \x01(\tB\x03\xe0A\x01R\x06locale\x12#\n" + + "\n" + + "appearance\x18\x03 \x01(\tB\x03\xe0A\x01R\n" + + "appearance\x12,\n" + + "\x0fmemo_visibility\x18\x04 \x01(\tB\x03\xe0A\x01R\x0ememoVisibility\x12\x19\n" + + "\x05theme\x18\x05 \x01(\tB\x03\xe0A\x01R\x05theme:F\xeaAC\n" + + "\x18memos.api.v1/UserSetting\x12\fusers/{user}*\fuserSettings2\vuserSetting\"F\n" + + "\x15GetUserSettingRequest\x12-\n" + + "\x04name\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" + + "\x11memos.api.v1/UserR\x04name\"\x96\x01\n" + + "\x18UpdateUserSettingRequest\x128\n" + + "\asetting\x18\x01 \x01(\v2\x19.memos.api.v1.UserSettingB\x03\xe0A\x02R\asetting\x12@\n" + + "\vupdate_mask\x18\x02 \x01(\v2\x1a.google.protobuf.FieldMaskB\x03\xe0A\x02R\n" + + "updateMask\"\xe7\x02\n" + + "\x0fUserAccessToken\x12\x17\n" + + "\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12&\n" + + "\faccess_token\x18\x02 \x01(\tB\x03\xe0A\x03R\vaccessToken\x12%\n" + + "\vdescription\x18\x03 \x01(\tB\x03\xe0A\x01R\vdescription\x12<\n" + + "\tissued_at\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\bissuedAt\x12>\n" + + "\n" + + "expires_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x01R\texpiresAt:n\xeaAk\n" + + "\x1cmemos.api.v1/UserAccessToken\x12(users/{user}/accessTokens/{access_token}*\x10userAccessTokens2\x0fuserAccessToken\"\x96\x01\n" + + "\x1bListUserAccessTokensRequest\x121\n" + + "\x06parent\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" + + "\x11memos.api.v1/UserR\x06parent\x12 \n" + + "\tpage_size\x18\x02 \x01(\x05B\x03\xe0A\x01R\bpageSize\x12\"\n" + + "\n" + + "page_token\x18\x03 \x01(\tB\x03\xe0A\x01R\tpageToken\"\xa9\x01\n" + + "\x1cListUserAccessTokensResponse\x12B\n" + + "\raccess_tokens\x18\x01 \x03(\v2\x1d.memos.api.v1.UserAccessTokenR\faccessTokens\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12\x1d\n" + + "\n" + + "total_size\x18\x03 \x01(\x05R\ttotalSize\"\xc5\x01\n" + + "\x1cCreateUserAccessTokenRequest\x121\n" + + "\x06parent\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" + + "\x11memos.api.v1/UserR\x06parent\x12E\n" + + "\faccess_token\x18\x02 \x01(\v2\x1d.memos.api.v1.UserAccessTokenB\x03\xe0A\x02R\vaccessToken\x12+\n" + + "\x0faccess_token_id\x18\x03 \x01(\tB\x03\xe0A\x01R\raccessTokenId\"X\n" + + "\x1cDeleteUserAccessTokenRequest\x128\n" + + "\x04name\x18\x01 \x01(\tB$\xe0A\x02\xfaA\x1e\n" + + "\x1cmemos.api.v1/UserAccessTokenR\x04name\"\x94\x04\n" + + "\vUserSession\x12\x17\n" + + "\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12\"\n" + + "\n" + + "session_id\x18\x02 \x01(\tB\x03\xe0A\x03R\tsessionId\x12@\n" + + "\vcreate_time\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\n" + + "createTime\x12M\n" + + "\x12last_accessed_time\x18\x04 \x01(\v2\x1a.google.protobuf.TimestampB\x03\xe0A\x03R\x10lastAccessedTime\x12J\n" + + "\vclient_info\x18\x05 \x01(\v2$.memos.api.v1.UserSession.ClientInfoB\x03\xe0A\x03R\n" + + "clientInfo\x1a\xa4\x01\n" + + "\n" + + "ClientInfo\x12\x1d\n" + + "\n" + + "user_agent\x18\x01 \x01(\tR\tuserAgent\x12\x1d\n" + + "\n" + + "ip_address\x18\x02 \x01(\tR\tipAddress\x12$\n" + + "\vdevice_type\x18\x03 \x01(\tB\x03\xe0A\x01R\n" + + "deviceType\x12\x13\n" + + "\x02os\x18\x04 \x01(\tB\x03\xe0A\x01R\x02os\x12\x1d\n" + + "\abrowser\x18\x05 \x01(\tB\x03\xe0A\x01R\abrowser:D\xeaAA\n" + + "\x18memos.api.v1/UserSession\x12\x1fusers/{user}/sessions/{session}\x1a\x04name\"L\n" + + "\x17ListUserSessionsRequest\x121\n" + + "\x06parent\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" + + "\x11memos.api.v1/UserR\x06parent\"Q\n" + + "\x18ListUserSessionsResponse\x125\n" + + "\bsessions\x18\x01 \x03(\v2\x19.memos.api.v1.UserSessionR\bsessions\"P\n" + + "\x18RevokeUserSessionRequest\x124\n" + + "\x04name\x18\x01 \x01(\tB \xe0A\x02\xfaA\x1a\n" + + "\x18memos.api.v1/UserSessionR\x04name\"_\n" + + "\x17ListAllUserStatsRequest\x12 \n" + + "\tpage_size\x18\x01 \x01(\x05B\x03\xe0A\x01R\bpageSize\x12\"\n" + + "\n" + + "page_token\x18\x02 \x01(\tB\x03\xe0A\x01R\tpageToken\"\x99\x01\n" + + "\x18ListAllUserStatsResponse\x126\n" + + "\n" + + "user_stats\x18\x01 \x03(\v2\x17.memos.api.v1.UserStatsR\tuserStats\x12&\n" + + "\x0fnext_page_token\x18\x02 \x01(\tR\rnextPageToken\x12\x1d\n" + + "\n" + + "total_size\x18\x03 \x01(\x05R\ttotalSize2\xe2\x10\n" + + "\vUserService\x12c\n" + + "\tListUsers\x12\x1e.memos.api.v1.ListUsersRequest\x1a\x1f.memos.api.v1.ListUsersResponse\"\x15\x82\xd3\xe4\x93\x02\x0f\x12\r/api/v1/users\x12b\n" + + "\aGetUser\x12\x1c.memos.api.v1.GetUserRequest\x1a\x12.memos.api.v1.User\"%\xdaA\x04name\x82\xd3\xe4\x93\x02\x18\x12\x16/api/v1/{name=users/*}\x12e\n" + + "\n" + + "CreateUser\x12\x1f.memos.api.v1.CreateUserRequest\x1a\x12.memos.api.v1.User\"\"\xdaA\x04user\x82\xd3\xe4\x93\x02\x15:\x04user\"\r/api/v1/users\x12\x7f\n" + + "\n" + + "UpdateUser\x12\x1f.memos.api.v1.UpdateUserRequest\x1a\x12.memos.api.v1.User\"<\xdaA\x10user,update_mask\x82\xd3\xe4\x93\x02#:\x04user2\x1b/api/v1/{user.name=users/*}\x12l\n" + + "\n" + + "DeleteUser\x12\x1f.memos.api.v1.DeleteUserRequest\x1a\x16.google.protobuf.Empty\"%\xdaA\x04name\x82\xd3\xe4\x93\x02\x18*\x16/api/v1/{name=users/*}\x12x\n" + + "\vSearchUsers\x12 .memos.api.v1.SearchUsersRequest\x1a!.memos.api.v1.SearchUsersResponse\"$\xdaA\x05query\x82\xd3\xe4\x93\x02\x16\x12\x14/api/v1/users:search\x12w\n" + + "\rGetUserAvatar\x12\".memos.api.v1.GetUserAvatarRequest\x1a\x14.google.api.HttpBody\",\xdaA\x04name\x82\xd3\xe4\x93\x02\x1f\x12\x1d/api/v1/{name=users/*}/avatar\x12~\n" + + "\x10ListAllUserStats\x12%.memos.api.v1.ListAllUserStatsRequest\x1a&.memos.api.v1.ListAllUserStatsResponse\"\x1b\x82\xd3\xe4\x93\x02\x15\x12\x13/api/v1/users:stats\x12z\n" + + "\fGetUserStats\x12!.memos.api.v1.GetUserStatsRequest\x1a\x17.memos.api.v1.UserStats\".\xdaA\x04name\x82\xd3\xe4\x93\x02!\x12\x1f/api/v1/{name=users/*}:getStats\x12\x82\x01\n" + + "\x0eGetUserSetting\x12#.memos.api.v1.GetUserSettingRequest\x1a\x19.memos.api.v1.UserSetting\"0\xdaA\x04name\x82\xd3\xe4\x93\x02#\x12!/api/v1/{name=users/*}:getSetting\x12\xab\x01\n" + + "\x11UpdateUserSetting\x12&.memos.api.v1.UpdateUserSettingRequest\x1a\x19.memos.api.v1.UserSetting\"S\xdaA\x13setting,update_mask\x82\xd3\xe4\x93\x027:\asetting2,/api/v1/{setting.name=users/*}:updateSetting\x12\xa5\x01\n" + + "\x14ListUserAccessTokens\x12).memos.api.v1.ListUserAccessTokensRequest\x1a*.memos.api.v1.ListUserAccessTokensResponse\"6\xdaA\x06parent\x82\xd3\xe4\x93\x02'\x12%/api/v1/{parent=users/*}/accessTokens\x12\xb5\x01\n" + + "\x15CreateUserAccessToken\x12*.memos.api.v1.CreateUserAccessTokenRequest\x1a\x1d.memos.api.v1.UserAccessToken\"Q\xdaA\x13parent,access_token\x82\xd3\xe4\x93\x025:\faccess_token\"%/api/v1/{parent=users/*}/accessTokens\x12\x91\x01\n" + + "\x15DeleteUserAccessToken\x12*.memos.api.v1.DeleteUserAccessTokenRequest\x1a\x16.google.protobuf.Empty\"4\xdaA\x04name\x82\xd3\xe4\x93\x02'*%/api/v1/{name=users/*/accessTokens/*}\x12\x95\x01\n" + + "\x10ListUserSessions\x12%.memos.api.v1.ListUserSessionsRequest\x1a&.memos.api.v1.ListUserSessionsResponse\"2\xdaA\x06parent\x82\xd3\xe4\x93\x02#\x12!/api/v1/{parent=users/*}/sessions\x12\x85\x01\n" + + "\x11RevokeUserSession\x12&.memos.api.v1.RevokeUserSessionRequest\x1a\x16.google.protobuf.Empty\"0\xdaA\x04name\x82\xd3\xe4\x93\x02#*!/api/v1/{name=users/*/sessions/*}B\xa8\x01\n" + + "\x10com.memos.api.v1B\x10UserServiceProtoP\x01Z0github.com/usememos/memos/proto/gen/api/v1;apiv1\xa2\x02\x03MAX\xaa\x02\fMemos.Api.V1\xca\x02\fMemos\\Api\\V1\xe2\x02\x18Memos\\Api\\V1\\GPBMetadata\xea\x02\x0eMemos::Api::V1b\x06proto3" + +var ( + file_api_v1_user_service_proto_rawDescOnce sync.Once + file_api_v1_user_service_proto_rawDescData []byte +) + +func file_api_v1_user_service_proto_rawDescGZIP() []byte { + file_api_v1_user_service_proto_rawDescOnce.Do(func() { + file_api_v1_user_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_v1_user_service_proto_rawDesc), len(file_api_v1_user_service_proto_rawDesc))) + }) + return file_api_v1_user_service_proto_rawDescData +} + +var file_api_v1_user_service_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_api_v1_user_service_proto_msgTypes = make([]protoimpl.MessageInfo, 29) +var file_api_v1_user_service_proto_goTypes = []any{ + (User_Role)(0), // 0: memos.api.v1.User.Role + (*User)(nil), // 1: memos.api.v1.User + (*ListUsersRequest)(nil), // 2: memos.api.v1.ListUsersRequest + (*ListUsersResponse)(nil), // 3: memos.api.v1.ListUsersResponse + (*GetUserRequest)(nil), // 4: memos.api.v1.GetUserRequest + (*CreateUserRequest)(nil), // 5: memos.api.v1.CreateUserRequest + (*UpdateUserRequest)(nil), // 6: memos.api.v1.UpdateUserRequest + (*DeleteUserRequest)(nil), // 7: memos.api.v1.DeleteUserRequest + (*SearchUsersRequest)(nil), // 8: memos.api.v1.SearchUsersRequest + (*SearchUsersResponse)(nil), // 9: memos.api.v1.SearchUsersResponse + (*GetUserAvatarRequest)(nil), // 10: memos.api.v1.GetUserAvatarRequest + (*UserStats)(nil), // 11: memos.api.v1.UserStats + (*GetUserStatsRequest)(nil), // 12: memos.api.v1.GetUserStatsRequest + (*UserSetting)(nil), // 13: memos.api.v1.UserSetting + (*GetUserSettingRequest)(nil), // 14: memos.api.v1.GetUserSettingRequest + (*UpdateUserSettingRequest)(nil), // 15: memos.api.v1.UpdateUserSettingRequest + (*UserAccessToken)(nil), // 16: memos.api.v1.UserAccessToken + (*ListUserAccessTokensRequest)(nil), // 17: memos.api.v1.ListUserAccessTokensRequest + (*ListUserAccessTokensResponse)(nil), // 18: memos.api.v1.ListUserAccessTokensResponse + (*CreateUserAccessTokenRequest)(nil), // 19: memos.api.v1.CreateUserAccessTokenRequest + (*DeleteUserAccessTokenRequest)(nil), // 20: memos.api.v1.DeleteUserAccessTokenRequest + (*UserSession)(nil), // 21: memos.api.v1.UserSession + (*ListUserSessionsRequest)(nil), // 22: memos.api.v1.ListUserSessionsRequest + (*ListUserSessionsResponse)(nil), // 23: memos.api.v1.ListUserSessionsResponse + (*RevokeUserSessionRequest)(nil), // 24: memos.api.v1.RevokeUserSessionRequest + (*ListAllUserStatsRequest)(nil), // 25: memos.api.v1.ListAllUserStatsRequest + (*ListAllUserStatsResponse)(nil), // 26: memos.api.v1.ListAllUserStatsResponse + nil, // 27: memos.api.v1.UserStats.TagCountEntry + (*UserStats_MemoTypeStats)(nil), // 28: memos.api.v1.UserStats.MemoTypeStats + (*UserSession_ClientInfo)(nil), // 29: memos.api.v1.UserSession.ClientInfo + (State)(0), // 30: memos.api.v1.State + (*timestamppb.Timestamp)(nil), // 31: google.protobuf.Timestamp + (*fieldmaskpb.FieldMask)(nil), // 32: google.protobuf.FieldMask + (*emptypb.Empty)(nil), // 33: google.protobuf.Empty + (*httpbody.HttpBody)(nil), // 34: google.api.HttpBody +} +var file_api_v1_user_service_proto_depIdxs = []int32{ + 0, // 0: memos.api.v1.User.role:type_name -> memos.api.v1.User.Role + 30, // 1: memos.api.v1.User.state:type_name -> memos.api.v1.State + 31, // 2: memos.api.v1.User.create_time:type_name -> google.protobuf.Timestamp + 31, // 3: memos.api.v1.User.update_time:type_name -> google.protobuf.Timestamp + 1, // 4: memos.api.v1.ListUsersResponse.users:type_name -> memos.api.v1.User + 32, // 5: memos.api.v1.GetUserRequest.read_mask:type_name -> google.protobuf.FieldMask + 1, // 6: memos.api.v1.CreateUserRequest.user:type_name -> memos.api.v1.User + 1, // 7: memos.api.v1.UpdateUserRequest.user:type_name -> memos.api.v1.User + 32, // 8: memos.api.v1.UpdateUserRequest.update_mask:type_name -> google.protobuf.FieldMask + 1, // 9: memos.api.v1.SearchUsersResponse.users:type_name -> memos.api.v1.User + 31, // 10: memos.api.v1.UserStats.memo_display_timestamps:type_name -> google.protobuf.Timestamp + 28, // 11: memos.api.v1.UserStats.memo_type_stats:type_name -> memos.api.v1.UserStats.MemoTypeStats + 27, // 12: memos.api.v1.UserStats.tag_count:type_name -> memos.api.v1.UserStats.TagCountEntry + 13, // 13: memos.api.v1.UpdateUserSettingRequest.setting:type_name -> memos.api.v1.UserSetting + 32, // 14: memos.api.v1.UpdateUserSettingRequest.update_mask:type_name -> google.protobuf.FieldMask + 31, // 15: memos.api.v1.UserAccessToken.issued_at:type_name -> google.protobuf.Timestamp + 31, // 16: memos.api.v1.UserAccessToken.expires_at:type_name -> google.protobuf.Timestamp + 16, // 17: memos.api.v1.ListUserAccessTokensResponse.access_tokens:type_name -> memos.api.v1.UserAccessToken + 16, // 18: memos.api.v1.CreateUserAccessTokenRequest.access_token:type_name -> memos.api.v1.UserAccessToken + 31, // 19: memos.api.v1.UserSession.create_time:type_name -> google.protobuf.Timestamp + 31, // 20: memos.api.v1.UserSession.last_accessed_time:type_name -> google.protobuf.Timestamp + 29, // 21: memos.api.v1.UserSession.client_info:type_name -> memos.api.v1.UserSession.ClientInfo + 21, // 22: memos.api.v1.ListUserSessionsResponse.sessions:type_name -> memos.api.v1.UserSession + 11, // 23: memos.api.v1.ListAllUserStatsResponse.user_stats:type_name -> memos.api.v1.UserStats + 2, // 24: memos.api.v1.UserService.ListUsers:input_type -> memos.api.v1.ListUsersRequest + 4, // 25: memos.api.v1.UserService.GetUser:input_type -> memos.api.v1.GetUserRequest + 5, // 26: memos.api.v1.UserService.CreateUser:input_type -> memos.api.v1.CreateUserRequest + 6, // 27: memos.api.v1.UserService.UpdateUser:input_type -> memos.api.v1.UpdateUserRequest + 7, // 28: memos.api.v1.UserService.DeleteUser:input_type -> memos.api.v1.DeleteUserRequest + 8, // 29: memos.api.v1.UserService.SearchUsers:input_type -> memos.api.v1.SearchUsersRequest + 10, // 30: memos.api.v1.UserService.GetUserAvatar:input_type -> memos.api.v1.GetUserAvatarRequest + 25, // 31: memos.api.v1.UserService.ListAllUserStats:input_type -> memos.api.v1.ListAllUserStatsRequest + 12, // 32: memos.api.v1.UserService.GetUserStats:input_type -> memos.api.v1.GetUserStatsRequest + 14, // 33: memos.api.v1.UserService.GetUserSetting:input_type -> memos.api.v1.GetUserSettingRequest + 15, // 34: memos.api.v1.UserService.UpdateUserSetting:input_type -> memos.api.v1.UpdateUserSettingRequest + 17, // 35: memos.api.v1.UserService.ListUserAccessTokens:input_type -> memos.api.v1.ListUserAccessTokensRequest + 19, // 36: memos.api.v1.UserService.CreateUserAccessToken:input_type -> memos.api.v1.CreateUserAccessTokenRequest + 20, // 37: memos.api.v1.UserService.DeleteUserAccessToken:input_type -> memos.api.v1.DeleteUserAccessTokenRequest + 22, // 38: memos.api.v1.UserService.ListUserSessions:input_type -> memos.api.v1.ListUserSessionsRequest + 24, // 39: memos.api.v1.UserService.RevokeUserSession:input_type -> memos.api.v1.RevokeUserSessionRequest + 3, // 40: memos.api.v1.UserService.ListUsers:output_type -> memos.api.v1.ListUsersResponse + 1, // 41: memos.api.v1.UserService.GetUser:output_type -> memos.api.v1.User + 1, // 42: memos.api.v1.UserService.CreateUser:output_type -> memos.api.v1.User + 1, // 43: memos.api.v1.UserService.UpdateUser:output_type -> memos.api.v1.User + 33, // 44: memos.api.v1.UserService.DeleteUser:output_type -> google.protobuf.Empty + 9, // 45: memos.api.v1.UserService.SearchUsers:output_type -> memos.api.v1.SearchUsersResponse + 34, // 46: memos.api.v1.UserService.GetUserAvatar:output_type -> google.api.HttpBody + 26, // 47: memos.api.v1.UserService.ListAllUserStats:output_type -> memos.api.v1.ListAllUserStatsResponse + 11, // 48: memos.api.v1.UserService.GetUserStats:output_type -> memos.api.v1.UserStats + 13, // 49: memos.api.v1.UserService.GetUserSetting:output_type -> memos.api.v1.UserSetting + 13, // 50: memos.api.v1.UserService.UpdateUserSetting:output_type -> memos.api.v1.UserSetting + 18, // 51: memos.api.v1.UserService.ListUserAccessTokens:output_type -> memos.api.v1.ListUserAccessTokensResponse + 16, // 52: memos.api.v1.UserService.CreateUserAccessToken:output_type -> memos.api.v1.UserAccessToken + 33, // 53: memos.api.v1.UserService.DeleteUserAccessToken:output_type -> google.protobuf.Empty + 23, // 54: memos.api.v1.UserService.ListUserSessions:output_type -> memos.api.v1.ListUserSessionsResponse + 33, // 55: memos.api.v1.UserService.RevokeUserSession:output_type -> google.protobuf.Empty + 40, // [40:56] is the sub-list for method output_type + 24, // [24:40] is the sub-list for method input_type + 24, // [24:24] is the sub-list for extension type_name + 24, // [24:24] is the sub-list for extension extendee + 0, // [0:24] is the sub-list for field type_name +} + +func init() { file_api_v1_user_service_proto_init() } +func file_api_v1_user_service_proto_init() { + if File_api_v1_user_service_proto != nil { + return + } + file_api_v1_common_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_user_service_proto_rawDesc), len(file_api_v1_user_service_proto_rawDesc)), + NumEnums: 1, + NumMessages: 29, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_api_v1_user_service_proto_goTypes, + DependencyIndexes: file_api_v1_user_service_proto_depIdxs, + EnumInfos: file_api_v1_user_service_proto_enumTypes, + MessageInfos: file_api_v1_user_service_proto_msgTypes, + }.Build() + File_api_v1_user_service_proto = out.File + file_api_v1_user_service_proto_goTypes = nil + file_api_v1_user_service_proto_depIdxs = nil +} diff --git a/proto/gen/api/v1/user_service.pb.gw.go b/proto/gen/api/v1/user_service.pb.gw.go new file mode 100644 index 0000000..0432db8 --- /dev/null +++ b/proto/gen/api/v1/user_service.pb.gw.go @@ -0,0 +1,1475 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: api/v1/user_service.proto + +/* +Package apiv1 is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package apiv1 + +import ( + "context" + "errors" + "io" + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// Suppress "imported and not used" errors +var ( + _ codes.Code + _ io.Reader + _ status.Status + _ = errors.New + _ = runtime.String + _ = utilities.NewDoubleArray + _ = metadata.Join +) + +var filter_UserService_ListUsers_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + +func request_UserService_ListUsers_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListUsersRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_ListUsers_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.ListUsers(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_UserService_ListUsers_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListUsersRequest + metadata runtime.ServerMetadata + ) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_ListUsers_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ListUsers(ctx, &protoReq) + return msg, metadata, err +} + +var filter_UserService_GetUser_0 = &utilities.DoubleArray{Encoding: map[string]int{"name": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} + +func request_UserService_GetUser_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetUserRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_GetUser_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.GetUser(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_UserService_GetUser_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetUserRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_GetUser_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.GetUser(ctx, &protoReq) + return msg, metadata, err +} + +var filter_UserService_CreateUser_0 = &utilities.DoubleArray{Encoding: map[string]int{"user": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} + +func request_UserService_CreateUser_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateUserRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.User); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_CreateUser_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.CreateUser(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_UserService_CreateUser_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateUserRequest + metadata runtime.ServerMetadata + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.User); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_CreateUser_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.CreateUser(ctx, &protoReq) + return msg, metadata, err +} + +var filter_UserService_UpdateUser_0 = &utilities.DoubleArray{Encoding: map[string]int{"user": 0, "name": 1}, Base: []int{1, 2, 1, 0, 0}, Check: []int{0, 1, 2, 3, 2}} + +func request_UserService_UpdateUser_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateUserRequest + metadata runtime.ServerMetadata + err error + ) + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.User); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 { + if fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.User); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } else { + protoReq.UpdateMask = fieldMask + } + } + val, ok := pathParams["user.name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "user.name") + } + err = runtime.PopulateFieldFromPath(&protoReq, "user.name", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "user.name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_UpdateUser_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.UpdateUser(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_UserService_UpdateUser_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateUserRequest + metadata runtime.ServerMetadata + err error + ) + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.User); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 { + if fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.User); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } else { + protoReq.UpdateMask = fieldMask + } + } + val, ok := pathParams["user.name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "user.name") + } + err = runtime.PopulateFieldFromPath(&protoReq, "user.name", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "user.name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_UpdateUser_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.UpdateUser(ctx, &protoReq) + return msg, metadata, err +} + +var filter_UserService_DeleteUser_0 = &utilities.DoubleArray{Encoding: map[string]int{"name": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} + +func request_UserService_DeleteUser_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteUserRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_DeleteUser_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.DeleteUser(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_UserService_DeleteUser_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteUserRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_DeleteUser_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.DeleteUser(ctx, &protoReq) + return msg, metadata, err +} + +var filter_UserService_SearchUsers_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + +func request_UserService_SearchUsers_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq SearchUsersRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_SearchUsers_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.SearchUsers(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_UserService_SearchUsers_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq SearchUsersRequest + metadata runtime.ServerMetadata + ) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_SearchUsers_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.SearchUsers(ctx, &protoReq) + return msg, metadata, err +} + +func request_UserService_GetUserAvatar_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetUserAvatarRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := client.GetUserAvatar(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_UserService_GetUserAvatar_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetUserAvatarRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := server.GetUserAvatar(ctx, &protoReq) + return msg, metadata, err +} + +var filter_UserService_ListAllUserStats_0 = &utilities.DoubleArray{Encoding: map[string]int{}, Base: []int(nil), Check: []int(nil)} + +func request_UserService_ListAllUserStats_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListAllUserStatsRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_ListAllUserStats_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.ListAllUserStats(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_UserService_ListAllUserStats_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListAllUserStatsRequest + metadata runtime.ServerMetadata + ) + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_ListAllUserStats_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ListAllUserStats(ctx, &protoReq) + return msg, metadata, err +} + +func request_UserService_GetUserStats_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetUserStatsRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := client.GetUserStats(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_UserService_GetUserStats_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetUserStatsRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := server.GetUserStats(ctx, &protoReq) + return msg, metadata, err +} + +func request_UserService_GetUserSetting_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetUserSettingRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := client.GetUserSetting(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_UserService_GetUserSetting_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetUserSettingRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := server.GetUserSetting(ctx, &protoReq) + return msg, metadata, err +} + +var filter_UserService_UpdateUserSetting_0 = &utilities.DoubleArray{Encoding: map[string]int{"setting": 0, "name": 1}, Base: []int{1, 2, 1, 0, 0}, Check: []int{0, 1, 2, 3, 2}} + +func request_UserService_UpdateUserSetting_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateUserSettingRequest + metadata runtime.ServerMetadata + err error + ) + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Setting); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 { + if fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Setting); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } else { + protoReq.UpdateMask = fieldMask + } + } + val, ok := pathParams["setting.name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "setting.name") + } + err = runtime.PopulateFieldFromPath(&protoReq, "setting.name", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "setting.name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_UpdateUserSetting_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.UpdateUserSetting(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_UserService_UpdateUserSetting_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateUserSettingRequest + metadata runtime.ServerMetadata + err error + ) + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Setting); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 { + if fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Setting); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } else { + protoReq.UpdateMask = fieldMask + } + } + val, ok := pathParams["setting.name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "setting.name") + } + err = runtime.PopulateFieldFromPath(&protoReq, "setting.name", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "setting.name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_UpdateUserSetting_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.UpdateUserSetting(ctx, &protoReq) + return msg, metadata, err +} + +var filter_UserService_ListUserAccessTokens_0 = &utilities.DoubleArray{Encoding: map[string]int{"parent": 0}, Base: []int{1, 1, 0}, Check: []int{0, 1, 2}} + +func request_UserService_ListUserAccessTokens_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListUserAccessTokensRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_ListUserAccessTokens_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.ListUserAccessTokens(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_UserService_ListUserAccessTokens_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListUserAccessTokensRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_ListUserAccessTokens_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.ListUserAccessTokens(ctx, &protoReq) + return msg, metadata, err +} + +var filter_UserService_CreateUserAccessToken_0 = &utilities.DoubleArray{Encoding: map[string]int{"access_token": 0, "parent": 1}, Base: []int{1, 1, 2, 0, 0}, Check: []int{0, 1, 1, 2, 3}} + +func request_UserService_CreateUserAccessToken_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateUserAccessTokenRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.AccessToken); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_CreateUserAccessToken_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.CreateUserAccessToken(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_UserService_CreateUserAccessToken_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateUserAccessTokenRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.AccessToken); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_UserService_CreateUserAccessToken_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.CreateUserAccessToken(ctx, &protoReq) + return msg, metadata, err +} + +func request_UserService_DeleteUserAccessToken_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteUserAccessTokenRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := client.DeleteUserAccessToken(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_UserService_DeleteUserAccessToken_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteUserAccessTokenRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := server.DeleteUserAccessToken(ctx, &protoReq) + return msg, metadata, err +} + +func request_UserService_ListUserSessions_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListUserSessionsRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } + msg, err := client.ListUserSessions(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_UserService_ListUserSessions_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListUserSessionsRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } + msg, err := server.ListUserSessions(ctx, &protoReq) + return msg, metadata, err +} + +func request_UserService_RevokeUserSession_0(ctx context.Context, marshaler runtime.Marshaler, client UserServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq RevokeUserSessionRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := client.RevokeUserSession(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_UserService_RevokeUserSession_0(ctx context.Context, marshaler runtime.Marshaler, server UserServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq RevokeUserSessionRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := server.RevokeUserSession(ctx, &protoReq) + return msg, metadata, err +} + +// RegisterUserServiceHandlerServer registers the http handlers for service UserService to "mux". +// UnaryRPC :call UserServiceServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterUserServiceHandlerFromEndpoint instead. +// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call. +func RegisterUserServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server UserServiceServer) error { + mux.Handle(http.MethodGet, pattern_UserService_ListUsers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/ListUsers", runtime.WithHTTPPathPattern("/api/v1/users")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_UserService_ListUsers_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_ListUsers_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_UserService_GetUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/GetUser", runtime.WithHTTPPathPattern("/api/v1/{name=users/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_UserService_GetUser_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_GetUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_UserService_CreateUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/CreateUser", runtime.WithHTTPPathPattern("/api/v1/users")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_UserService_CreateUser_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_CreateUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPatch, pattern_UserService_UpdateUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/UpdateUser", runtime.WithHTTPPathPattern("/api/v1/{user.name=users/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_UserService_UpdateUser_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_UpdateUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_UserService_DeleteUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/DeleteUser", runtime.WithHTTPPathPattern("/api/v1/{name=users/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_UserService_DeleteUser_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_DeleteUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_UserService_SearchUsers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/SearchUsers", runtime.WithHTTPPathPattern("/api/v1/users:search")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_UserService_SearchUsers_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_SearchUsers_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_UserService_GetUserAvatar_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/GetUserAvatar", runtime.WithHTTPPathPattern("/api/v1/{name=users/*}/avatar")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_UserService_GetUserAvatar_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_GetUserAvatar_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_UserService_ListAllUserStats_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/ListAllUserStats", runtime.WithHTTPPathPattern("/api/v1/users:stats")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_UserService_ListAllUserStats_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_ListAllUserStats_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_UserService_GetUserStats_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/GetUserStats", runtime.WithHTTPPathPattern("/api/v1/{name=users/*}:getStats")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_UserService_GetUserStats_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_GetUserStats_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_UserService_GetUserSetting_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/GetUserSetting", runtime.WithHTTPPathPattern("/api/v1/{name=users/*}:getSetting")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_UserService_GetUserSetting_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_GetUserSetting_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPatch, pattern_UserService_UpdateUserSetting_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/UpdateUserSetting", runtime.WithHTTPPathPattern("/api/v1/{setting.name=users/*}:updateSetting")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_UserService_UpdateUserSetting_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_UpdateUserSetting_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_UserService_ListUserAccessTokens_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/ListUserAccessTokens", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/accessTokens")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_UserService_ListUserAccessTokens_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_ListUserAccessTokens_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_UserService_CreateUserAccessToken_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/CreateUserAccessToken", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/accessTokens")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_UserService_CreateUserAccessToken_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_CreateUserAccessToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_UserService_DeleteUserAccessToken_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/DeleteUserAccessToken", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/accessTokens/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_UserService_DeleteUserAccessToken_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_DeleteUserAccessToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_UserService_ListUserSessions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/ListUserSessions", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/sessions")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_UserService_ListUserSessions_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_ListUserSessions_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_UserService_RevokeUserSession_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.UserService/RevokeUserSession", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/sessions/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_UserService_RevokeUserSession_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_RevokeUserSession_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + + return nil +} + +// RegisterUserServiceHandlerFromEndpoint is same as RegisterUserServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterUserServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.NewClient(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + return RegisterUserServiceHandler(ctx, mux, conn) +} + +// RegisterUserServiceHandler registers the http handlers for service UserService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterUserServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterUserServiceHandlerClient(ctx, mux, NewUserServiceClient(conn)) +} + +// RegisterUserServiceHandlerClient registers the http handlers for service UserService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "UserServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "UserServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "UserServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares. +func RegisterUserServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client UserServiceClient) error { + mux.Handle(http.MethodGet, pattern_UserService_ListUsers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/ListUsers", runtime.WithHTTPPathPattern("/api/v1/users")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_UserService_ListUsers_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_ListUsers_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_UserService_GetUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/GetUser", runtime.WithHTTPPathPattern("/api/v1/{name=users/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_UserService_GetUser_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_GetUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_UserService_CreateUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/CreateUser", runtime.WithHTTPPathPattern("/api/v1/users")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_UserService_CreateUser_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_CreateUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPatch, pattern_UserService_UpdateUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/UpdateUser", runtime.WithHTTPPathPattern("/api/v1/{user.name=users/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_UserService_UpdateUser_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_UpdateUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_UserService_DeleteUser_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/DeleteUser", runtime.WithHTTPPathPattern("/api/v1/{name=users/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_UserService_DeleteUser_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_DeleteUser_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_UserService_SearchUsers_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/SearchUsers", runtime.WithHTTPPathPattern("/api/v1/users:search")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_UserService_SearchUsers_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_SearchUsers_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_UserService_GetUserAvatar_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/GetUserAvatar", runtime.WithHTTPPathPattern("/api/v1/{name=users/*}/avatar")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_UserService_GetUserAvatar_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_GetUserAvatar_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_UserService_ListAllUserStats_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/ListAllUserStats", runtime.WithHTTPPathPattern("/api/v1/users:stats")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_UserService_ListAllUserStats_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_ListAllUserStats_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_UserService_GetUserStats_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/GetUserStats", runtime.WithHTTPPathPattern("/api/v1/{name=users/*}:getStats")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_UserService_GetUserStats_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_GetUserStats_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_UserService_GetUserSetting_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/GetUserSetting", runtime.WithHTTPPathPattern("/api/v1/{name=users/*}:getSetting")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_UserService_GetUserSetting_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_GetUserSetting_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPatch, pattern_UserService_UpdateUserSetting_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/UpdateUserSetting", runtime.WithHTTPPathPattern("/api/v1/{setting.name=users/*}:updateSetting")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_UserService_UpdateUserSetting_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_UpdateUserSetting_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_UserService_ListUserAccessTokens_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/ListUserAccessTokens", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/accessTokens")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_UserService_ListUserAccessTokens_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_ListUserAccessTokens_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_UserService_CreateUserAccessToken_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/CreateUserAccessToken", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/accessTokens")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_UserService_CreateUserAccessToken_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_CreateUserAccessToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_UserService_DeleteUserAccessToken_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/DeleteUserAccessToken", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/accessTokens/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_UserService_DeleteUserAccessToken_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_DeleteUserAccessToken_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_UserService_ListUserSessions_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/ListUserSessions", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/sessions")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_UserService_ListUserSessions_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_ListUserSessions_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_UserService_RevokeUserSession_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.UserService/RevokeUserSession", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/sessions/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_UserService_RevokeUserSession_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_UserService_RevokeUserSession_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + return nil +} + +var ( + pattern_UserService_ListUsers_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "users"}, "")) + pattern_UserService_GetUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "name"}, "")) + pattern_UserService_CreateUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "users"}, "")) + pattern_UserService_UpdateUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "user.name"}, "")) + pattern_UserService_DeleteUser_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "name"}, "")) + pattern_UserService_SearchUsers_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "users"}, "search")) + pattern_UserService_GetUserAvatar_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "name", "avatar"}, "")) + pattern_UserService_ListAllUserStats_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"api", "v1", "users"}, "stats")) + pattern_UserService_GetUserStats_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "name"}, "getStats")) + pattern_UserService_GetUserSetting_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "name"}, "getSetting")) + pattern_UserService_UpdateUserSetting_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3}, []string{"api", "v1", "users", "setting.name"}, "updateSetting")) + pattern_UserService_ListUserAccessTokens_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "accessTokens"}, "")) + pattern_UserService_CreateUserAccessToken_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "accessTokens"}, "")) + pattern_UserService_DeleteUserAccessToken_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "accessTokens", "name"}, "")) + pattern_UserService_ListUserSessions_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "sessions"}, "")) + pattern_UserService_RevokeUserSession_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "sessions", "name"}, "")) +) + +var ( + forward_UserService_ListUsers_0 = runtime.ForwardResponseMessage + forward_UserService_GetUser_0 = runtime.ForwardResponseMessage + forward_UserService_CreateUser_0 = runtime.ForwardResponseMessage + forward_UserService_UpdateUser_0 = runtime.ForwardResponseMessage + forward_UserService_DeleteUser_0 = runtime.ForwardResponseMessage + forward_UserService_SearchUsers_0 = runtime.ForwardResponseMessage + forward_UserService_GetUserAvatar_0 = runtime.ForwardResponseMessage + forward_UserService_ListAllUserStats_0 = runtime.ForwardResponseMessage + forward_UserService_GetUserStats_0 = runtime.ForwardResponseMessage + forward_UserService_GetUserSetting_0 = runtime.ForwardResponseMessage + forward_UserService_UpdateUserSetting_0 = runtime.ForwardResponseMessage + forward_UserService_ListUserAccessTokens_0 = runtime.ForwardResponseMessage + forward_UserService_CreateUserAccessToken_0 = runtime.ForwardResponseMessage + forward_UserService_DeleteUserAccessToken_0 = runtime.ForwardResponseMessage + forward_UserService_ListUserSessions_0 = runtime.ForwardResponseMessage + forward_UserService_RevokeUserSession_0 = runtime.ForwardResponseMessage +) diff --git a/proto/gen/api/v1/user_service_grpc.pb.go b/proto/gen/api/v1/user_service_grpc.pb.go new file mode 100644 index 0000000..8f2c556 --- /dev/null +++ b/proto/gen/api/v1/user_service_grpc.pb.go @@ -0,0 +1,725 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc (unknown) +// source: api/v1/user_service.proto + +package apiv1 + +import ( + context "context" + httpbody "google.golang.org/genproto/googleapis/api/httpbody" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + UserService_ListUsers_FullMethodName = "/memos.api.v1.UserService/ListUsers" + UserService_GetUser_FullMethodName = "/memos.api.v1.UserService/GetUser" + UserService_CreateUser_FullMethodName = "/memos.api.v1.UserService/CreateUser" + UserService_UpdateUser_FullMethodName = "/memos.api.v1.UserService/UpdateUser" + UserService_DeleteUser_FullMethodName = "/memos.api.v1.UserService/DeleteUser" + UserService_SearchUsers_FullMethodName = "/memos.api.v1.UserService/SearchUsers" + UserService_GetUserAvatar_FullMethodName = "/memos.api.v1.UserService/GetUserAvatar" + UserService_ListAllUserStats_FullMethodName = "/memos.api.v1.UserService/ListAllUserStats" + UserService_GetUserStats_FullMethodName = "/memos.api.v1.UserService/GetUserStats" + UserService_GetUserSetting_FullMethodName = "/memos.api.v1.UserService/GetUserSetting" + UserService_UpdateUserSetting_FullMethodName = "/memos.api.v1.UserService/UpdateUserSetting" + UserService_ListUserAccessTokens_FullMethodName = "/memos.api.v1.UserService/ListUserAccessTokens" + UserService_CreateUserAccessToken_FullMethodName = "/memos.api.v1.UserService/CreateUserAccessToken" + UserService_DeleteUserAccessToken_FullMethodName = "/memos.api.v1.UserService/DeleteUserAccessToken" + UserService_ListUserSessions_FullMethodName = "/memos.api.v1.UserService/ListUserSessions" + UserService_RevokeUserSession_FullMethodName = "/memos.api.v1.UserService/RevokeUserSession" +) + +// UserServiceClient is the client API for UserService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type UserServiceClient interface { + // ListUsers returns a list of users. + ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (*ListUsersResponse, error) + // GetUser gets a user by name. + GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*User, error) + // CreateUser creates a new user. + CreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*User, error) + // UpdateUser updates a user. + UpdateUser(ctx context.Context, in *UpdateUserRequest, opts ...grpc.CallOption) (*User, error) + // DeleteUser deletes a user. + DeleteUser(ctx context.Context, in *DeleteUserRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + // SearchUsers searches for users based on query. + SearchUsers(ctx context.Context, in *SearchUsersRequest, opts ...grpc.CallOption) (*SearchUsersResponse, error) + // GetUserAvatar gets the avatar of a user. + GetUserAvatar(ctx context.Context, in *GetUserAvatarRequest, opts ...grpc.CallOption) (*httpbody.HttpBody, error) + // ListAllUserStats returns statistics for all users. + ListAllUserStats(ctx context.Context, in *ListAllUserStatsRequest, opts ...grpc.CallOption) (*ListAllUserStatsResponse, error) + // GetUserStats returns statistics for a specific user. + GetUserStats(ctx context.Context, in *GetUserStatsRequest, opts ...grpc.CallOption) (*UserStats, error) + // GetUserSetting returns the user setting. + GetUserSetting(ctx context.Context, in *GetUserSettingRequest, opts ...grpc.CallOption) (*UserSetting, error) + // UpdateUserSetting updates the user setting. + UpdateUserSetting(ctx context.Context, in *UpdateUserSettingRequest, opts ...grpc.CallOption) (*UserSetting, error) + // ListUserAccessTokens returns a list of access tokens for a user. + ListUserAccessTokens(ctx context.Context, in *ListUserAccessTokensRequest, opts ...grpc.CallOption) (*ListUserAccessTokensResponse, error) + // CreateUserAccessToken creates a new access token for a user. + CreateUserAccessToken(ctx context.Context, in *CreateUserAccessTokenRequest, opts ...grpc.CallOption) (*UserAccessToken, error) + // DeleteUserAccessToken deletes an access token. + DeleteUserAccessToken(ctx context.Context, in *DeleteUserAccessTokenRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) + // ListUserSessions returns a list of active sessions for a user. + ListUserSessions(ctx context.Context, in *ListUserSessionsRequest, opts ...grpc.CallOption) (*ListUserSessionsResponse, error) + // RevokeUserSession revokes a specific session for a user. + RevokeUserSession(ctx context.Context, in *RevokeUserSessionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +type userServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewUserServiceClient(cc grpc.ClientConnInterface) UserServiceClient { + return &userServiceClient{cc} +} + +func (c *userServiceClient) ListUsers(ctx context.Context, in *ListUsersRequest, opts ...grpc.CallOption) (*ListUsersResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListUsersResponse) + err := c.cc.Invoke(ctx, UserService_ListUsers_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *userServiceClient) GetUser(ctx context.Context, in *GetUserRequest, opts ...grpc.CallOption) (*User, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(User) + err := c.cc.Invoke(ctx, UserService_GetUser_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *userServiceClient) CreateUser(ctx context.Context, in *CreateUserRequest, opts ...grpc.CallOption) (*User, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(User) + err := c.cc.Invoke(ctx, UserService_CreateUser_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *userServiceClient) UpdateUser(ctx context.Context, in *UpdateUserRequest, opts ...grpc.CallOption) (*User, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(User) + err := c.cc.Invoke(ctx, UserService_UpdateUser_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *userServiceClient) DeleteUser(ctx context.Context, in *DeleteUserRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, UserService_DeleteUser_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *userServiceClient) SearchUsers(ctx context.Context, in *SearchUsersRequest, opts ...grpc.CallOption) (*SearchUsersResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(SearchUsersResponse) + err := c.cc.Invoke(ctx, UserService_SearchUsers_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *userServiceClient) GetUserAvatar(ctx context.Context, in *GetUserAvatarRequest, opts ...grpc.CallOption) (*httpbody.HttpBody, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(httpbody.HttpBody) + err := c.cc.Invoke(ctx, UserService_GetUserAvatar_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *userServiceClient) ListAllUserStats(ctx context.Context, in *ListAllUserStatsRequest, opts ...grpc.CallOption) (*ListAllUserStatsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListAllUserStatsResponse) + err := c.cc.Invoke(ctx, UserService_ListAllUserStats_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *userServiceClient) GetUserStats(ctx context.Context, in *GetUserStatsRequest, opts ...grpc.CallOption) (*UserStats, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UserStats) + err := c.cc.Invoke(ctx, UserService_GetUserStats_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *userServiceClient) GetUserSetting(ctx context.Context, in *GetUserSettingRequest, opts ...grpc.CallOption) (*UserSetting, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UserSetting) + err := c.cc.Invoke(ctx, UserService_GetUserSetting_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *userServiceClient) UpdateUserSetting(ctx context.Context, in *UpdateUserSettingRequest, opts ...grpc.CallOption) (*UserSetting, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UserSetting) + err := c.cc.Invoke(ctx, UserService_UpdateUserSetting_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *userServiceClient) ListUserAccessTokens(ctx context.Context, in *ListUserAccessTokensRequest, opts ...grpc.CallOption) (*ListUserAccessTokensResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListUserAccessTokensResponse) + err := c.cc.Invoke(ctx, UserService_ListUserAccessTokens_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *userServiceClient) CreateUserAccessToken(ctx context.Context, in *CreateUserAccessTokenRequest, opts ...grpc.CallOption) (*UserAccessToken, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UserAccessToken) + err := c.cc.Invoke(ctx, UserService_CreateUserAccessToken_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *userServiceClient) DeleteUserAccessToken(ctx context.Context, in *DeleteUserAccessTokenRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, UserService_DeleteUserAccessToken_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *userServiceClient) ListUserSessions(ctx context.Context, in *ListUserSessionsRequest, opts ...grpc.CallOption) (*ListUserSessionsResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListUserSessionsResponse) + err := c.cc.Invoke(ctx, UserService_ListUserSessions_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *userServiceClient) RevokeUserSession(ctx context.Context, in *RevokeUserSessionRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, UserService_RevokeUserSession_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// UserServiceServer is the server API for UserService service. +// All implementations must embed UnimplementedUserServiceServer +// for forward compatibility. +type UserServiceServer interface { + // ListUsers returns a list of users. + ListUsers(context.Context, *ListUsersRequest) (*ListUsersResponse, error) + // GetUser gets a user by name. + GetUser(context.Context, *GetUserRequest) (*User, error) + // CreateUser creates a new user. + CreateUser(context.Context, *CreateUserRequest) (*User, error) + // UpdateUser updates a user. + UpdateUser(context.Context, *UpdateUserRequest) (*User, error) + // DeleteUser deletes a user. + DeleteUser(context.Context, *DeleteUserRequest) (*emptypb.Empty, error) + // SearchUsers searches for users based on query. + SearchUsers(context.Context, *SearchUsersRequest) (*SearchUsersResponse, error) + // GetUserAvatar gets the avatar of a user. + GetUserAvatar(context.Context, *GetUserAvatarRequest) (*httpbody.HttpBody, error) + // ListAllUserStats returns statistics for all users. + ListAllUserStats(context.Context, *ListAllUserStatsRequest) (*ListAllUserStatsResponse, error) + // GetUserStats returns statistics for a specific user. + GetUserStats(context.Context, *GetUserStatsRequest) (*UserStats, error) + // GetUserSetting returns the user setting. + GetUserSetting(context.Context, *GetUserSettingRequest) (*UserSetting, error) + // UpdateUserSetting updates the user setting. + UpdateUserSetting(context.Context, *UpdateUserSettingRequest) (*UserSetting, error) + // ListUserAccessTokens returns a list of access tokens for a user. + ListUserAccessTokens(context.Context, *ListUserAccessTokensRequest) (*ListUserAccessTokensResponse, error) + // CreateUserAccessToken creates a new access token for a user. + CreateUserAccessToken(context.Context, *CreateUserAccessTokenRequest) (*UserAccessToken, error) + // DeleteUserAccessToken deletes an access token. + DeleteUserAccessToken(context.Context, *DeleteUserAccessTokenRequest) (*emptypb.Empty, error) + // ListUserSessions returns a list of active sessions for a user. + ListUserSessions(context.Context, *ListUserSessionsRequest) (*ListUserSessionsResponse, error) + // RevokeUserSession revokes a specific session for a user. + RevokeUserSession(context.Context, *RevokeUserSessionRequest) (*emptypb.Empty, error) + mustEmbedUnimplementedUserServiceServer() +} + +// UnimplementedUserServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedUserServiceServer struct{} + +func (UnimplementedUserServiceServer) ListUsers(context.Context, *ListUsersRequest) (*ListUsersResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListUsers not implemented") +} +func (UnimplementedUserServiceServer) GetUser(context.Context, *GetUserRequest) (*User, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetUser not implemented") +} +func (UnimplementedUserServiceServer) CreateUser(context.Context, *CreateUserRequest) (*User, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateUser not implemented") +} +func (UnimplementedUserServiceServer) UpdateUser(context.Context, *UpdateUserRequest) (*User, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateUser not implemented") +} +func (UnimplementedUserServiceServer) DeleteUser(context.Context, *DeleteUserRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteUser not implemented") +} +func (UnimplementedUserServiceServer) SearchUsers(context.Context, *SearchUsersRequest) (*SearchUsersResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method SearchUsers not implemented") +} +func (UnimplementedUserServiceServer) GetUserAvatar(context.Context, *GetUserAvatarRequest) (*httpbody.HttpBody, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetUserAvatar not implemented") +} +func (UnimplementedUserServiceServer) ListAllUserStats(context.Context, *ListAllUserStatsRequest) (*ListAllUserStatsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListAllUserStats not implemented") +} +func (UnimplementedUserServiceServer) GetUserStats(context.Context, *GetUserStatsRequest) (*UserStats, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetUserStats not implemented") +} +func (UnimplementedUserServiceServer) GetUserSetting(context.Context, *GetUserSettingRequest) (*UserSetting, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetUserSetting not implemented") +} +func (UnimplementedUserServiceServer) UpdateUserSetting(context.Context, *UpdateUserSettingRequest) (*UserSetting, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateUserSetting not implemented") +} +func (UnimplementedUserServiceServer) ListUserAccessTokens(context.Context, *ListUserAccessTokensRequest) (*ListUserAccessTokensResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListUserAccessTokens not implemented") +} +func (UnimplementedUserServiceServer) CreateUserAccessToken(context.Context, *CreateUserAccessTokenRequest) (*UserAccessToken, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateUserAccessToken not implemented") +} +func (UnimplementedUserServiceServer) DeleteUserAccessToken(context.Context, *DeleteUserAccessTokenRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteUserAccessToken not implemented") +} +func (UnimplementedUserServiceServer) ListUserSessions(context.Context, *ListUserSessionsRequest) (*ListUserSessionsResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListUserSessions not implemented") +} +func (UnimplementedUserServiceServer) RevokeUserSession(context.Context, *RevokeUserSessionRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method RevokeUserSession not implemented") +} +func (UnimplementedUserServiceServer) mustEmbedUnimplementedUserServiceServer() {} +func (UnimplementedUserServiceServer) testEmbeddedByValue() {} + +// UnsafeUserServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to UserServiceServer will +// result in compilation errors. +type UnsafeUserServiceServer interface { + mustEmbedUnimplementedUserServiceServer() +} + +func RegisterUserServiceServer(s grpc.ServiceRegistrar, srv UserServiceServer) { + // If the following call pancis, it indicates UnimplementedUserServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&UserService_ServiceDesc, srv) +} + +func _UserService_ListUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListUsersRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).ListUsers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_ListUsers_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).ListUsers(ctx, req.(*ListUsersRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _UserService_GetUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetUserRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).GetUser(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_GetUser_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).GetUser(ctx, req.(*GetUserRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _UserService_CreateUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateUserRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).CreateUser(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_CreateUser_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).CreateUser(ctx, req.(*CreateUserRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _UserService_UpdateUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateUserRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).UpdateUser(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_UpdateUser_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).UpdateUser(ctx, req.(*UpdateUserRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _UserService_DeleteUser_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteUserRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).DeleteUser(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_DeleteUser_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).DeleteUser(ctx, req.(*DeleteUserRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _UserService_SearchUsers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SearchUsersRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).SearchUsers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_SearchUsers_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).SearchUsers(ctx, req.(*SearchUsersRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _UserService_GetUserAvatar_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetUserAvatarRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).GetUserAvatar(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_GetUserAvatar_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).GetUserAvatar(ctx, req.(*GetUserAvatarRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _UserService_ListAllUserStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListAllUserStatsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).ListAllUserStats(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_ListAllUserStats_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).ListAllUserStats(ctx, req.(*ListAllUserStatsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _UserService_GetUserStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetUserStatsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).GetUserStats(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_GetUserStats_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).GetUserStats(ctx, req.(*GetUserStatsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _UserService_GetUserSetting_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetUserSettingRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).GetUserSetting(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_GetUserSetting_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).GetUserSetting(ctx, req.(*GetUserSettingRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _UserService_UpdateUserSetting_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateUserSettingRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).UpdateUserSetting(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_UpdateUserSetting_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).UpdateUserSetting(ctx, req.(*UpdateUserSettingRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _UserService_ListUserAccessTokens_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListUserAccessTokensRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).ListUserAccessTokens(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_ListUserAccessTokens_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).ListUserAccessTokens(ctx, req.(*ListUserAccessTokensRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _UserService_CreateUserAccessToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateUserAccessTokenRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).CreateUserAccessToken(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_CreateUserAccessToken_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).CreateUserAccessToken(ctx, req.(*CreateUserAccessTokenRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _UserService_DeleteUserAccessToken_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteUserAccessTokenRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).DeleteUserAccessToken(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_DeleteUserAccessToken_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).DeleteUserAccessToken(ctx, req.(*DeleteUserAccessTokenRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _UserService_ListUserSessions_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListUserSessionsRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).ListUserSessions(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_ListUserSessions_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).ListUserSessions(ctx, req.(*ListUserSessionsRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _UserService_RevokeUserSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(RevokeUserSessionRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(UserServiceServer).RevokeUserSession(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: UserService_RevokeUserSession_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(UserServiceServer).RevokeUserSession(ctx, req.(*RevokeUserSessionRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// UserService_ServiceDesc is the grpc.ServiceDesc for UserService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var UserService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "memos.api.v1.UserService", + HandlerType: (*UserServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ListUsers", + Handler: _UserService_ListUsers_Handler, + }, + { + MethodName: "GetUser", + Handler: _UserService_GetUser_Handler, + }, + { + MethodName: "CreateUser", + Handler: _UserService_CreateUser_Handler, + }, + { + MethodName: "UpdateUser", + Handler: _UserService_UpdateUser_Handler, + }, + { + MethodName: "DeleteUser", + Handler: _UserService_DeleteUser_Handler, + }, + { + MethodName: "SearchUsers", + Handler: _UserService_SearchUsers_Handler, + }, + { + MethodName: "GetUserAvatar", + Handler: _UserService_GetUserAvatar_Handler, + }, + { + MethodName: "ListAllUserStats", + Handler: _UserService_ListAllUserStats_Handler, + }, + { + MethodName: "GetUserStats", + Handler: _UserService_GetUserStats_Handler, + }, + { + MethodName: "GetUserSetting", + Handler: _UserService_GetUserSetting_Handler, + }, + { + MethodName: "UpdateUserSetting", + Handler: _UserService_UpdateUserSetting_Handler, + }, + { + MethodName: "ListUserAccessTokens", + Handler: _UserService_ListUserAccessTokens_Handler, + }, + { + MethodName: "CreateUserAccessToken", + Handler: _UserService_CreateUserAccessToken_Handler, + }, + { + MethodName: "DeleteUserAccessToken", + Handler: _UserService_DeleteUserAccessToken_Handler, + }, + { + MethodName: "ListUserSessions", + Handler: _UserService_ListUserSessions_Handler, + }, + { + MethodName: "RevokeUserSession", + Handler: _UserService_RevokeUserSession_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "api/v1/user_service.proto", +} diff --git a/proto/gen/api/v1/webhook_service.pb.go b/proto/gen/api/v1/webhook_service.pb.go new file mode 100644 index 0000000..16c3869 --- /dev/null +++ b/proto/gen/api/v1/webhook_service.pb.go @@ -0,0 +1,497 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: api/v1/webhook_service.proto + +package apiv1 + +import ( + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + fieldmaskpb "google.golang.org/protobuf/types/known/fieldmaskpb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type Webhook struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The resource name of the webhook. + // Format: users/{user}/webhooks/{webhook} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // The display name of the webhook. + DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + // The target URL for the webhook. + Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *Webhook) Reset() { + *x = Webhook{} + mi := &file_api_v1_webhook_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *Webhook) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Webhook) ProtoMessage() {} + +func (x *Webhook) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_webhook_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Webhook.ProtoReflect.Descriptor instead. +func (*Webhook) Descriptor() ([]byte, []int) { + return file_api_v1_webhook_service_proto_rawDescGZIP(), []int{0} +} + +func (x *Webhook) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *Webhook) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + +func (x *Webhook) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +type ListWebhooksRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The parent resource where webhooks are listed. + // Format: users/{user} + Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListWebhooksRequest) Reset() { + *x = ListWebhooksRequest{} + mi := &file_api_v1_webhook_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListWebhooksRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListWebhooksRequest) ProtoMessage() {} + +func (x *ListWebhooksRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_webhook_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListWebhooksRequest.ProtoReflect.Descriptor instead. +func (*ListWebhooksRequest) Descriptor() ([]byte, []int) { + return file_api_v1_webhook_service_proto_rawDescGZIP(), []int{1} +} + +func (x *ListWebhooksRequest) GetParent() string { + if x != nil { + return x.Parent + } + return "" +} + +type ListWebhooksResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The list of webhooks. + Webhooks []*Webhook `protobuf:"bytes,1,rep,name=webhooks,proto3" json:"webhooks,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListWebhooksResponse) Reset() { + *x = ListWebhooksResponse{} + mi := &file_api_v1_webhook_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListWebhooksResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListWebhooksResponse) ProtoMessage() {} + +func (x *ListWebhooksResponse) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_webhook_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListWebhooksResponse.ProtoReflect.Descriptor instead. +func (*ListWebhooksResponse) Descriptor() ([]byte, []int) { + return file_api_v1_webhook_service_proto_rawDescGZIP(), []int{2} +} + +func (x *ListWebhooksResponse) GetWebhooks() []*Webhook { + if x != nil { + return x.Webhooks + } + return nil +} + +type GetWebhookRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The resource name of the webhook to retrieve. + // Format: users/{user}/webhooks/{webhook} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetWebhookRequest) Reset() { + *x = GetWebhookRequest{} + mi := &file_api_v1_webhook_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetWebhookRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetWebhookRequest) ProtoMessage() {} + +func (x *GetWebhookRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_webhook_service_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetWebhookRequest.ProtoReflect.Descriptor instead. +func (*GetWebhookRequest) Descriptor() ([]byte, []int) { + return file_api_v1_webhook_service_proto_rawDescGZIP(), []int{3} +} + +func (x *GetWebhookRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +type CreateWebhookRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The parent resource where this webhook will be created. + // Format: users/{user} + Parent string `protobuf:"bytes,1,opt,name=parent,proto3" json:"parent,omitempty"` + // Required. The webhook to create. + Webhook *Webhook `protobuf:"bytes,2,opt,name=webhook,proto3" json:"webhook,omitempty"` + // Optional. If set, validate the request, but do not actually create the webhook. + ValidateOnly bool `protobuf:"varint,3,opt,name=validate_only,json=validateOnly,proto3" json:"validate_only,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateWebhookRequest) Reset() { + *x = CreateWebhookRequest{} + mi := &file_api_v1_webhook_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateWebhookRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateWebhookRequest) ProtoMessage() {} + +func (x *CreateWebhookRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_webhook_service_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateWebhookRequest.ProtoReflect.Descriptor instead. +func (*CreateWebhookRequest) Descriptor() ([]byte, []int) { + return file_api_v1_webhook_service_proto_rawDescGZIP(), []int{4} +} + +func (x *CreateWebhookRequest) GetParent() string { + if x != nil { + return x.Parent + } + return "" +} + +func (x *CreateWebhookRequest) GetWebhook() *Webhook { + if x != nil { + return x.Webhook + } + return nil +} + +func (x *CreateWebhookRequest) GetValidateOnly() bool { + if x != nil { + return x.ValidateOnly + } + return false +} + +type UpdateWebhookRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The webhook resource which replaces the resource on the server. + Webhook *Webhook `protobuf:"bytes,1,opt,name=webhook,proto3" json:"webhook,omitempty"` + // Optional. The list of fields to update. + UpdateMask *fieldmaskpb.FieldMask `protobuf:"bytes,2,opt,name=update_mask,json=updateMask,proto3" json:"update_mask,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateWebhookRequest) Reset() { + *x = UpdateWebhookRequest{} + mi := &file_api_v1_webhook_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateWebhookRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateWebhookRequest) ProtoMessage() {} + +func (x *UpdateWebhookRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_webhook_service_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateWebhookRequest.ProtoReflect.Descriptor instead. +func (*UpdateWebhookRequest) Descriptor() ([]byte, []int) { + return file_api_v1_webhook_service_proto_rawDescGZIP(), []int{5} +} + +func (x *UpdateWebhookRequest) GetWebhook() *Webhook { + if x != nil { + return x.Webhook + } + return nil +} + +func (x *UpdateWebhookRequest) GetUpdateMask() *fieldmaskpb.FieldMask { + if x != nil { + return x.UpdateMask + } + return nil +} + +type DeleteWebhookRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Required. The resource name of the webhook to delete. + // Format: users/{user}/webhooks/{webhook} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteWebhookRequest) Reset() { + *x = DeleteWebhookRequest{} + mi := &file_api_v1_webhook_service_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteWebhookRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteWebhookRequest) ProtoMessage() {} + +func (x *DeleteWebhookRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_webhook_service_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteWebhookRequest.ProtoReflect.Descriptor instead. +func (*DeleteWebhookRequest) Descriptor() ([]byte, []int) { + return file_api_v1_webhook_service_proto_rawDescGZIP(), []int{6} +} + +func (x *DeleteWebhookRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +var File_api_v1_webhook_service_proto protoreflect.FileDescriptor + +const file_api_v1_webhook_service_proto_rawDesc = "" + + "\n" + + "\x1capi/v1/webhook_service.proto\x12\fmemos.api.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a\x1bgoogle/protobuf/empty.proto\x1a google/protobuf/field_mask.proto\"\xb0\x01\n" + + "\aWebhook\x12\x17\n" + + "\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12&\n" + + "\fdisplay_name\x18\x02 \x01(\tB\x03\xe0A\x02R\vdisplayName\x12\x15\n" + + "\x03url\x18\x03 \x01(\tB\x03\xe0A\x02R\x03url:M\xeaAJ\n" + + "\x14memos.api.v1/Webhook\x12\x1fusers/{user}/webhooks/{webhook}*\bwebhooks2\awebhook\"K\n" + + "\x13ListWebhooksRequest\x124\n" + + "\x06parent\x18\x01 \x01(\tB\x1c\xe0A\x02\xfaA\x16\x12\x14memos.api.v1/WebhookR\x06parent\"I\n" + + "\x14ListWebhooksResponse\x121\n" + + "\bwebhooks\x18\x01 \x03(\v2\x15.memos.api.v1.WebhookR\bwebhooks\"E\n" + + "\x11GetWebhookRequest\x120\n" + + "\x04name\x18\x01 \x01(\tB\x1c\xe0A\x02\xfaA\x16\n" + + "\x14memos.api.v1/WebhookR\x04name\"\xac\x01\n" + + "\x14CreateWebhookRequest\x124\n" + + "\x06parent\x18\x01 \x01(\tB\x1c\xe0A\x02\xfaA\x16\x12\x14memos.api.v1/WebhookR\x06parent\x124\n" + + "\awebhook\x18\x02 \x01(\v2\x15.memos.api.v1.WebhookB\x03\xe0A\x02R\awebhook\x12(\n" + + "\rvalidate_only\x18\x03 \x01(\bB\x03\xe0A\x01R\fvalidateOnly\"\x8e\x01\n" + + "\x14UpdateWebhookRequest\x124\n" + + "\awebhook\x18\x01 \x01(\v2\x15.memos.api.v1.WebhookB\x03\xe0A\x02R\awebhook\x12@\n" + + "\vupdate_mask\x18\x02 \x01(\v2\x1a.google.protobuf.FieldMaskB\x03\xe0A\x01R\n" + + "updateMask\"H\n" + + "\x14DeleteWebhookRequest\x120\n" + + "\x04name\x18\x01 \x01(\tB\x1c\xe0A\x02\xfaA\x16\n" + + "\x14memos.api.v1/WebhookR\x04name2\xc4\x05\n" + + "\x0eWebhookService\x12\x89\x01\n" + + "\fListWebhooks\x12!.memos.api.v1.ListWebhooksRequest\x1a\".memos.api.v1.ListWebhooksResponse\"2\xdaA\x06parent\x82\xd3\xe4\x93\x02#\x12!/api/v1/{parent=users/*}/webhooks\x12v\n" + + "\n" + + "GetWebhook\x12\x1f.memos.api.v1.GetWebhookRequest\x1a\x15.memos.api.v1.Webhook\"0\xdaA\x04name\x82\xd3\xe4\x93\x02#\x12!/api/v1/{name=users/*/webhooks/*}\x12\x8f\x01\n" + + "\rCreateWebhook\x12\".memos.api.v1.CreateWebhookRequest\x1a\x15.memos.api.v1.Webhook\"C\xdaA\x0eparent,webhook\x82\xd3\xe4\x93\x02,:\awebhook\"!/api/v1/{parent=users/*}/webhooks\x12\x9c\x01\n" + + "\rUpdateWebhook\x12\".memos.api.v1.UpdateWebhookRequest\x1a\x15.memos.api.v1.Webhook\"P\xdaA\x13webhook,update_mask\x82\xd3\xe4\x93\x024:\awebhook2)/api/v1/{webhook.name=users/*/webhooks/*}\x12}\n" + + "\rDeleteWebhook\x12\".memos.api.v1.DeleteWebhookRequest\x1a\x16.google.protobuf.Empty\"0\xdaA\x04name\x82\xd3\xe4\x93\x02#*!/api/v1/{name=users/*/webhooks/*}B\xab\x01\n" + + "\x10com.memos.api.v1B\x13WebhookServiceProtoP\x01Z0github.com/usememos/memos/proto/gen/api/v1;apiv1\xa2\x02\x03MAX\xaa\x02\fMemos.Api.V1\xca\x02\fMemos\\Api\\V1\xe2\x02\x18Memos\\Api\\V1\\GPBMetadata\xea\x02\x0eMemos::Api::V1b\x06proto3" + +var ( + file_api_v1_webhook_service_proto_rawDescOnce sync.Once + file_api_v1_webhook_service_proto_rawDescData []byte +) + +func file_api_v1_webhook_service_proto_rawDescGZIP() []byte { + file_api_v1_webhook_service_proto_rawDescOnce.Do(func() { + file_api_v1_webhook_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_v1_webhook_service_proto_rawDesc), len(file_api_v1_webhook_service_proto_rawDesc))) + }) + return file_api_v1_webhook_service_proto_rawDescData +} + +var file_api_v1_webhook_service_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_api_v1_webhook_service_proto_goTypes = []any{ + (*Webhook)(nil), // 0: memos.api.v1.Webhook + (*ListWebhooksRequest)(nil), // 1: memos.api.v1.ListWebhooksRequest + (*ListWebhooksResponse)(nil), // 2: memos.api.v1.ListWebhooksResponse + (*GetWebhookRequest)(nil), // 3: memos.api.v1.GetWebhookRequest + (*CreateWebhookRequest)(nil), // 4: memos.api.v1.CreateWebhookRequest + (*UpdateWebhookRequest)(nil), // 5: memos.api.v1.UpdateWebhookRequest + (*DeleteWebhookRequest)(nil), // 6: memos.api.v1.DeleteWebhookRequest + (*fieldmaskpb.FieldMask)(nil), // 7: google.protobuf.FieldMask + (*emptypb.Empty)(nil), // 8: google.protobuf.Empty +} +var file_api_v1_webhook_service_proto_depIdxs = []int32{ + 0, // 0: memos.api.v1.ListWebhooksResponse.webhooks:type_name -> memos.api.v1.Webhook + 0, // 1: memos.api.v1.CreateWebhookRequest.webhook:type_name -> memos.api.v1.Webhook + 0, // 2: memos.api.v1.UpdateWebhookRequest.webhook:type_name -> memos.api.v1.Webhook + 7, // 3: memos.api.v1.UpdateWebhookRequest.update_mask:type_name -> google.protobuf.FieldMask + 1, // 4: memos.api.v1.WebhookService.ListWebhooks:input_type -> memos.api.v1.ListWebhooksRequest + 3, // 5: memos.api.v1.WebhookService.GetWebhook:input_type -> memos.api.v1.GetWebhookRequest + 4, // 6: memos.api.v1.WebhookService.CreateWebhook:input_type -> memos.api.v1.CreateWebhookRequest + 5, // 7: memos.api.v1.WebhookService.UpdateWebhook:input_type -> memos.api.v1.UpdateWebhookRequest + 6, // 8: memos.api.v1.WebhookService.DeleteWebhook:input_type -> memos.api.v1.DeleteWebhookRequest + 2, // 9: memos.api.v1.WebhookService.ListWebhooks:output_type -> memos.api.v1.ListWebhooksResponse + 0, // 10: memos.api.v1.WebhookService.GetWebhook:output_type -> memos.api.v1.Webhook + 0, // 11: memos.api.v1.WebhookService.CreateWebhook:output_type -> memos.api.v1.Webhook + 0, // 12: memos.api.v1.WebhookService.UpdateWebhook:output_type -> memos.api.v1.Webhook + 8, // 13: memos.api.v1.WebhookService.DeleteWebhook:output_type -> google.protobuf.Empty + 9, // [9:14] is the sub-list for method output_type + 4, // [4:9] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_api_v1_webhook_service_proto_init() } +func file_api_v1_webhook_service_proto_init() { + if File_api_v1_webhook_service_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_webhook_service_proto_rawDesc), len(file_api_v1_webhook_service_proto_rawDesc)), + NumEnums: 0, + NumMessages: 7, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_api_v1_webhook_service_proto_goTypes, + DependencyIndexes: file_api_v1_webhook_service_proto_depIdxs, + MessageInfos: file_api_v1_webhook_service_proto_msgTypes, + }.Build() + File_api_v1_webhook_service_proto = out.File + file_api_v1_webhook_service_proto_goTypes = nil + file_api_v1_webhook_service_proto_depIdxs = nil +} diff --git a/proto/gen/api/v1/webhook_service.pb.gw.go b/proto/gen/api/v1/webhook_service.pb.gw.go new file mode 100644 index 0000000..e8f938e --- /dev/null +++ b/proto/gen/api/v1/webhook_service.pb.gw.go @@ -0,0 +1,543 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: api/v1/webhook_service.proto + +/* +Package apiv1 is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package apiv1 + +import ( + "context" + "errors" + "io" + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// Suppress "imported and not used" errors +var ( + _ codes.Code + _ io.Reader + _ status.Status + _ = errors.New + _ = runtime.String + _ = utilities.NewDoubleArray + _ = metadata.Join +) + +func request_WebhookService_ListWebhooks_0(ctx context.Context, marshaler runtime.Marshaler, client WebhookServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListWebhooksRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } + msg, err := client.ListWebhooks(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_WebhookService_ListWebhooks_0(ctx context.Context, marshaler runtime.Marshaler, server WebhookServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq ListWebhooksRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } + msg, err := server.ListWebhooks(ctx, &protoReq) + return msg, metadata, err +} + +func request_WebhookService_GetWebhook_0(ctx context.Context, marshaler runtime.Marshaler, client WebhookServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetWebhookRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := client.GetWebhook(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_WebhookService_GetWebhook_0(ctx context.Context, marshaler runtime.Marshaler, server WebhookServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetWebhookRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := server.GetWebhook(ctx, &protoReq) + return msg, metadata, err +} + +var filter_WebhookService_CreateWebhook_0 = &utilities.DoubleArray{Encoding: map[string]int{"webhook": 0, "parent": 1}, Base: []int{1, 1, 2, 0, 0}, Check: []int{0, 1, 1, 2, 3}} + +func request_WebhookService_CreateWebhook_0(ctx context.Context, marshaler runtime.Marshaler, client WebhookServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateWebhookRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Webhook); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_WebhookService_CreateWebhook_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.CreateWebhook(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_WebhookService_CreateWebhook_0(ctx context.Context, marshaler runtime.Marshaler, server WebhookServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq CreateWebhookRequest + metadata runtime.ServerMetadata + err error + ) + if err := marshaler.NewDecoder(req.Body).Decode(&protoReq.Webhook); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + val, ok := pathParams["parent"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "parent") + } + protoReq.Parent, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "parent", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_WebhookService_CreateWebhook_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.CreateWebhook(ctx, &protoReq) + return msg, metadata, err +} + +var filter_WebhookService_UpdateWebhook_0 = &utilities.DoubleArray{Encoding: map[string]int{"webhook": 0, "name": 1}, Base: []int{1, 2, 1, 0, 0}, Check: []int{0, 1, 2, 3, 2}} + +func request_WebhookService_UpdateWebhook_0(ctx context.Context, marshaler runtime.Marshaler, client WebhookServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateWebhookRequest + metadata runtime.ServerMetadata + err error + ) + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Webhook); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 { + if fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Webhook); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } else { + protoReq.UpdateMask = fieldMask + } + } + val, ok := pathParams["webhook.name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "webhook.name") + } + err = runtime.PopulateFieldFromPath(&protoReq, "webhook.name", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "webhook.name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_WebhookService_UpdateWebhook_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.UpdateWebhook(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_WebhookService_UpdateWebhook_0(ctx context.Context, marshaler runtime.Marshaler, server WebhookServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateWebhookRequest + metadata runtime.ServerMetadata + err error + ) + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Webhook); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 { + if fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Webhook); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } else { + protoReq.UpdateMask = fieldMask + } + } + val, ok := pathParams["webhook.name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "webhook.name") + } + err = runtime.PopulateFieldFromPath(&protoReq, "webhook.name", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "webhook.name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_WebhookService_UpdateWebhook_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.UpdateWebhook(ctx, &protoReq) + return msg, metadata, err +} + +func request_WebhookService_DeleteWebhook_0(ctx context.Context, marshaler runtime.Marshaler, client WebhookServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteWebhookRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := client.DeleteWebhook(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_WebhookService_DeleteWebhook_0(ctx context.Context, marshaler runtime.Marshaler, server WebhookServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq DeleteWebhookRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := server.DeleteWebhook(ctx, &protoReq) + return msg, metadata, err +} + +// RegisterWebhookServiceHandlerServer registers the http handlers for service WebhookService to "mux". +// UnaryRPC :call WebhookServiceServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterWebhookServiceHandlerFromEndpoint instead. +// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call. +func RegisterWebhookServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server WebhookServiceServer) error { + mux.Handle(http.MethodGet, pattern_WebhookService_ListWebhooks_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.WebhookService/ListWebhooks", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/webhooks")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_WebhookService_ListWebhooks_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_WebhookService_ListWebhooks_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_WebhookService_GetWebhook_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.WebhookService/GetWebhook", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/webhooks/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_WebhookService_GetWebhook_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_WebhookService_GetWebhook_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_WebhookService_CreateWebhook_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.WebhookService/CreateWebhook", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/webhooks")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_WebhookService_CreateWebhook_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_WebhookService_CreateWebhook_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPatch, pattern_WebhookService_UpdateWebhook_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.WebhookService/UpdateWebhook", runtime.WithHTTPPathPattern("/api/v1/{webhook.name=users/*/webhooks/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_WebhookService_UpdateWebhook_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_WebhookService_UpdateWebhook_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_WebhookService_DeleteWebhook_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.WebhookService/DeleteWebhook", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/webhooks/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_WebhookService_DeleteWebhook_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_WebhookService_DeleteWebhook_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + + return nil +} + +// RegisterWebhookServiceHandlerFromEndpoint is same as RegisterWebhookServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterWebhookServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.NewClient(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + return RegisterWebhookServiceHandler(ctx, mux, conn) +} + +// RegisterWebhookServiceHandler registers the http handlers for service WebhookService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterWebhookServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterWebhookServiceHandlerClient(ctx, mux, NewWebhookServiceClient(conn)) +} + +// RegisterWebhookServiceHandlerClient registers the http handlers for service WebhookService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "WebhookServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "WebhookServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "WebhookServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares. +func RegisterWebhookServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client WebhookServiceClient) error { + mux.Handle(http.MethodGet, pattern_WebhookService_ListWebhooks_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.WebhookService/ListWebhooks", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/webhooks")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_WebhookService_ListWebhooks_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_WebhookService_ListWebhooks_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_WebhookService_GetWebhook_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.WebhookService/GetWebhook", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/webhooks/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_WebhookService_GetWebhook_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_WebhookService_GetWebhook_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPost, pattern_WebhookService_CreateWebhook_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.WebhookService/CreateWebhook", runtime.WithHTTPPathPattern("/api/v1/{parent=users/*}/webhooks")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_WebhookService_CreateWebhook_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_WebhookService_CreateWebhook_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPatch, pattern_WebhookService_UpdateWebhook_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.WebhookService/UpdateWebhook", runtime.WithHTTPPathPattern("/api/v1/{webhook.name=users/*/webhooks/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_WebhookService_UpdateWebhook_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_WebhookService_UpdateWebhook_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodDelete, pattern_WebhookService_DeleteWebhook_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.WebhookService/DeleteWebhook", runtime.WithHTTPPathPattern("/api/v1/{name=users/*/webhooks/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_WebhookService_DeleteWebhook_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_WebhookService_DeleteWebhook_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + return nil +} + +var ( + pattern_WebhookService_ListWebhooks_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "webhooks"}, "")) + pattern_WebhookService_GetWebhook_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "webhooks", "name"}, "")) + pattern_WebhookService_CreateWebhook_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 4, 2, 5, 3, 2, 4}, []string{"api", "v1", "users", "parent", "webhooks"}, "")) + pattern_WebhookService_UpdateWebhook_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "webhooks", "webhook.name"}, "")) + pattern_WebhookService_DeleteWebhook_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 1, 0, 2, 3, 1, 0, 4, 4, 5, 4}, []string{"api", "v1", "users", "webhooks", "name"}, "")) +) + +var ( + forward_WebhookService_ListWebhooks_0 = runtime.ForwardResponseMessage + forward_WebhookService_GetWebhook_0 = runtime.ForwardResponseMessage + forward_WebhookService_CreateWebhook_0 = runtime.ForwardResponseMessage + forward_WebhookService_UpdateWebhook_0 = runtime.ForwardResponseMessage + forward_WebhookService_DeleteWebhook_0 = runtime.ForwardResponseMessage +) diff --git a/proto/gen/api/v1/webhook_service_grpc.pb.go b/proto/gen/api/v1/webhook_service_grpc.pb.go new file mode 100644 index 0000000..e81076b --- /dev/null +++ b/proto/gen/api/v1/webhook_service_grpc.pb.go @@ -0,0 +1,284 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc (unknown) +// source: api/v1/webhook_service.proto + +package apiv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + emptypb "google.golang.org/protobuf/types/known/emptypb" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + WebhookService_ListWebhooks_FullMethodName = "/memos.api.v1.WebhookService/ListWebhooks" + WebhookService_GetWebhook_FullMethodName = "/memos.api.v1.WebhookService/GetWebhook" + WebhookService_CreateWebhook_FullMethodName = "/memos.api.v1.WebhookService/CreateWebhook" + WebhookService_UpdateWebhook_FullMethodName = "/memos.api.v1.WebhookService/UpdateWebhook" + WebhookService_DeleteWebhook_FullMethodName = "/memos.api.v1.WebhookService/DeleteWebhook" +) + +// WebhookServiceClient is the client API for WebhookService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type WebhookServiceClient interface { + // ListWebhooks returns a list of webhooks for a user. + ListWebhooks(ctx context.Context, in *ListWebhooksRequest, opts ...grpc.CallOption) (*ListWebhooksResponse, error) + // GetWebhook gets a webhook by name. + GetWebhook(ctx context.Context, in *GetWebhookRequest, opts ...grpc.CallOption) (*Webhook, error) + // CreateWebhook creates a new webhook for a user. + CreateWebhook(ctx context.Context, in *CreateWebhookRequest, opts ...grpc.CallOption) (*Webhook, error) + // UpdateWebhook updates a webhook for a user. + UpdateWebhook(ctx context.Context, in *UpdateWebhookRequest, opts ...grpc.CallOption) (*Webhook, error) + // DeleteWebhook deletes a webhook for a user. + DeleteWebhook(ctx context.Context, in *DeleteWebhookRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) +} + +type webhookServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewWebhookServiceClient(cc grpc.ClientConnInterface) WebhookServiceClient { + return &webhookServiceClient{cc} +} + +func (c *webhookServiceClient) ListWebhooks(ctx context.Context, in *ListWebhooksRequest, opts ...grpc.CallOption) (*ListWebhooksResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListWebhooksResponse) + err := c.cc.Invoke(ctx, WebhookService_ListWebhooks_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *webhookServiceClient) GetWebhook(ctx context.Context, in *GetWebhookRequest, opts ...grpc.CallOption) (*Webhook, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Webhook) + err := c.cc.Invoke(ctx, WebhookService_GetWebhook_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *webhookServiceClient) CreateWebhook(ctx context.Context, in *CreateWebhookRequest, opts ...grpc.CallOption) (*Webhook, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Webhook) + err := c.cc.Invoke(ctx, WebhookService_CreateWebhook_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *webhookServiceClient) UpdateWebhook(ctx context.Context, in *UpdateWebhookRequest, opts ...grpc.CallOption) (*Webhook, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(Webhook) + err := c.cc.Invoke(ctx, WebhookService_UpdateWebhook_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *webhookServiceClient) DeleteWebhook(ctx context.Context, in *DeleteWebhookRequest, opts ...grpc.CallOption) (*emptypb.Empty, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(emptypb.Empty) + err := c.cc.Invoke(ctx, WebhookService_DeleteWebhook_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// WebhookServiceServer is the server API for WebhookService service. +// All implementations must embed UnimplementedWebhookServiceServer +// for forward compatibility. +type WebhookServiceServer interface { + // ListWebhooks returns a list of webhooks for a user. + ListWebhooks(context.Context, *ListWebhooksRequest) (*ListWebhooksResponse, error) + // GetWebhook gets a webhook by name. + GetWebhook(context.Context, *GetWebhookRequest) (*Webhook, error) + // CreateWebhook creates a new webhook for a user. + CreateWebhook(context.Context, *CreateWebhookRequest) (*Webhook, error) + // UpdateWebhook updates a webhook for a user. + UpdateWebhook(context.Context, *UpdateWebhookRequest) (*Webhook, error) + // DeleteWebhook deletes a webhook for a user. + DeleteWebhook(context.Context, *DeleteWebhookRequest) (*emptypb.Empty, error) + mustEmbedUnimplementedWebhookServiceServer() +} + +// UnimplementedWebhookServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedWebhookServiceServer struct{} + +func (UnimplementedWebhookServiceServer) ListWebhooks(context.Context, *ListWebhooksRequest) (*ListWebhooksResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method ListWebhooks not implemented") +} +func (UnimplementedWebhookServiceServer) GetWebhook(context.Context, *GetWebhookRequest) (*Webhook, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetWebhook not implemented") +} +func (UnimplementedWebhookServiceServer) CreateWebhook(context.Context, *CreateWebhookRequest) (*Webhook, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateWebhook not implemented") +} +func (UnimplementedWebhookServiceServer) UpdateWebhook(context.Context, *UpdateWebhookRequest) (*Webhook, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateWebhook not implemented") +} +func (UnimplementedWebhookServiceServer) DeleteWebhook(context.Context, *DeleteWebhookRequest) (*emptypb.Empty, error) { + return nil, status.Errorf(codes.Unimplemented, "method DeleteWebhook not implemented") +} +func (UnimplementedWebhookServiceServer) mustEmbedUnimplementedWebhookServiceServer() {} +func (UnimplementedWebhookServiceServer) testEmbeddedByValue() {} + +// UnsafeWebhookServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to WebhookServiceServer will +// result in compilation errors. +type UnsafeWebhookServiceServer interface { + mustEmbedUnimplementedWebhookServiceServer() +} + +func RegisterWebhookServiceServer(s grpc.ServiceRegistrar, srv WebhookServiceServer) { + // If the following call pancis, it indicates UnimplementedWebhookServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&WebhookService_ServiceDesc, srv) +} + +func _WebhookService_ListWebhooks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListWebhooksRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WebhookServiceServer).ListWebhooks(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: WebhookService_ListWebhooks_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WebhookServiceServer).ListWebhooks(ctx, req.(*ListWebhooksRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _WebhookService_GetWebhook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetWebhookRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WebhookServiceServer).GetWebhook(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: WebhookService_GetWebhook_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WebhookServiceServer).GetWebhook(ctx, req.(*GetWebhookRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _WebhookService_CreateWebhook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateWebhookRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WebhookServiceServer).CreateWebhook(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: WebhookService_CreateWebhook_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WebhookServiceServer).CreateWebhook(ctx, req.(*CreateWebhookRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _WebhookService_UpdateWebhook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateWebhookRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WebhookServiceServer).UpdateWebhook(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: WebhookService_UpdateWebhook_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WebhookServiceServer).UpdateWebhook(ctx, req.(*UpdateWebhookRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _WebhookService_DeleteWebhook_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteWebhookRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WebhookServiceServer).DeleteWebhook(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: WebhookService_DeleteWebhook_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WebhookServiceServer).DeleteWebhook(ctx, req.(*DeleteWebhookRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// WebhookService_ServiceDesc is the grpc.ServiceDesc for WebhookService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var WebhookService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "memos.api.v1.WebhookService", + HandlerType: (*WebhookServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ListWebhooks", + Handler: _WebhookService_ListWebhooks_Handler, + }, + { + MethodName: "GetWebhook", + Handler: _WebhookService_GetWebhook_Handler, + }, + { + MethodName: "CreateWebhook", + Handler: _WebhookService_CreateWebhook_Handler, + }, + { + MethodName: "UpdateWebhook", + Handler: _WebhookService_UpdateWebhook_Handler, + }, + { + MethodName: "DeleteWebhook", + Handler: _WebhookService_DeleteWebhook_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "api/v1/webhook_service.proto", +} diff --git a/proto/gen/api/v1/workspace_service.pb.go b/proto/gen/api/v1/workspace_service.pb.go new file mode 100644 index 0000000..96c5084 --- /dev/null +++ b/proto/gen/api/v1/workspace_service.pb.go @@ -0,0 +1,1039 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: api/v1/workspace_service.proto + +package apiv1 + +import ( + _ "google.golang.org/genproto/googleapis/api/annotations" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + fieldmaskpb "google.golang.org/protobuf/types/known/fieldmaskpb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type WorkspaceStorageSetting_StorageType int32 + +const ( + WorkspaceStorageSetting_STORAGE_TYPE_UNSPECIFIED WorkspaceStorageSetting_StorageType = 0 + // DATABASE is the database storage type. + WorkspaceStorageSetting_DATABASE WorkspaceStorageSetting_StorageType = 1 + // LOCAL is the local storage type. + WorkspaceStorageSetting_LOCAL WorkspaceStorageSetting_StorageType = 2 + // S3 is the S3 storage type. + WorkspaceStorageSetting_S3 WorkspaceStorageSetting_StorageType = 3 +) + +// Enum value maps for WorkspaceStorageSetting_StorageType. +var ( + WorkspaceStorageSetting_StorageType_name = map[int32]string{ + 0: "STORAGE_TYPE_UNSPECIFIED", + 1: "DATABASE", + 2: "LOCAL", + 3: "S3", + } + WorkspaceStorageSetting_StorageType_value = map[string]int32{ + "STORAGE_TYPE_UNSPECIFIED": 0, + "DATABASE": 1, + "LOCAL": 2, + "S3": 3, + } +) + +func (x WorkspaceStorageSetting_StorageType) Enum() *WorkspaceStorageSetting_StorageType { + p := new(WorkspaceStorageSetting_StorageType) + *p = x + return p +} + +func (x WorkspaceStorageSetting_StorageType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (WorkspaceStorageSetting_StorageType) Descriptor() protoreflect.EnumDescriptor { + return file_api_v1_workspace_service_proto_enumTypes[0].Descriptor() +} + +func (WorkspaceStorageSetting_StorageType) Type() protoreflect.EnumType { + return &file_api_v1_workspace_service_proto_enumTypes[0] +} + +func (x WorkspaceStorageSetting_StorageType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use WorkspaceStorageSetting_StorageType.Descriptor instead. +func (WorkspaceStorageSetting_StorageType) EnumDescriptor() ([]byte, []int) { + return file_api_v1_workspace_service_proto_rawDescGZIP(), []int{5, 0} +} + +// Workspace profile message containing basic workspace information. +type WorkspaceProfile struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The name of instance owner. + // Format: users/{user} + Owner string `protobuf:"bytes,1,opt,name=owner,proto3" json:"owner,omitempty"` + // Version is the current version of instance. + Version string `protobuf:"bytes,2,opt,name=version,proto3" json:"version,omitempty"` + // Mode is the instance mode (e.g. "prod", "dev" or "demo"). + Mode string `protobuf:"bytes,3,opt,name=mode,proto3" json:"mode,omitempty"` + // Instance URL is the URL of the instance. + InstanceUrl string `protobuf:"bytes,6,opt,name=instance_url,json=instanceUrl,proto3" json:"instance_url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkspaceProfile) Reset() { + *x = WorkspaceProfile{} + mi := &file_api_v1_workspace_service_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkspaceProfile) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkspaceProfile) ProtoMessage() {} + +func (x *WorkspaceProfile) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_workspace_service_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkspaceProfile.ProtoReflect.Descriptor instead. +func (*WorkspaceProfile) Descriptor() ([]byte, []int) { + return file_api_v1_workspace_service_proto_rawDescGZIP(), []int{0} +} + +func (x *WorkspaceProfile) GetOwner() string { + if x != nil { + return x.Owner + } + return "" +} + +func (x *WorkspaceProfile) GetVersion() string { + if x != nil { + return x.Version + } + return "" +} + +func (x *WorkspaceProfile) GetMode() string { + if x != nil { + return x.Mode + } + return "" +} + +func (x *WorkspaceProfile) GetInstanceUrl() string { + if x != nil { + return x.InstanceUrl + } + return "" +} + +// Request for workspace profile. +type GetWorkspaceProfileRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetWorkspaceProfileRequest) Reset() { + *x = GetWorkspaceProfileRequest{} + mi := &file_api_v1_workspace_service_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetWorkspaceProfileRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetWorkspaceProfileRequest) ProtoMessage() {} + +func (x *GetWorkspaceProfileRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_workspace_service_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetWorkspaceProfileRequest.ProtoReflect.Descriptor instead. +func (*GetWorkspaceProfileRequest) Descriptor() ([]byte, []int) { + return file_api_v1_workspace_service_proto_rawDescGZIP(), []int{1} +} + +// A workspace setting resource. +type WorkspaceSetting struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The name of the workspace setting. + // Format: workspace/settings/{setting} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Types that are valid to be assigned to Value: + // + // *WorkspaceSetting_GeneralSetting + // *WorkspaceSetting_StorageSetting + // *WorkspaceSetting_MemoRelatedSetting + Value isWorkspaceSetting_Value `protobuf_oneof:"value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkspaceSetting) Reset() { + *x = WorkspaceSetting{} + mi := &file_api_v1_workspace_service_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkspaceSetting) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkspaceSetting) ProtoMessage() {} + +func (x *WorkspaceSetting) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_workspace_service_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkspaceSetting.ProtoReflect.Descriptor instead. +func (*WorkspaceSetting) Descriptor() ([]byte, []int) { + return file_api_v1_workspace_service_proto_rawDescGZIP(), []int{2} +} + +func (x *WorkspaceSetting) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *WorkspaceSetting) GetValue() isWorkspaceSetting_Value { + if x != nil { + return x.Value + } + return nil +} + +func (x *WorkspaceSetting) GetGeneralSetting() *WorkspaceGeneralSetting { + if x != nil { + if x, ok := x.Value.(*WorkspaceSetting_GeneralSetting); ok { + return x.GeneralSetting + } + } + return nil +} + +func (x *WorkspaceSetting) GetStorageSetting() *WorkspaceStorageSetting { + if x != nil { + if x, ok := x.Value.(*WorkspaceSetting_StorageSetting); ok { + return x.StorageSetting + } + } + return nil +} + +func (x *WorkspaceSetting) GetMemoRelatedSetting() *WorkspaceMemoRelatedSetting { + if x != nil { + if x, ok := x.Value.(*WorkspaceSetting_MemoRelatedSetting); ok { + return x.MemoRelatedSetting + } + } + return nil +} + +type isWorkspaceSetting_Value interface { + isWorkspaceSetting_Value() +} + +type WorkspaceSetting_GeneralSetting struct { + GeneralSetting *WorkspaceGeneralSetting `protobuf:"bytes,2,opt,name=general_setting,json=generalSetting,proto3,oneof"` +} + +type WorkspaceSetting_StorageSetting struct { + StorageSetting *WorkspaceStorageSetting `protobuf:"bytes,3,opt,name=storage_setting,json=storageSetting,proto3,oneof"` +} + +type WorkspaceSetting_MemoRelatedSetting struct { + MemoRelatedSetting *WorkspaceMemoRelatedSetting `protobuf:"bytes,4,opt,name=memo_related_setting,json=memoRelatedSetting,proto3,oneof"` +} + +func (*WorkspaceSetting_GeneralSetting) isWorkspaceSetting_Value() {} + +func (*WorkspaceSetting_StorageSetting) isWorkspaceSetting_Value() {} + +func (*WorkspaceSetting_MemoRelatedSetting) isWorkspaceSetting_Value() {} + +type WorkspaceGeneralSetting struct { + state protoimpl.MessageState `protogen:"open.v1"` + // theme is the name of the selected theme. + // This references a CSS file in the web/public/themes/ directory. + Theme string `protobuf:"bytes,1,opt,name=theme,proto3" json:"theme,omitempty"` + // disallow_user_registration disallows user registration. + DisallowUserRegistration bool `protobuf:"varint,2,opt,name=disallow_user_registration,json=disallowUserRegistration,proto3" json:"disallow_user_registration,omitempty"` + // disallow_password_auth disallows password authentication. + DisallowPasswordAuth bool `protobuf:"varint,3,opt,name=disallow_password_auth,json=disallowPasswordAuth,proto3" json:"disallow_password_auth,omitempty"` + // additional_script is the additional script. + AdditionalScript string `protobuf:"bytes,4,opt,name=additional_script,json=additionalScript,proto3" json:"additional_script,omitempty"` + // additional_style is the additional style. + AdditionalStyle string `protobuf:"bytes,5,opt,name=additional_style,json=additionalStyle,proto3" json:"additional_style,omitempty"` + // custom_profile is the custom profile. + CustomProfile *WorkspaceCustomProfile `protobuf:"bytes,6,opt,name=custom_profile,json=customProfile,proto3" json:"custom_profile,omitempty"` + // week_start_day_offset is the week start day offset from Sunday. + // 0: Sunday, 1: Monday, 2: Tuesday, 3: Wednesday, 4: Thursday, 5: Friday, 6: Saturday + // Default is Sunday. + WeekStartDayOffset int32 `protobuf:"varint,7,opt,name=week_start_day_offset,json=weekStartDayOffset,proto3" json:"week_start_day_offset,omitempty"` + // disallow_change_username disallows changing username. + DisallowChangeUsername bool `protobuf:"varint,8,opt,name=disallow_change_username,json=disallowChangeUsername,proto3" json:"disallow_change_username,omitempty"` + // disallow_change_nickname disallows changing nickname. + DisallowChangeNickname bool `protobuf:"varint,9,opt,name=disallow_change_nickname,json=disallowChangeNickname,proto3" json:"disallow_change_nickname,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkspaceGeneralSetting) Reset() { + *x = WorkspaceGeneralSetting{} + mi := &file_api_v1_workspace_service_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkspaceGeneralSetting) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkspaceGeneralSetting) ProtoMessage() {} + +func (x *WorkspaceGeneralSetting) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_workspace_service_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkspaceGeneralSetting.ProtoReflect.Descriptor instead. +func (*WorkspaceGeneralSetting) Descriptor() ([]byte, []int) { + return file_api_v1_workspace_service_proto_rawDescGZIP(), []int{3} +} + +func (x *WorkspaceGeneralSetting) GetTheme() string { + if x != nil { + return x.Theme + } + return "" +} + +func (x *WorkspaceGeneralSetting) GetDisallowUserRegistration() bool { + if x != nil { + return x.DisallowUserRegistration + } + return false +} + +func (x *WorkspaceGeneralSetting) GetDisallowPasswordAuth() bool { + if x != nil { + return x.DisallowPasswordAuth + } + return false +} + +func (x *WorkspaceGeneralSetting) GetAdditionalScript() string { + if x != nil { + return x.AdditionalScript + } + return "" +} + +func (x *WorkspaceGeneralSetting) GetAdditionalStyle() string { + if x != nil { + return x.AdditionalStyle + } + return "" +} + +func (x *WorkspaceGeneralSetting) GetCustomProfile() *WorkspaceCustomProfile { + if x != nil { + return x.CustomProfile + } + return nil +} + +func (x *WorkspaceGeneralSetting) GetWeekStartDayOffset() int32 { + if x != nil { + return x.WeekStartDayOffset + } + return 0 +} + +func (x *WorkspaceGeneralSetting) GetDisallowChangeUsername() bool { + if x != nil { + return x.DisallowChangeUsername + } + return false +} + +func (x *WorkspaceGeneralSetting) GetDisallowChangeNickname() bool { + if x != nil { + return x.DisallowChangeNickname + } + return false +} + +type WorkspaceCustomProfile struct { + state protoimpl.MessageState `protogen:"open.v1"` + Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"` + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + LogoUrl string `protobuf:"bytes,3,opt,name=logo_url,json=logoUrl,proto3" json:"logo_url,omitempty"` + Locale string `protobuf:"bytes,4,opt,name=locale,proto3" json:"locale,omitempty"` + Appearance string `protobuf:"bytes,5,opt,name=appearance,proto3" json:"appearance,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkspaceCustomProfile) Reset() { + *x = WorkspaceCustomProfile{} + mi := &file_api_v1_workspace_service_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkspaceCustomProfile) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkspaceCustomProfile) ProtoMessage() {} + +func (x *WorkspaceCustomProfile) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_workspace_service_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkspaceCustomProfile.ProtoReflect.Descriptor instead. +func (*WorkspaceCustomProfile) Descriptor() ([]byte, []int) { + return file_api_v1_workspace_service_proto_rawDescGZIP(), []int{4} +} + +func (x *WorkspaceCustomProfile) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *WorkspaceCustomProfile) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *WorkspaceCustomProfile) GetLogoUrl() string { + if x != nil { + return x.LogoUrl + } + return "" +} + +func (x *WorkspaceCustomProfile) GetLocale() string { + if x != nil { + return x.Locale + } + return "" +} + +func (x *WorkspaceCustomProfile) GetAppearance() string { + if x != nil { + return x.Appearance + } + return "" +} + +type WorkspaceStorageSetting struct { + state protoimpl.MessageState `protogen:"open.v1"` + // storage_type is the storage type. + StorageType WorkspaceStorageSetting_StorageType `protobuf:"varint,1,opt,name=storage_type,json=storageType,proto3,enum=memos.api.v1.WorkspaceStorageSetting_StorageType" json:"storage_type,omitempty"` + // The template of file path. + // e.g. assets/{timestamp}_{filename} + FilepathTemplate string `protobuf:"bytes,2,opt,name=filepath_template,json=filepathTemplate,proto3" json:"filepath_template,omitempty"` + // The max upload size in megabytes. + UploadSizeLimitMb int64 `protobuf:"varint,3,opt,name=upload_size_limit_mb,json=uploadSizeLimitMb,proto3" json:"upload_size_limit_mb,omitempty"` + // The S3 config. + S3Config *WorkspaceStorageSetting_S3Config `protobuf:"bytes,4,opt,name=s3_config,json=s3Config,proto3" json:"s3_config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkspaceStorageSetting) Reset() { + *x = WorkspaceStorageSetting{} + mi := &file_api_v1_workspace_service_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkspaceStorageSetting) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkspaceStorageSetting) ProtoMessage() {} + +func (x *WorkspaceStorageSetting) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_workspace_service_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkspaceStorageSetting.ProtoReflect.Descriptor instead. +func (*WorkspaceStorageSetting) Descriptor() ([]byte, []int) { + return file_api_v1_workspace_service_proto_rawDescGZIP(), []int{5} +} + +func (x *WorkspaceStorageSetting) GetStorageType() WorkspaceStorageSetting_StorageType { + if x != nil { + return x.StorageType + } + return WorkspaceStorageSetting_STORAGE_TYPE_UNSPECIFIED +} + +func (x *WorkspaceStorageSetting) GetFilepathTemplate() string { + if x != nil { + return x.FilepathTemplate + } + return "" +} + +func (x *WorkspaceStorageSetting) GetUploadSizeLimitMb() int64 { + if x != nil { + return x.UploadSizeLimitMb + } + return 0 +} + +func (x *WorkspaceStorageSetting) GetS3Config() *WorkspaceStorageSetting_S3Config { + if x != nil { + return x.S3Config + } + return nil +} + +type WorkspaceMemoRelatedSetting struct { + state protoimpl.MessageState `protogen:"open.v1"` + // disallow_public_visibility disallows set memo as public visibility. + DisallowPublicVisibility bool `protobuf:"varint,1,opt,name=disallow_public_visibility,json=disallowPublicVisibility,proto3" json:"disallow_public_visibility,omitempty"` + // display_with_update_time orders and displays memo with update time. + DisplayWithUpdateTime bool `protobuf:"varint,2,opt,name=display_with_update_time,json=displayWithUpdateTime,proto3" json:"display_with_update_time,omitempty"` + // content_length_limit is the limit of content length. Unit is byte. + ContentLengthLimit int32 `protobuf:"varint,3,opt,name=content_length_limit,json=contentLengthLimit,proto3" json:"content_length_limit,omitempty"` + // enable_double_click_edit enables editing on double click. + EnableDoubleClickEdit bool `protobuf:"varint,4,opt,name=enable_double_click_edit,json=enableDoubleClickEdit,proto3" json:"enable_double_click_edit,omitempty"` + // enable_link_preview enables links preview. + EnableLinkPreview bool `protobuf:"varint,5,opt,name=enable_link_preview,json=enableLinkPreview,proto3" json:"enable_link_preview,omitempty"` + // enable_comment enables comment. + EnableComment bool `protobuf:"varint,6,opt,name=enable_comment,json=enableComment,proto3" json:"enable_comment,omitempty"` + // reactions is the list of reactions. + Reactions []string `protobuf:"bytes,7,rep,name=reactions,proto3" json:"reactions,omitempty"` + // disable_markdown_shortcuts disallow the registration of markdown shortcuts. + DisableMarkdownShortcuts bool `protobuf:"varint,8,opt,name=disable_markdown_shortcuts,json=disableMarkdownShortcuts,proto3" json:"disable_markdown_shortcuts,omitempty"` + // enable_blur_nsfw_content enables blurring of content marked as not safe for work (NSFW). + EnableBlurNsfwContent bool `protobuf:"varint,9,opt,name=enable_blur_nsfw_content,json=enableBlurNsfwContent,proto3" json:"enable_blur_nsfw_content,omitempty"` + // nsfw_tags is the list of tags that mark content as NSFW for blurring. + NsfwTags []string `protobuf:"bytes,10,rep,name=nsfw_tags,json=nsfwTags,proto3" json:"nsfw_tags,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkspaceMemoRelatedSetting) Reset() { + *x = WorkspaceMemoRelatedSetting{} + mi := &file_api_v1_workspace_service_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkspaceMemoRelatedSetting) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkspaceMemoRelatedSetting) ProtoMessage() {} + +func (x *WorkspaceMemoRelatedSetting) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_workspace_service_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkspaceMemoRelatedSetting.ProtoReflect.Descriptor instead. +func (*WorkspaceMemoRelatedSetting) Descriptor() ([]byte, []int) { + return file_api_v1_workspace_service_proto_rawDescGZIP(), []int{6} +} + +func (x *WorkspaceMemoRelatedSetting) GetDisallowPublicVisibility() bool { + if x != nil { + return x.DisallowPublicVisibility + } + return false +} + +func (x *WorkspaceMemoRelatedSetting) GetDisplayWithUpdateTime() bool { + if x != nil { + return x.DisplayWithUpdateTime + } + return false +} + +func (x *WorkspaceMemoRelatedSetting) GetContentLengthLimit() int32 { + if x != nil { + return x.ContentLengthLimit + } + return 0 +} + +func (x *WorkspaceMemoRelatedSetting) GetEnableDoubleClickEdit() bool { + if x != nil { + return x.EnableDoubleClickEdit + } + return false +} + +func (x *WorkspaceMemoRelatedSetting) GetEnableLinkPreview() bool { + if x != nil { + return x.EnableLinkPreview + } + return false +} + +func (x *WorkspaceMemoRelatedSetting) GetEnableComment() bool { + if x != nil { + return x.EnableComment + } + return false +} + +func (x *WorkspaceMemoRelatedSetting) GetReactions() []string { + if x != nil { + return x.Reactions + } + return nil +} + +func (x *WorkspaceMemoRelatedSetting) GetDisableMarkdownShortcuts() bool { + if x != nil { + return x.DisableMarkdownShortcuts + } + return false +} + +func (x *WorkspaceMemoRelatedSetting) GetEnableBlurNsfwContent() bool { + if x != nil { + return x.EnableBlurNsfwContent + } + return false +} + +func (x *WorkspaceMemoRelatedSetting) GetNsfwTags() []string { + if x != nil { + return x.NsfwTags + } + return nil +} + +// Request message for GetWorkspaceSetting method. +type GetWorkspaceSettingRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The resource name of the workspace setting. + // Format: workspace/settings/{setting} + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetWorkspaceSettingRequest) Reset() { + *x = GetWorkspaceSettingRequest{} + mi := &file_api_v1_workspace_service_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetWorkspaceSettingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetWorkspaceSettingRequest) ProtoMessage() {} + +func (x *GetWorkspaceSettingRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_workspace_service_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetWorkspaceSettingRequest.ProtoReflect.Descriptor instead. +func (*GetWorkspaceSettingRequest) Descriptor() ([]byte, []int) { + return file_api_v1_workspace_service_proto_rawDescGZIP(), []int{7} +} + +func (x *GetWorkspaceSettingRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +// Request message for UpdateWorkspaceSetting method. +type UpdateWorkspaceSettingRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The workspace setting resource which replaces the resource on the server. + Setting *WorkspaceSetting `protobuf:"bytes,1,opt,name=setting,proto3" json:"setting,omitempty"` + // The list of fields to update. + UpdateMask *fieldmaskpb.FieldMask `protobuf:"bytes,2,opt,name=update_mask,json=updateMask,proto3" json:"update_mask,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateWorkspaceSettingRequest) Reset() { + *x = UpdateWorkspaceSettingRequest{} + mi := &file_api_v1_workspace_service_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateWorkspaceSettingRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateWorkspaceSettingRequest) ProtoMessage() {} + +func (x *UpdateWorkspaceSettingRequest) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_workspace_service_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateWorkspaceSettingRequest.ProtoReflect.Descriptor instead. +func (*UpdateWorkspaceSettingRequest) Descriptor() ([]byte, []int) { + return file_api_v1_workspace_service_proto_rawDescGZIP(), []int{8} +} + +func (x *UpdateWorkspaceSettingRequest) GetSetting() *WorkspaceSetting { + if x != nil { + return x.Setting + } + return nil +} + +func (x *UpdateWorkspaceSettingRequest) GetUpdateMask() *fieldmaskpb.FieldMask { + if x != nil { + return x.UpdateMask + } + return nil +} + +// Reference: https://developers.cloudflare.com/r2/examples/aws/aws-sdk-go/ +type WorkspaceStorageSetting_S3Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + AccessKeyId string `protobuf:"bytes,1,opt,name=access_key_id,json=accessKeyId,proto3" json:"access_key_id,omitempty"` + AccessKeySecret string `protobuf:"bytes,2,opt,name=access_key_secret,json=accessKeySecret,proto3" json:"access_key_secret,omitempty"` + Endpoint string `protobuf:"bytes,3,opt,name=endpoint,proto3" json:"endpoint,omitempty"` + Region string `protobuf:"bytes,4,opt,name=region,proto3" json:"region,omitempty"` + Bucket string `protobuf:"bytes,5,opt,name=bucket,proto3" json:"bucket,omitempty"` + UsePathStyle bool `protobuf:"varint,6,opt,name=use_path_style,json=usePathStyle,proto3" json:"use_path_style,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkspaceStorageSetting_S3Config) Reset() { + *x = WorkspaceStorageSetting_S3Config{} + mi := &file_api_v1_workspace_service_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkspaceStorageSetting_S3Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkspaceStorageSetting_S3Config) ProtoMessage() {} + +func (x *WorkspaceStorageSetting_S3Config) ProtoReflect() protoreflect.Message { + mi := &file_api_v1_workspace_service_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkspaceStorageSetting_S3Config.ProtoReflect.Descriptor instead. +func (*WorkspaceStorageSetting_S3Config) Descriptor() ([]byte, []int) { + return file_api_v1_workspace_service_proto_rawDescGZIP(), []int{5, 0} +} + +func (x *WorkspaceStorageSetting_S3Config) GetAccessKeyId() string { + if x != nil { + return x.AccessKeyId + } + return "" +} + +func (x *WorkspaceStorageSetting_S3Config) GetAccessKeySecret() string { + if x != nil { + return x.AccessKeySecret + } + return "" +} + +func (x *WorkspaceStorageSetting_S3Config) GetEndpoint() string { + if x != nil { + return x.Endpoint + } + return "" +} + +func (x *WorkspaceStorageSetting_S3Config) GetRegion() string { + if x != nil { + return x.Region + } + return "" +} + +func (x *WorkspaceStorageSetting_S3Config) GetBucket() string { + if x != nil { + return x.Bucket + } + return "" +} + +func (x *WorkspaceStorageSetting_S3Config) GetUsePathStyle() bool { + if x != nil { + return x.UsePathStyle + } + return false +} + +var File_api_v1_workspace_service_proto protoreflect.FileDescriptor + +const file_api_v1_workspace_service_proto_rawDesc = "" + + "\n" + + "\x1eapi/v1/workspace_service.proto\x12\fmemos.api.v1\x1a\x1cgoogle/api/annotations.proto\x1a\x17google/api/client.proto\x1a\x1fgoogle/api/field_behavior.proto\x1a\x19google/api/resource.proto\x1a google/protobuf/field_mask.proto\"y\n" + + "\x10WorkspaceProfile\x12\x14\n" + + "\x05owner\x18\x01 \x01(\tR\x05owner\x12\x18\n" + + "\aversion\x18\x02 \x01(\tR\aversion\x12\x12\n" + + "\x04mode\x18\x03 \x01(\tR\x04mode\x12!\n" + + "\finstance_url\x18\x06 \x01(\tR\vinstanceUrl\"\x1c\n" + + "\x1aGetWorkspaceProfileRequest\"\x9f\x03\n" + + "\x10WorkspaceSetting\x12\x17\n" + + "\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12P\n" + + "\x0fgeneral_setting\x18\x02 \x01(\v2%.memos.api.v1.WorkspaceGeneralSettingH\x00R\x0egeneralSetting\x12P\n" + + "\x0fstorage_setting\x18\x03 \x01(\v2%.memos.api.v1.WorkspaceStorageSettingH\x00R\x0estorageSetting\x12]\n" + + "\x14memo_related_setting\x18\x04 \x01(\v2).memos.api.v1.WorkspaceMemoRelatedSettingH\x00R\x12memoRelatedSetting:f\xeaAc\n" + + "\x1eapi.memos.dev/WorkspaceSetting\x12\x1cworkspace/settings/{setting}*\x11workspaceSettings2\x10workspaceSettingB\a\n" + + "\x05value\"\xef\x03\n" + + "\x17WorkspaceGeneralSetting\x12\x14\n" + + "\x05theme\x18\x01 \x01(\tR\x05theme\x12<\n" + + "\x1adisallow_user_registration\x18\x02 \x01(\bR\x18disallowUserRegistration\x124\n" + + "\x16disallow_password_auth\x18\x03 \x01(\bR\x14disallowPasswordAuth\x12+\n" + + "\x11additional_script\x18\x04 \x01(\tR\x10additionalScript\x12)\n" + + "\x10additional_style\x18\x05 \x01(\tR\x0fadditionalStyle\x12K\n" + + "\x0ecustom_profile\x18\x06 \x01(\v2$.memos.api.v1.WorkspaceCustomProfileR\rcustomProfile\x121\n" + + "\x15week_start_day_offset\x18\a \x01(\x05R\x12weekStartDayOffset\x128\n" + + "\x18disallow_change_username\x18\b \x01(\bR\x16disallowChangeUsername\x128\n" + + "\x18disallow_change_nickname\x18\t \x01(\bR\x16disallowChangeNickname\"\xa3\x01\n" + + "\x16WorkspaceCustomProfile\x12\x14\n" + + "\x05title\x18\x01 \x01(\tR\x05title\x12 \n" + + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x19\n" + + "\blogo_url\x18\x03 \x01(\tR\alogoUrl\x12\x16\n" + + "\x06locale\x18\x04 \x01(\tR\x06locale\x12\x1e\n" + + "\n" + + "appearance\x18\x05 \x01(\tR\n" + + "appearance\"\xb7\x04\n" + + "\x17WorkspaceStorageSetting\x12T\n" + + "\fstorage_type\x18\x01 \x01(\x0e21.memos.api.v1.WorkspaceStorageSetting.StorageTypeR\vstorageType\x12+\n" + + "\x11filepath_template\x18\x02 \x01(\tR\x10filepathTemplate\x12/\n" + + "\x14upload_size_limit_mb\x18\x03 \x01(\x03R\x11uploadSizeLimitMb\x12K\n" + + "\ts3_config\x18\x04 \x01(\v2..memos.api.v1.WorkspaceStorageSetting.S3ConfigR\bs3Config\x1a\xcc\x01\n" + + "\bS3Config\x12\"\n" + + "\raccess_key_id\x18\x01 \x01(\tR\vaccessKeyId\x12*\n" + + "\x11access_key_secret\x18\x02 \x01(\tR\x0faccessKeySecret\x12\x1a\n" + + "\bendpoint\x18\x03 \x01(\tR\bendpoint\x12\x16\n" + + "\x06region\x18\x04 \x01(\tR\x06region\x12\x16\n" + + "\x06bucket\x18\x05 \x01(\tR\x06bucket\x12$\n" + + "\x0euse_path_style\x18\x06 \x01(\bR\fusePathStyle\"L\n" + + "\vStorageType\x12\x1c\n" + + "\x18STORAGE_TYPE_UNSPECIFIED\x10\x00\x12\f\n" + + "\bDATABASE\x10\x01\x12\t\n" + + "\x05LOCAL\x10\x02\x12\x06\n" + + "\x02S3\x10\x03\"\x88\x04\n" + + "\x1bWorkspaceMemoRelatedSetting\x12<\n" + + "\x1adisallow_public_visibility\x18\x01 \x01(\bR\x18disallowPublicVisibility\x127\n" + + "\x18display_with_update_time\x18\x02 \x01(\bR\x15displayWithUpdateTime\x120\n" + + "\x14content_length_limit\x18\x03 \x01(\x05R\x12contentLengthLimit\x127\n" + + "\x18enable_double_click_edit\x18\x04 \x01(\bR\x15enableDoubleClickEdit\x12.\n" + + "\x13enable_link_preview\x18\x05 \x01(\bR\x11enableLinkPreview\x12%\n" + + "\x0eenable_comment\x18\x06 \x01(\bR\renableComment\x12\x1c\n" + + "\treactions\x18\a \x03(\tR\treactions\x12<\n" + + "\x1adisable_markdown_shortcuts\x18\b \x01(\bR\x18disableMarkdownShortcuts\x127\n" + + "\x18enable_blur_nsfw_content\x18\t \x01(\bR\x15enableBlurNsfwContent\x12\x1b\n" + + "\tnsfw_tags\x18\n" + + " \x03(\tR\bnsfwTags\"X\n" + + "\x1aGetWorkspaceSettingRequest\x12:\n" + + "\x04name\x18\x01 \x01(\tB&\xe0A\x02\xfaA \n" + + "\x1eapi.memos.dev/WorkspaceSettingR\x04name\"\xa0\x01\n" + + "\x1dUpdateWorkspaceSettingRequest\x12=\n" + + "\asetting\x18\x01 \x01(\v2\x1e.memos.api.v1.WorkspaceSettingB\x03\xe0A\x02R\asetting\x12@\n" + + "\vupdate_mask\x18\x02 \x01(\v2\x1a.google.protobuf.FieldMaskB\x03\xe0A\x01R\n" + + "updateMask2\xe9\x03\n" + + "\x10WorkspaceService\x12\x82\x01\n" + + "\x13GetWorkspaceProfile\x12(.memos.api.v1.GetWorkspaceProfileRequest\x1a\x1e.memos.api.v1.WorkspaceProfile\"!\x82\xd3\xe4\x93\x02\x1b\x12\x19/api/v1/workspace/profile\x12\x93\x01\n" + + "\x13GetWorkspaceSetting\x12(.memos.api.v1.GetWorkspaceSettingRequest\x1a\x1e.memos.api.v1.WorkspaceSetting\"2\xdaA\x04name\x82\xd3\xe4\x93\x02%\x12#/api/v1/{name=workspace/settings/*}\x12\xb9\x01\n" + + "\x16UpdateWorkspaceSetting\x12+.memos.api.v1.UpdateWorkspaceSettingRequest\x1a\x1e.memos.api.v1.WorkspaceSetting\"R\xdaA\x13setting,update_mask\x82\xd3\xe4\x93\x026:\asetting2+/api/v1/{setting.name=workspace/settings/*}B\xad\x01\n" + + "\x10com.memos.api.v1B\x15WorkspaceServiceProtoP\x01Z0github.com/usememos/memos/proto/gen/api/v1;apiv1\xa2\x02\x03MAX\xaa\x02\fMemos.Api.V1\xca\x02\fMemos\\Api\\V1\xe2\x02\x18Memos\\Api\\V1\\GPBMetadata\xea\x02\x0eMemos::Api::V1b\x06proto3" + +var ( + file_api_v1_workspace_service_proto_rawDescOnce sync.Once + file_api_v1_workspace_service_proto_rawDescData []byte +) + +func file_api_v1_workspace_service_proto_rawDescGZIP() []byte { + file_api_v1_workspace_service_proto_rawDescOnce.Do(func() { + file_api_v1_workspace_service_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_v1_workspace_service_proto_rawDesc), len(file_api_v1_workspace_service_proto_rawDesc))) + }) + return file_api_v1_workspace_service_proto_rawDescData +} + +var file_api_v1_workspace_service_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_api_v1_workspace_service_proto_msgTypes = make([]protoimpl.MessageInfo, 10) +var file_api_v1_workspace_service_proto_goTypes = []any{ + (WorkspaceStorageSetting_StorageType)(0), // 0: memos.api.v1.WorkspaceStorageSetting.StorageType + (*WorkspaceProfile)(nil), // 1: memos.api.v1.WorkspaceProfile + (*GetWorkspaceProfileRequest)(nil), // 2: memos.api.v1.GetWorkspaceProfileRequest + (*WorkspaceSetting)(nil), // 3: memos.api.v1.WorkspaceSetting + (*WorkspaceGeneralSetting)(nil), // 4: memos.api.v1.WorkspaceGeneralSetting + (*WorkspaceCustomProfile)(nil), // 5: memos.api.v1.WorkspaceCustomProfile + (*WorkspaceStorageSetting)(nil), // 6: memos.api.v1.WorkspaceStorageSetting + (*WorkspaceMemoRelatedSetting)(nil), // 7: memos.api.v1.WorkspaceMemoRelatedSetting + (*GetWorkspaceSettingRequest)(nil), // 8: memos.api.v1.GetWorkspaceSettingRequest + (*UpdateWorkspaceSettingRequest)(nil), // 9: memos.api.v1.UpdateWorkspaceSettingRequest + (*WorkspaceStorageSetting_S3Config)(nil), // 10: memos.api.v1.WorkspaceStorageSetting.S3Config + (*fieldmaskpb.FieldMask)(nil), // 11: google.protobuf.FieldMask +} +var file_api_v1_workspace_service_proto_depIdxs = []int32{ + 4, // 0: memos.api.v1.WorkspaceSetting.general_setting:type_name -> memos.api.v1.WorkspaceGeneralSetting + 6, // 1: memos.api.v1.WorkspaceSetting.storage_setting:type_name -> memos.api.v1.WorkspaceStorageSetting + 7, // 2: memos.api.v1.WorkspaceSetting.memo_related_setting:type_name -> memos.api.v1.WorkspaceMemoRelatedSetting + 5, // 3: memos.api.v1.WorkspaceGeneralSetting.custom_profile:type_name -> memos.api.v1.WorkspaceCustomProfile + 0, // 4: memos.api.v1.WorkspaceStorageSetting.storage_type:type_name -> memos.api.v1.WorkspaceStorageSetting.StorageType + 10, // 5: memos.api.v1.WorkspaceStorageSetting.s3_config:type_name -> memos.api.v1.WorkspaceStorageSetting.S3Config + 3, // 6: memos.api.v1.UpdateWorkspaceSettingRequest.setting:type_name -> memos.api.v1.WorkspaceSetting + 11, // 7: memos.api.v1.UpdateWorkspaceSettingRequest.update_mask:type_name -> google.protobuf.FieldMask + 2, // 8: memos.api.v1.WorkspaceService.GetWorkspaceProfile:input_type -> memos.api.v1.GetWorkspaceProfileRequest + 8, // 9: memos.api.v1.WorkspaceService.GetWorkspaceSetting:input_type -> memos.api.v1.GetWorkspaceSettingRequest + 9, // 10: memos.api.v1.WorkspaceService.UpdateWorkspaceSetting:input_type -> memos.api.v1.UpdateWorkspaceSettingRequest + 1, // 11: memos.api.v1.WorkspaceService.GetWorkspaceProfile:output_type -> memos.api.v1.WorkspaceProfile + 3, // 12: memos.api.v1.WorkspaceService.GetWorkspaceSetting:output_type -> memos.api.v1.WorkspaceSetting + 3, // 13: memos.api.v1.WorkspaceService.UpdateWorkspaceSetting:output_type -> memos.api.v1.WorkspaceSetting + 11, // [11:14] is the sub-list for method output_type + 8, // [8:11] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name +} + +func init() { file_api_v1_workspace_service_proto_init() } +func file_api_v1_workspace_service_proto_init() { + if File_api_v1_workspace_service_proto != nil { + return + } + file_api_v1_workspace_service_proto_msgTypes[2].OneofWrappers = []any{ + (*WorkspaceSetting_GeneralSetting)(nil), + (*WorkspaceSetting_StorageSetting)(nil), + (*WorkspaceSetting_MemoRelatedSetting)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_v1_workspace_service_proto_rawDesc), len(file_api_v1_workspace_service_proto_rawDesc)), + NumEnums: 1, + NumMessages: 10, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_api_v1_workspace_service_proto_goTypes, + DependencyIndexes: file_api_v1_workspace_service_proto_depIdxs, + EnumInfos: file_api_v1_workspace_service_proto_enumTypes, + MessageInfos: file_api_v1_workspace_service_proto_msgTypes, + }.Build() + File_api_v1_workspace_service_proto = out.File + file_api_v1_workspace_service_proto_goTypes = nil + file_api_v1_workspace_service_proto_depIdxs = nil +} diff --git a/proto/gen/api/v1/workspace_service.pb.gw.go b/proto/gen/api/v1/workspace_service.pb.gw.go new file mode 100644 index 0000000..acfb832 --- /dev/null +++ b/proto/gen/api/v1/workspace_service.pb.gw.go @@ -0,0 +1,349 @@ +// Code generated by protoc-gen-grpc-gateway. DO NOT EDIT. +// source: api/v1/workspace_service.proto + +/* +Package apiv1 is a reverse proxy. + +It translates gRPC into RESTful JSON APIs. +*/ +package apiv1 + +import ( + "context" + "errors" + "io" + "net/http" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/grpc-ecosystem/grpc-gateway/v2/utilities" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/grpclog" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" +) + +// Suppress "imported and not used" errors +var ( + _ codes.Code + _ io.Reader + _ status.Status + _ = errors.New + _ = runtime.String + _ = utilities.NewDoubleArray + _ = metadata.Join +) + +func request_WorkspaceService_GetWorkspaceProfile_0(ctx context.Context, marshaler runtime.Marshaler, client WorkspaceServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetWorkspaceProfileRequest + metadata runtime.ServerMetadata + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + msg, err := client.GetWorkspaceProfile(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_WorkspaceService_GetWorkspaceProfile_0(ctx context.Context, marshaler runtime.Marshaler, server WorkspaceServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetWorkspaceProfileRequest + metadata runtime.ServerMetadata + ) + msg, err := server.GetWorkspaceProfile(ctx, &protoReq) + return msg, metadata, err +} + +func request_WorkspaceService_GetWorkspaceSetting_0(ctx context.Context, marshaler runtime.Marshaler, client WorkspaceServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetWorkspaceSettingRequest + metadata runtime.ServerMetadata + err error + ) + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := client.GetWorkspaceSetting(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_WorkspaceService_GetWorkspaceSetting_0(ctx context.Context, marshaler runtime.Marshaler, server WorkspaceServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq GetWorkspaceSettingRequest + metadata runtime.ServerMetadata + err error + ) + val, ok := pathParams["name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "name") + } + protoReq.Name, err = runtime.String(val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "name", err) + } + msg, err := server.GetWorkspaceSetting(ctx, &protoReq) + return msg, metadata, err +} + +var filter_WorkspaceService_UpdateWorkspaceSetting_0 = &utilities.DoubleArray{Encoding: map[string]int{"setting": 0, "name": 1}, Base: []int{1, 2, 1, 0, 0}, Check: []int{0, 1, 2, 3, 2}} + +func request_WorkspaceService_UpdateWorkspaceSetting_0(ctx context.Context, marshaler runtime.Marshaler, client WorkspaceServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateWorkspaceSettingRequest + metadata runtime.ServerMetadata + err error + ) + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Setting); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if req.Body != nil { + _, _ = io.Copy(io.Discard, req.Body) + } + if protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 { + if fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Setting); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } else { + protoReq.UpdateMask = fieldMask + } + } + val, ok := pathParams["setting.name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "setting.name") + } + err = runtime.PopulateFieldFromPath(&protoReq, "setting.name", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "setting.name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_WorkspaceService_UpdateWorkspaceSetting_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.UpdateWorkspaceSetting(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) + return msg, metadata, err +} + +func local_request_WorkspaceService_UpdateWorkspaceSetting_0(ctx context.Context, marshaler runtime.Marshaler, server WorkspaceServiceServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { + var ( + protoReq UpdateWorkspaceSettingRequest + metadata runtime.ServerMetadata + err error + ) + newReader, berr := utilities.IOReaderFactory(req.Body) + if berr != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr) + } + if err := marshaler.NewDecoder(newReader()).Decode(&protoReq.Setting); err != nil && !errors.Is(err, io.EOF) { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if protoReq.UpdateMask == nil || len(protoReq.UpdateMask.GetPaths()) == 0 { + if fieldMask, err := runtime.FieldMaskFromRequestBody(newReader(), protoReq.Setting); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } else { + protoReq.UpdateMask = fieldMask + } + } + val, ok := pathParams["setting.name"] + if !ok { + return nil, metadata, status.Errorf(codes.InvalidArgument, "missing parameter %s", "setting.name") + } + err = runtime.PopulateFieldFromPath(&protoReq, "setting.name", val) + if err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "setting.name", err) + } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_WorkspaceService_UpdateWorkspaceSetting_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.UpdateWorkspaceSetting(ctx, &protoReq) + return msg, metadata, err +} + +// RegisterWorkspaceServiceHandlerServer registers the http handlers for service WorkspaceService to "mux". +// UnaryRPC :call WorkspaceServiceServer directly. +// StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906. +// Note that using this registration option will cause many gRPC library features to stop working. Consider using RegisterWorkspaceServiceHandlerFromEndpoint instead. +// GRPC interceptors will not work for this type of registration. To use interceptors, you must use the "runtime.WithMiddlewares" option in the "runtime.NewServeMux" call. +func RegisterWorkspaceServiceHandlerServer(ctx context.Context, mux *runtime.ServeMux, server WorkspaceServiceServer) error { + mux.Handle(http.MethodGet, pattern_WorkspaceService_GetWorkspaceProfile_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.WorkspaceService/GetWorkspaceProfile", runtime.WithHTTPPathPattern("/api/v1/workspace/profile")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_WorkspaceService_GetWorkspaceProfile_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_WorkspaceService_GetWorkspaceProfile_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_WorkspaceService_GetWorkspaceSetting_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.WorkspaceService/GetWorkspaceSetting", runtime.WithHTTPPathPattern("/api/v1/{name=workspace/settings/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_WorkspaceService_GetWorkspaceSetting_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_WorkspaceService_GetWorkspaceSetting_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPatch, pattern_WorkspaceService_UpdateWorkspaceSetting_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + var stream runtime.ServerTransportStream + ctx = grpc.NewContextWithServerTransportStream(ctx, &stream) + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/memos.api.v1.WorkspaceService/UpdateWorkspaceSetting", runtime.WithHTTPPathPattern("/api/v1/{setting.name=workspace/settings/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := local_request_WorkspaceService_UpdateWorkspaceSetting_0(annotatedContext, inboundMarshaler, server, req, pathParams) + md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer()) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_WorkspaceService_UpdateWorkspaceSetting_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + + return nil +} + +// RegisterWorkspaceServiceHandlerFromEndpoint is same as RegisterWorkspaceServiceHandler but +// automatically dials to "endpoint" and closes the connection when "ctx" gets done. +func RegisterWorkspaceServiceHandlerFromEndpoint(ctx context.Context, mux *runtime.ServeMux, endpoint string, opts []grpc.DialOption) (err error) { + conn, err := grpc.NewClient(endpoint, opts...) + if err != nil { + return err + } + defer func() { + if err != nil { + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + return + } + go func() { + <-ctx.Done() + if cerr := conn.Close(); cerr != nil { + grpclog.Errorf("Failed to close conn to %s: %v", endpoint, cerr) + } + }() + }() + return RegisterWorkspaceServiceHandler(ctx, mux, conn) +} + +// RegisterWorkspaceServiceHandler registers the http handlers for service WorkspaceService to "mux". +// The handlers forward requests to the grpc endpoint over "conn". +func RegisterWorkspaceServiceHandler(ctx context.Context, mux *runtime.ServeMux, conn *grpc.ClientConn) error { + return RegisterWorkspaceServiceHandlerClient(ctx, mux, NewWorkspaceServiceClient(conn)) +} + +// RegisterWorkspaceServiceHandlerClient registers the http handlers for service WorkspaceService +// to "mux". The handlers forward requests to the grpc endpoint over the given implementation of "WorkspaceServiceClient". +// Note: the gRPC framework executes interceptors within the gRPC handler. If the passed in "WorkspaceServiceClient" +// doesn't go through the normal gRPC flow (creating a gRPC client etc.) then it will be up to the passed in +// "WorkspaceServiceClient" to call the correct interceptors. This client ignores the HTTP middlewares. +func RegisterWorkspaceServiceHandlerClient(ctx context.Context, mux *runtime.ServeMux, client WorkspaceServiceClient) error { + mux.Handle(http.MethodGet, pattern_WorkspaceService_GetWorkspaceProfile_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.WorkspaceService/GetWorkspaceProfile", runtime.WithHTTPPathPattern("/api/v1/workspace/profile")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_WorkspaceService_GetWorkspaceProfile_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_WorkspaceService_GetWorkspaceProfile_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodGet, pattern_WorkspaceService_GetWorkspaceSetting_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.WorkspaceService/GetWorkspaceSetting", runtime.WithHTTPPathPattern("/api/v1/{name=workspace/settings/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_WorkspaceService_GetWorkspaceSetting_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_WorkspaceService_GetWorkspaceSetting_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + mux.Handle(http.MethodPatch, pattern_WorkspaceService_UpdateWorkspaceSetting_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) { + ctx, cancel := context.WithCancel(req.Context()) + defer cancel() + inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req) + annotatedContext, err := runtime.AnnotateContext(ctx, mux, req, "/memos.api.v1.WorkspaceService/UpdateWorkspaceSetting", runtime.WithHTTPPathPattern("/api/v1/{setting.name=workspace/settings/*}")) + if err != nil { + runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err) + return + } + resp, md, err := request_WorkspaceService_UpdateWorkspaceSetting_0(annotatedContext, inboundMarshaler, client, req, pathParams) + annotatedContext = runtime.NewServerMetadataContext(annotatedContext, md) + if err != nil { + runtime.HTTPError(annotatedContext, mux, outboundMarshaler, w, req, err) + return + } + forward_WorkspaceService_UpdateWorkspaceSetting_0(annotatedContext, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...) + }) + return nil +} + +var ( + pattern_WorkspaceService_GetWorkspaceProfile_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"api", "v1", "workspace", "profile"}, "")) + pattern_WorkspaceService_GetWorkspaceSetting_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 3, 5, 4}, []string{"api", "v1", "workspace", "settings", "name"}, "")) + pattern_WorkspaceService_UpdateWorkspaceSetting_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3, 1, 0, 4, 3, 5, 4}, []string{"api", "v1", "workspace", "settings", "setting.name"}, "")) +) + +var ( + forward_WorkspaceService_GetWorkspaceProfile_0 = runtime.ForwardResponseMessage + forward_WorkspaceService_GetWorkspaceSetting_0 = runtime.ForwardResponseMessage + forward_WorkspaceService_UpdateWorkspaceSetting_0 = runtime.ForwardResponseMessage +) diff --git a/proto/gen/api/v1/workspace_service_grpc.pb.go b/proto/gen/api/v1/workspace_service_grpc.pb.go new file mode 100644 index 0000000..36e6f33 --- /dev/null +++ b/proto/gen/api/v1/workspace_service_grpc.pb.go @@ -0,0 +1,203 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.5.1 +// - protoc (unknown) +// source: api/v1/workspace_service.proto + +package apiv1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + WorkspaceService_GetWorkspaceProfile_FullMethodName = "/memos.api.v1.WorkspaceService/GetWorkspaceProfile" + WorkspaceService_GetWorkspaceSetting_FullMethodName = "/memos.api.v1.WorkspaceService/GetWorkspaceSetting" + WorkspaceService_UpdateWorkspaceSetting_FullMethodName = "/memos.api.v1.WorkspaceService/UpdateWorkspaceSetting" +) + +// WorkspaceServiceClient is the client API for WorkspaceService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +type WorkspaceServiceClient interface { + // Gets the workspace profile. + GetWorkspaceProfile(ctx context.Context, in *GetWorkspaceProfileRequest, opts ...grpc.CallOption) (*WorkspaceProfile, error) + // Gets a workspace setting. + GetWorkspaceSetting(ctx context.Context, in *GetWorkspaceSettingRequest, opts ...grpc.CallOption) (*WorkspaceSetting, error) + // Updates a workspace setting. + UpdateWorkspaceSetting(ctx context.Context, in *UpdateWorkspaceSettingRequest, opts ...grpc.CallOption) (*WorkspaceSetting, error) +} + +type workspaceServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewWorkspaceServiceClient(cc grpc.ClientConnInterface) WorkspaceServiceClient { + return &workspaceServiceClient{cc} +} + +func (c *workspaceServiceClient) GetWorkspaceProfile(ctx context.Context, in *GetWorkspaceProfileRequest, opts ...grpc.CallOption) (*WorkspaceProfile, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(WorkspaceProfile) + err := c.cc.Invoke(ctx, WorkspaceService_GetWorkspaceProfile_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *workspaceServiceClient) GetWorkspaceSetting(ctx context.Context, in *GetWorkspaceSettingRequest, opts ...grpc.CallOption) (*WorkspaceSetting, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(WorkspaceSetting) + err := c.cc.Invoke(ctx, WorkspaceService_GetWorkspaceSetting_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *workspaceServiceClient) UpdateWorkspaceSetting(ctx context.Context, in *UpdateWorkspaceSettingRequest, opts ...grpc.CallOption) (*WorkspaceSetting, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(WorkspaceSetting) + err := c.cc.Invoke(ctx, WorkspaceService_UpdateWorkspaceSetting_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// WorkspaceServiceServer is the server API for WorkspaceService service. +// All implementations must embed UnimplementedWorkspaceServiceServer +// for forward compatibility. +type WorkspaceServiceServer interface { + // Gets the workspace profile. + GetWorkspaceProfile(context.Context, *GetWorkspaceProfileRequest) (*WorkspaceProfile, error) + // Gets a workspace setting. + GetWorkspaceSetting(context.Context, *GetWorkspaceSettingRequest) (*WorkspaceSetting, error) + // Updates a workspace setting. + UpdateWorkspaceSetting(context.Context, *UpdateWorkspaceSettingRequest) (*WorkspaceSetting, error) + mustEmbedUnimplementedWorkspaceServiceServer() +} + +// UnimplementedWorkspaceServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedWorkspaceServiceServer struct{} + +func (UnimplementedWorkspaceServiceServer) GetWorkspaceProfile(context.Context, *GetWorkspaceProfileRequest) (*WorkspaceProfile, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetWorkspaceProfile not implemented") +} +func (UnimplementedWorkspaceServiceServer) GetWorkspaceSetting(context.Context, *GetWorkspaceSettingRequest) (*WorkspaceSetting, error) { + return nil, status.Errorf(codes.Unimplemented, "method GetWorkspaceSetting not implemented") +} +func (UnimplementedWorkspaceServiceServer) UpdateWorkspaceSetting(context.Context, *UpdateWorkspaceSettingRequest) (*WorkspaceSetting, error) { + return nil, status.Errorf(codes.Unimplemented, "method UpdateWorkspaceSetting not implemented") +} +func (UnimplementedWorkspaceServiceServer) mustEmbedUnimplementedWorkspaceServiceServer() {} +func (UnimplementedWorkspaceServiceServer) testEmbeddedByValue() {} + +// UnsafeWorkspaceServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to WorkspaceServiceServer will +// result in compilation errors. +type UnsafeWorkspaceServiceServer interface { + mustEmbedUnimplementedWorkspaceServiceServer() +} + +func RegisterWorkspaceServiceServer(s grpc.ServiceRegistrar, srv WorkspaceServiceServer) { + // If the following call pancis, it indicates UnimplementedWorkspaceServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&WorkspaceService_ServiceDesc, srv) +} + +func _WorkspaceService_GetWorkspaceProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetWorkspaceProfileRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WorkspaceServiceServer).GetWorkspaceProfile(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: WorkspaceService_GetWorkspaceProfile_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WorkspaceServiceServer).GetWorkspaceProfile(ctx, req.(*GetWorkspaceProfileRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _WorkspaceService_GetWorkspaceSetting_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetWorkspaceSettingRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WorkspaceServiceServer).GetWorkspaceSetting(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: WorkspaceService_GetWorkspaceSetting_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WorkspaceServiceServer).GetWorkspaceSetting(ctx, req.(*GetWorkspaceSettingRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _WorkspaceService_UpdateWorkspaceSetting_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateWorkspaceSettingRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(WorkspaceServiceServer).UpdateWorkspaceSetting(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: WorkspaceService_UpdateWorkspaceSetting_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(WorkspaceServiceServer).UpdateWorkspaceSetting(ctx, req.(*UpdateWorkspaceSettingRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// WorkspaceService_ServiceDesc is the grpc.ServiceDesc for WorkspaceService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var WorkspaceService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "memos.api.v1.WorkspaceService", + HandlerType: (*WorkspaceServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetWorkspaceProfile", + Handler: _WorkspaceService_GetWorkspaceProfile_Handler, + }, + { + MethodName: "GetWorkspaceSetting", + Handler: _WorkspaceService_GetWorkspaceSetting_Handler, + }, + { + MethodName: "UpdateWorkspaceSetting", + Handler: _WorkspaceService_UpdateWorkspaceSetting_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "api/v1/workspace_service.proto", +} diff --git a/proto/gen/apidocs.swagger.yaml b/proto/gen/apidocs.swagger.yaml new file mode 100644 index 0000000..fbe3ad0 --- /dev/null +++ b/proto/gen/apidocs.swagger.yaml @@ -0,0 +1,4220 @@ +swagger: "2.0" +info: + title: api/v1/activity_service.proto + version: version not set +tags: + - name: ActivityService + - name: AttachmentService + - name: UserService + - name: AuthService + - name: IdentityProviderService + - name: InboxService + - name: MarkdownService + - name: MemoService + - name: ShortcutService + - name: WebhookService + - name: WorkspaceService +consumes: + - application/json +produces: + - application/json +paths: + /api/v1/activities: + get: + summary: ListActivities returns a list of activities. + operationId: ActivityService_ListActivities + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1ListActivitiesResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: pageSize + description: "The maximum number of activities to return.\r\nThe service may return fewer than this value.\r\nIf unspecified, at most 100 activities will be returned.\r\nThe maximum value is 1000; values above 1000 will be coerced to 1000." + in: query + required: false + type: integer + format: int32 + - name: pageToken + description: "A page token, received from a previous `ListActivities` call.\r\nProvide this to retrieve the subsequent page." + in: query + required: false + type: string + tags: + - ActivityService + /api/v1/attachments: + get: + summary: ListAttachments lists all attachments. + operationId: AttachmentService_ListAttachments + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1ListAttachmentsResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: pageSize + description: "Optional. The maximum number of attachments to return.\r\nThe service may return fewer than this value.\r\nIf unspecified, at most 50 attachments will be returned.\r\nThe maximum value is 1000; values above 1000 will be coerced to 1000." + in: query + required: false + type: integer + format: int32 + - name: pageToken + description: "Optional. A page token, received from a previous `ListAttachments` call.\r\nProvide this to retrieve the subsequent page." + in: query + required: false + type: string + - name: filter + description: "Optional. Filter to apply to the list results.\r\nExample: \"type=image/png\" or \"filename:*.jpg\"\r\nSupported operators: =, !=, <, <=, >, >=, :\r\nSupported fields: filename, type, size, create_time, memo" + in: query + required: false + type: string + - name: orderBy + description: "Optional. The order to sort results by.\r\nExample: \"create_time desc\" or \"filename asc\"" + in: query + required: false + type: string + tags: + - AttachmentService + post: + summary: CreateAttachment creates a new attachment. + operationId: AttachmentService_CreateAttachment + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1Attachment' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: attachment + description: Required. The attachment to create. + in: body + required: true + schema: + $ref: '#/definitions/v1Attachment' + required: + - attachment + - name: attachmentId + description: "Optional. The attachment ID to use for this attachment.\r\nIf empty, a unique ID will be generated." + in: query + required: false + type: string + tags: + - AttachmentService + /api/v1/auth/sessions: + post: + summary: "CreateSession authenticates a user and creates a new session.\r\nReturns the authenticated user information upon successful authentication." + operationId: AuthService_CreateSession + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1CreateSessionResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: body + in: body + required: true + schema: + $ref: '#/definitions/v1CreateSessionRequest' + tags: + - AuthService + /api/v1/auth/sessions/current: + get: + summary: "GetCurrentSession returns the current active session information.\r\nThis method is idempotent and safe, suitable for checking current session state." + operationId: AuthService_GetCurrentSession + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1GetCurrentSessionResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + tags: + - AuthService + delete: + summary: "DeleteSession terminates the current user session.\r\nThis is an idempotent operation that invalidates the user's authentication." + operationId: AuthService_DeleteSession + responses: + "200": + description: A successful response. + schema: + type: object + properties: {} + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + tags: + - AuthService + /api/v1/identityProviders: + get: + summary: ListIdentityProviders lists identity providers. + operationId: IdentityProviderService_ListIdentityProviders + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1ListIdentityProvidersResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + tags: + - IdentityProviderService + post: + summary: CreateIdentityProvider creates an identity provider. + operationId: IdentityProviderService_CreateIdentityProvider + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/apiv1IdentityProvider' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: identityProvider + description: Required. The identity provider to create. + in: body + required: true + schema: + $ref: '#/definitions/apiv1IdentityProvider' + required: + - identityProvider + - name: identityProviderId + description: "Optional. The ID to use for the identity provider, which will become the final component of the resource name.\r\nIf not provided, the system will generate one." + in: query + required: false + type: string + tags: + - IdentityProviderService + /api/v1/markdown/links:getMetadata: + get: + summary: "GetLinkMetadata returns metadata for a given link.\r\nThis is useful for generating link previews." + operationId: MarkdownService_GetLinkMetadata + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1LinkMetadata' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: link + description: The link URL to get metadata for. + in: query + required: true + type: string + tags: + - MarkdownService + /api/v1/markdown:parse: + post: + summary: "ParseMarkdown parses the given markdown content and returns a list of nodes.\r\nThis is a utility method that transforms markdown text into structured nodes." + operationId: MarkdownService_ParseMarkdown + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1ParseMarkdownResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: body + in: body + required: true + schema: + $ref: '#/definitions/v1ParseMarkdownRequest' + tags: + - MarkdownService + /api/v1/markdown:restore: + post: + summary: "RestoreMarkdownNodes restores the given nodes to markdown content.\r\nThis is the inverse operation of ParseMarkdown." + operationId: MarkdownService_RestoreMarkdownNodes + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1RestoreMarkdownNodesResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: body + in: body + required: true + schema: + $ref: '#/definitions/v1RestoreMarkdownNodesRequest' + tags: + - MarkdownService + /api/v1/markdown:stringify: + post: + summary: "StringifyMarkdownNodes stringify the given nodes to plain text content.\r\nThis removes all markdown formatting and returns plain text." + operationId: MarkdownService_StringifyMarkdownNodes + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1StringifyMarkdownNodesResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: body + in: body + required: true + schema: + $ref: '#/definitions/v1StringifyMarkdownNodesRequest' + tags: + - MarkdownService + /api/v1/memos: + get: + summary: ListMemos lists memos with pagination and filter. + operationId: MemoService_ListMemos + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1ListMemosResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: parent + description: |- + Optional. The parent is the owner of the memos. + If not specified or `users/-`, it will list all memos. + Format: users/{user} + in: query + required: false + type: string + - name: pageSize + description: |- + Optional. The maximum number of memos to return. + The service may return fewer than this value. + If unspecified, at most 50 memos will be returned. + The maximum value is 1000; values above 1000 will be coerced to 1000. + in: query + required: false + type: integer + format: int32 + - name: pageToken + description: |- + Optional. A page token, received from a previous `ListMemos` call. + Provide this to retrieve the subsequent page. + in: query + required: false + type: string + - name: state + description: |- + Optional. The state of the memos to list. + Default to `NORMAL`. Set to `ARCHIVED` to list archived memos. + in: query + required: false + type: string + enum: + - STATE_UNSPECIFIED + - NORMAL + - ARCHIVED + default: STATE_UNSPECIFIED + - name: orderBy + description: |- + Optional. The order to sort results by. + Default to "display_time desc". + Example: "display_time desc" or "create_time asc" + in: query + required: false + type: string + - name: filter + description: |- + Optional. Filter to apply to the list results. + Filter is a CEL expression to filter memos. + Refer to `Shortcut.filter`. + in: query + required: false + type: string + - name: showDeleted + description: Optional. If true, show deleted memos in the response. + in: query + required: false + type: boolean + - name: oldFilter + description: |- + [Deprecated] Old filter contains some specific conditions to filter memos. + Format: "creator == 'users/{user}' && visibilities == ['PUBLIC', 'PROTECTED']" + in: query + required: false + type: string + tags: + - MemoService + post: + summary: CreateMemo creates a memo. + operationId: MemoService_CreateMemo + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/apiv1Memo' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: memo + description: Required. The memo to create. + in: body + required: true + schema: + $ref: '#/definitions/apiv1Memo' + required: + - memo + - name: memoId + description: |- + Optional. The memo ID to use for this memo. + If empty, a unique ID will be generated. + in: query + required: false + type: string + - name: validateOnly + description: Optional. If set, validate the request but don't actually create the memo. + in: query + required: false + type: boolean + - name: requestId + description: Optional. An idempotency token. + in: query + required: false + type: string + tags: + - MemoService + /api/v1/memos:export: + post: + summary: ExportMemos exports memos for the current user + operationId: MemoService_ExportMemos + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1ExportMemosResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: body + in: body + required: true + schema: + $ref: '#/definitions/v1ExportMemosRequest' + tags: + - MemoService + /api/v1/memos:import: + post: + summary: ImportMemos imports memos from provided data + operationId: MemoService_ImportMemos + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1ImportMemosResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: body + in: body + required: true + schema: + $ref: '#/definitions/v1ImportMemosRequest' + tags: + - MemoService + /api/v1/users: + get: + summary: ListUsers returns a list of users. + operationId: UserService_ListUsers + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1ListUsersResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: pageSize + description: "Optional. The maximum number of users to return.\r\nThe service may return fewer than this value.\r\nIf unspecified, at most 50 users will be returned.\r\nThe maximum value is 1000; values above 1000 will be coerced to 1000." + in: query + required: false + type: integer + format: int32 + - name: pageToken + description: "Optional. A page token, received from a previous `ListUsers` call.\r\nProvide this to retrieve the subsequent page." + in: query + required: false + type: string + - name: filter + description: "Optional. Filter to apply to the list results.\r\nExample: \"state=ACTIVE\" or \"role=USER\" or \"email:@example.com\"\r\nSupported operators: =, !=, <, <=, >, >=, :\r\nSupported fields: username, email, role, state, create_time, update_time" + in: query + required: false + type: string + - name: orderBy + description: "Optional. The order to sort results by.\r\nExample: \"create_time desc\" or \"username asc\"" + in: query + required: false + type: string + - name: showDeleted + description: Optional. If true, show deleted users in the response. + in: query + required: false + type: boolean + tags: + - UserService + post: + summary: CreateUser creates a new user. + operationId: UserService_CreateUser + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1User' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: user + description: Required. The user to create. + in: body + required: true + schema: + $ref: '#/definitions/v1User' + required: + - user + - name: userId + description: "Optional. The user ID to use for this user.\r\nIf empty, a unique ID will be generated.\r\nMust match the pattern [a-z0-9-]+" + in: query + required: false + type: string + - name: validateOnly + description: Optional. If set, validate the request but don't actually create the user. + in: query + required: false + type: boolean + - name: requestId + description: "Optional. An idempotency token that can be used to ensure that multiple\r\nrequests to create a user have the same result." + in: query + required: false + type: string + tags: + - UserService + /api/v1/users:search: + get: + summary: SearchUsers searches for users based on query. + operationId: UserService_SearchUsers + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1SearchUsersResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: query + description: Required. The search query. + in: query + required: true + type: string + - name: pageSize + description: Optional. The maximum number of users to return. + in: query + required: false + type: integer + format: int32 + - name: pageToken + description: Optional. A page token for pagination. + in: query + required: false + type: string + tags: + - UserService + /api/v1/users:stats: + get: + summary: ListAllUserStats returns statistics for all users. + operationId: UserService_ListAllUserStats + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1ListAllUserStatsResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: pageSize + description: Optional. The maximum number of user stats to return. + in: query + required: false + type: integer + format: int32 + - name: pageToken + description: Optional. A page token for pagination. + in: query + required: false + type: string + tags: + - UserService + /api/v1/workspace/profile: + get: + summary: Gets the workspace profile. + operationId: WorkspaceService_GetWorkspaceProfile + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1WorkspaceProfile' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + tags: + - WorkspaceService + /api/v1/{attachment.name}: + patch: + summary: UpdateAttachment updates a attachment. + operationId: AttachmentService_UpdateAttachment + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1Attachment' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: attachment.name + description: "The name of the attachment.\r\nFormat: attachments/{attachment}" + in: path + required: true + type: string + pattern: attachments/[^/]+ + - name: attachment + description: Required. The attachment which replaces the attachment on the server. + in: body + required: true + schema: + type: object + properties: + createTime: + type: string + format: date-time + description: Output only. The creation timestamp. + readOnly: true + filename: + type: string + description: The filename of the attachment. + content: + type: string + format: byte + description: Input only. The content of the attachment. + externalLink: + type: string + description: Optional. The external link of the attachment. + type: + type: string + description: The MIME type of the attachment. + size: + type: string + format: int64 + description: Output only. The size of the attachment in bytes. + readOnly: true + memo: + type: string + title: "Optional. The related memo. Refer to `Memo.name`.\r\nFormat: memos/{memo}" + title: Required. The attachment which replaces the attachment on the server. + required: + - filename + - type + - attachment + tags: + - AttachmentService + /api/v1/{identityProvider.name}: + patch: + summary: UpdateIdentityProvider updates an identity provider. + operationId: IdentityProviderService_UpdateIdentityProvider + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/apiv1IdentityProvider' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: identityProvider.name + description: "The resource name of the identity provider.\r\nFormat: identityProviders/{idp}" + in: path + required: true + type: string + pattern: identityProviders/[^/]+ + - name: identityProvider + description: Required. The identity provider to update. + in: body + required: true + schema: + type: object + properties: + type: + $ref: '#/definitions/apiv1IdentityProviderType' + description: Required. The type of the identity provider. + title: + type: string + description: Required. The display title of the identity provider. + identifierFilter: + type: string + description: Optional. Filter applied to user identifiers. + config: + $ref: '#/definitions/apiv1IdentityProviderConfig' + description: Required. Configuration for the identity provider. + title: Required. The identity provider to update. + required: + - type + - title + - config + - identityProvider + tags: + - IdentityProviderService + /api/v1/{inbox.name}: + patch: + summary: UpdateInbox updates an inbox. + operationId: InboxService_UpdateInbox + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1Inbox' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: inbox.name + description: "The resource name of the inbox.\r\nFormat: inboxes/{inbox}" + in: path + required: true + type: string + pattern: inboxes/[^/]+ + - name: inbox + description: Required. The inbox to update. + in: body + required: true + schema: + type: object + properties: + sender: + type: string + title: "The sender of the inbox notification.\r\nFormat: users/{user}" + readOnly: true + receiver: + type: string + title: "The receiver of the inbox notification.\r\nFormat: users/{user}" + readOnly: true + status: + $ref: '#/definitions/v1InboxStatus' + description: The status of the inbox notification. + createTime: + type: string + format: date-time + description: Output only. The creation timestamp. + readOnly: true + type: + $ref: '#/definitions/v1InboxType' + description: The type of the inbox notification. + readOnly: true + activityId: + type: integer + format: int32 + description: Optional. The activity ID associated with this inbox notification. + title: Required. The inbox to update. + required: + - inbox + - name: allowMissing + description: Optional. If set to true, allows updating missing fields. + in: query + required: false + type: boolean + tags: + - InboxService + /api/v1/{memo.name}: + patch: + summary: UpdateMemo updates a memo. + operationId: MemoService_UpdateMemo + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/apiv1Memo' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: memo.name + description: |- + The resource name of the memo. + Format: memos/{memo}, memo is the user defined id or uuid. + in: path + required: true + type: string + pattern: memos/[^/]+ + - name: memo + description: |- + Required. The memo to update. + The `name` field is required. + in: body + required: true + schema: + type: object + properties: + state: + $ref: '#/definitions/v1State' + description: The state of the memo. + creator: + type: string + title: |- + The name of the creator. + Format: users/{user} + readOnly: true + createTime: + type: string + format: date-time + description: Output only. The creation timestamp. + readOnly: true + updateTime: + type: string + format: date-time + description: Output only. The last update timestamp. + readOnly: true + displayTime: + type: string + format: date-time + description: The display timestamp of the memo. + content: + type: string + description: Required. The content of the memo in Markdown format. + nodes: + type: array + items: + type: object + $ref: '#/definitions/v1Node' + description: Output only. The parsed nodes from the content. + readOnly: true + visibility: + $ref: '#/definitions/v1Visibility' + description: The visibility of the memo. + tags: + type: array + items: + type: string + description: Output only. The tags extracted from the content. + readOnly: true + pinned: + type: boolean + description: Whether the memo is pinned. + attachments: + type: array + items: + type: object + $ref: '#/definitions/v1Attachment' + description: Optional. The attachments of the memo. + relations: + type: array + items: + type: object + $ref: '#/definitions/v1MemoRelation' + description: Optional. The relations of the memo. + reactions: + type: array + items: + type: object + $ref: '#/definitions/v1Reaction' + description: Output only. The reactions to the memo. + readOnly: true + property: + $ref: '#/definitions/v1MemoProperty' + description: Output only. The computed properties of the memo. + readOnly: true + parent: + type: string + title: |- + Output only. The name of the parent memo. + Format: memos/{memo} + readOnly: true + snippet: + type: string + description: Output only. The snippet of the memo content. Plain text only. + readOnly: true + location: + $ref: '#/definitions/apiv1Location' + description: Optional. The location of the memo. + title: |- + Required. The memo to update. + The `name` field is required. + required: + - state + - content + - visibility + - memo + - name: allowMissing + description: Optional. If set to true, allows updating sensitive fields. + in: query + required: false + type: boolean + tags: + - MemoService + /api/v1/{name_1}: + get: + summary: GetAttachment returns a attachment by name. + operationId: AttachmentService_GetAttachment + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1Attachment' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name_1 + description: "Required. The attachment name of the attachment to retrieve.\r\nFormat: attachments/{attachment}" + in: path + required: true + type: string + pattern: attachments/[^/]+ + tags: + - AttachmentService + delete: + summary: DeleteUser deletes a user. + operationId: UserService_DeleteUser + responses: + "200": + description: A successful response. + schema: + type: object + properties: {} + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name_1 + description: "Required. The resource name of the user to delete.\r\nFormat: users/{user}" + in: path + required: true + type: string + pattern: users/[^/]+ + - name: force + description: Optional. If set to true, the user will be deleted even if they have associated data. + in: query + required: false + type: boolean + tags: + - UserService + /api/v1/{name_2}: + get: + summary: GetUser gets a user by name. + operationId: UserService_GetUser + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1User' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name_2 + description: "Required. The resource name of the user.\r\nFormat: users/{user}" + in: path + required: true + type: string + pattern: users/[^/]+ + - name: readMask + description: "Optional. The fields to return in the response.\r\nIf not specified, all fields are returned." + in: query + required: false + type: string + tags: + - UserService + delete: + summary: DeleteUserAccessToken deletes an access token. + operationId: UserService_DeleteUserAccessToken + responses: + "200": + description: A successful response. + schema: + type: object + properties: {} + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name_2 + description: "Required. The resource name of the access token to delete.\r\nFormat: users/{user}/accessTokens/{access_token}" + in: path + required: true + type: string + pattern: users/[^/]+/accessTokens/[^/]+ + tags: + - UserService + /api/v1/{name_3}: + get: + summary: GetIdentityProvider gets an identity provider. + operationId: IdentityProviderService_GetIdentityProvider + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/apiv1IdentityProvider' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name_3 + description: "Required. The resource name of the identity provider to get.\r\nFormat: identityProviders/{idp}" + in: path + required: true + type: string + pattern: identityProviders/[^/]+ + tags: + - IdentityProviderService + delete: + summary: RevokeUserSession revokes a specific session for a user. + operationId: UserService_RevokeUserSession + responses: + "200": + description: A successful response. + schema: + type: object + properties: {} + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name_3 + description: "Required. The resource name of the session to revoke.\r\nFormat: users/{user}/sessions/{session}" + in: path + required: true + type: string + pattern: users/[^/]+/sessions/[^/]+ + tags: + - UserService + /api/v1/{name_4}: + get: + summary: GetMemo gets a memo. + operationId: MemoService_GetMemo + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/apiv1Memo' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name_4 + description: |- + Required. The resource name of the memo. + Format: memos/{memo} + in: path + required: true + type: string + pattern: memos/[^/]+ + - name: readMask + description: |- + Optional. The fields to return in the response. + If not specified, all fields are returned. + in: query + required: false + type: string + tags: + - MemoService + delete: + summary: DeleteIdentityProvider deletes an identity provider. + operationId: IdentityProviderService_DeleteIdentityProvider + responses: + "200": + description: A successful response. + schema: + type: object + properties: {} + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name_4 + description: "Required. The resource name of the identity provider to delete.\r\nFormat: identityProviders/{idp}" + in: path + required: true + type: string + pattern: identityProviders/[^/]+ + tags: + - IdentityProviderService + /api/v1/{name_5}: + get: + summary: GetShortcut gets a shortcut by name. + operationId: ShortcutService_GetShortcut + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/apiv1Shortcut' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name_5 + description: "Required. The resource name of the shortcut to retrieve.\r\nFormat: users/{user}/shortcuts/{shortcut}" + in: path + required: true + type: string + pattern: users/[^/]+/shortcuts/[^/]+ + tags: + - ShortcutService + delete: + summary: DeleteInbox deletes an inbox. + operationId: InboxService_DeleteInbox + responses: + "200": + description: A successful response. + schema: + type: object + properties: {} + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name_5 + description: "Required. The resource name of the inbox to delete.\r\nFormat: inboxes/{inbox}" + in: path + required: true + type: string + pattern: inboxes/[^/]+ + tags: + - InboxService + /api/v1/{name_6}: + get: + summary: GetWebhook gets a webhook by name. + operationId: WebhookService_GetWebhook + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/apiv1Webhook' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name_6 + description: "Required. The resource name of the webhook to retrieve.\r\nFormat: users/{user}/webhooks/{webhook}" + in: path + required: true + type: string + pattern: users/[^/]+/webhooks/[^/]+ + tags: + - WebhookService + delete: + summary: DeleteMemo deletes a memo. + operationId: MemoService_DeleteMemo + responses: + "200": + description: A successful response. + schema: + type: object + properties: {} + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name_6 + description: |- + Required. The resource name of the memo to delete. + Format: memos/{memo} + in: path + required: true + type: string + pattern: memos/[^/]+ + - name: force + description: Optional. If set to true, the memo will be deleted even if it has associated data. + in: query + required: false + type: boolean + tags: + - MemoService + /api/v1/{name_7}: + get: + summary: Gets a workspace setting. + operationId: WorkspaceService_GetWorkspaceSetting + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/apiv1WorkspaceSetting' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name_7 + description: "The resource name of the workspace setting.\r\nFormat: workspace/settings/{setting}" + in: path + required: true + type: string + pattern: workspace/settings/[^/]+ + tags: + - WorkspaceService + delete: + summary: DeleteMemoReaction deletes a reaction for a memo. + operationId: MemoService_DeleteMemoReaction + responses: + "200": + description: A successful response. + schema: + type: object + properties: {} + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name_7 + description: |- + Required. The resource name of the reaction to delete. + Format: reactions/{reaction} + in: path + required: true + type: string + pattern: reactions/[^/]+ + tags: + - MemoService + /api/v1/{name_8}: + delete: + summary: DeleteShortcut deletes a shortcut for a user. + operationId: ShortcutService_DeleteShortcut + responses: + "200": + description: A successful response. + schema: + type: object + properties: {} + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name_8 + description: "Required. The resource name of the shortcut to delete.\r\nFormat: users/{user}/shortcuts/{shortcut}" + in: path + required: true + type: string + pattern: users/[^/]+/shortcuts/[^/]+ + tags: + - ShortcutService + /api/v1/{name_9}: + delete: + summary: DeleteWebhook deletes a webhook for a user. + operationId: WebhookService_DeleteWebhook + responses: + "200": + description: A successful response. + schema: + type: object + properties: {} + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name_9 + description: "Required. The resource name of the webhook to delete.\r\nFormat: users/{user}/webhooks/{webhook}" + in: path + required: true + type: string + pattern: users/[^/]+/webhooks/[^/]+ + tags: + - WebhookService + /api/v1/{name}: + get: + summary: GetActivity returns the activity with the given id. + operationId: ActivityService_GetActivity + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1Activity' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name + description: "The name of the activity.\r\nFormat: activities/{id}, id is the system generated auto-incremented id." + in: path + required: true + type: string + pattern: activities/[^/]+ + tags: + - ActivityService + delete: + summary: DeleteAttachment deletes a attachment by name. + operationId: AttachmentService_DeleteAttachment + responses: + "200": + description: A successful response. + schema: + type: object + properties: {} + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name + description: "Required. The attachment name of the attachment to delete.\r\nFormat: attachments/{attachment}" + in: path + required: true + type: string + pattern: attachments/[^/]+ + tags: + - AttachmentService + /api/v1/{name}/attachments: + get: + summary: ListMemoAttachments lists attachments for a memo. + operationId: MemoService_ListMemoAttachments + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1ListMemoAttachmentsResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name + description: |- + Required. The resource name of the memo. + Format: memos/{memo} + in: path + required: true + type: string + pattern: memos/[^/]+ + - name: pageSize + description: Optional. The maximum number of attachments to return. + in: query + required: false + type: integer + format: int32 + - name: pageToken + description: Optional. A page token for pagination. + in: query + required: false + type: string + tags: + - MemoService + patch: + summary: SetMemoAttachments sets attachments for a memo. + operationId: MemoService_SetMemoAttachments + responses: + "200": + description: A successful response. + schema: + type: object + properties: {} + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name + description: |- + Required. The resource name of the memo. + Format: memos/{memo} + in: path + required: true + type: string + pattern: memos/[^/]+ + - name: body + in: body + required: true + schema: + $ref: '#/definitions/MemoServiceSetMemoAttachmentsBody' + tags: + - MemoService + /api/v1/{name}/avatar: + get: + summary: GetUserAvatar gets the avatar of a user. + operationId: UserService_GetUserAvatar + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/apiHttpBody' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name + description: "Required. The resource name of the user.\r\nFormat: users/{user}" + in: path + required: true + type: string + pattern: users/[^/]+ + tags: + - UserService + /api/v1/{name}/comments: + get: + summary: ListMemoComments lists comments for a memo. + operationId: MemoService_ListMemoComments + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1ListMemoCommentsResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name + description: |- + Required. The resource name of the memo. + Format: memos/{memo} + in: path + required: true + type: string + pattern: memos/[^/]+ + - name: pageSize + description: Optional. The maximum number of comments to return. + in: query + required: false + type: integer + format: int32 + - name: pageToken + description: Optional. A page token for pagination. + in: query + required: false + type: string + - name: orderBy + description: Optional. The order to sort results by. + in: query + required: false + type: string + tags: + - MemoService + post: + summary: CreateMemoComment creates a comment for a memo. + operationId: MemoService_CreateMemoComment + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/apiv1Memo' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name + description: |- + Required. The resource name of the memo. + Format: memos/{memo} + in: path + required: true + type: string + pattern: memos/[^/]+ + - name: comment + description: Required. The comment to create. + in: body + required: true + schema: + $ref: '#/definitions/apiv1Memo' + required: + - comment + - name: commentId + description: Optional. The comment ID to use. + in: query + required: false + type: string + tags: + - MemoService + /api/v1/{name}/reactions: + get: + summary: ListMemoReactions lists reactions for a memo. + operationId: MemoService_ListMemoReactions + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1ListMemoReactionsResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name + description: |- + Required. The resource name of the memo. + Format: memos/{memo} + in: path + required: true + type: string + pattern: memos/[^/]+ + - name: pageSize + description: Optional. The maximum number of reactions to return. + in: query + required: false + type: integer + format: int32 + - name: pageToken + description: Optional. A page token for pagination. + in: query + required: false + type: string + tags: + - MemoService + post: + summary: UpsertMemoReaction upserts a reaction for a memo. + operationId: MemoService_UpsertMemoReaction + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1Reaction' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name + description: |- + Required. The resource name of the memo. + Format: memos/{memo} + in: path + required: true + type: string + pattern: memos/[^/]+ + - name: body + in: body + required: true + schema: + $ref: '#/definitions/MemoServiceUpsertMemoReactionBody' + tags: + - MemoService + /api/v1/{name}/relations: + get: + summary: ListMemoRelations lists relations for a memo. + operationId: MemoService_ListMemoRelations + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1ListMemoRelationsResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name + description: |- + Required. The resource name of the memo. + Format: memos/{memo} + in: path + required: true + type: string + pattern: memos/[^/]+ + - name: pageSize + description: Optional. The maximum number of relations to return. + in: query + required: false + type: integer + format: int32 + - name: pageToken + description: Optional. A page token for pagination. + in: query + required: false + type: string + tags: + - MemoService + patch: + summary: SetMemoRelations sets relations for a memo. + operationId: MemoService_SetMemoRelations + responses: + "200": + description: A successful response. + schema: + type: object + properties: {} + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name + description: |- + Required. The resource name of the memo. + Format: memos/{memo} + in: path + required: true + type: string + pattern: memos/[^/]+ + - name: body + in: body + required: true + schema: + $ref: '#/definitions/MemoServiceSetMemoRelationsBody' + tags: + - MemoService + /api/v1/{name}:getSetting: + get: + summary: GetUserSetting returns the user setting. + operationId: UserService_GetUserSetting + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/apiv1UserSetting' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name + description: "Required. The resource name of the user.\r\nFormat: users/{user}" + in: path + required: true + type: string + pattern: users/[^/]+ + tags: + - UserService + /api/v1/{name}:getStats: + get: + summary: GetUserStats returns statistics for a specific user. + operationId: UserService_GetUserStats + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1UserStats' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name + description: "Required. The resource name of the user.\r\nFormat: users/{user}" + in: path + required: true + type: string + pattern: users/[^/]+ + tags: + - UserService + /api/v1/{parent}/accessTokens: + get: + summary: ListUserAccessTokens returns a list of access tokens for a user. + operationId: UserService_ListUserAccessTokens + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1ListUserAccessTokensResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: parent + description: "Required. The parent resource whose access tokens will be listed.\r\nFormat: users/{user}" + in: path + required: true + type: string + pattern: users/[^/]+ + - name: pageSize + description: Optional. The maximum number of access tokens to return. + in: query + required: false + type: integer + format: int32 + - name: pageToken + description: Optional. A page token for pagination. + in: query + required: false + type: string + tags: + - UserService + post: + summary: CreateUserAccessToken creates a new access token for a user. + operationId: UserService_CreateUserAccessToken + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1UserAccessToken' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: parent + description: "Required. The parent resource where this access token will be created.\r\nFormat: users/{user}" + in: path + required: true + type: string + pattern: users/[^/]+ + - name: accessToken + description: Required. The access token to create. + in: body + required: true + schema: + $ref: '#/definitions/v1UserAccessToken' + required: + - accessToken + - name: accessTokenId + description: Optional. The access token ID to use. + in: query + required: false + type: string + tags: + - UserService + /api/v1/{parent}/inboxes: + get: + summary: ListInboxes lists inboxes for a user. + operationId: InboxService_ListInboxes + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1ListInboxesResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: parent + description: "Required. The parent resource whose inboxes will be listed.\r\nFormat: users/{user}" + in: path + required: true + type: string + pattern: users/[^/]+ + - name: pageSize + description: "Optional. The maximum number of inboxes to return.\r\nThe service may return fewer than this value.\r\nIf unspecified, at most 50 inboxes will be returned.\r\nThe maximum value is 1000; values above 1000 will be coerced to 1000." + in: query + required: false + type: integer + format: int32 + - name: pageToken + description: "Optional. A page token, received from a previous `ListInboxes` call.\r\nProvide this to retrieve the subsequent page." + in: query + required: false + type: string + - name: filter + description: "Optional. Filter to apply to the list results.\r\nExample: \"status=UNREAD\" or \"type=MEMO_COMMENT\"\r\nSupported operators: =, !=\r\nSupported fields: status, type, sender, create_time" + in: query + required: false + type: string + - name: orderBy + description: "Optional. The order to sort results by.\r\nExample: \"create_time desc\" or \"status asc\"" + in: query + required: false + type: string + tags: + - InboxService + /api/v1/{parent}/memos: + get: + summary: ListMemos lists memos with pagination and filter. + operationId: MemoService_ListMemos2 + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1ListMemosResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: parent + description: |- + Optional. The parent is the owner of the memos. + If not specified or `users/-`, it will list all memos. + Format: users/{user} + in: path + required: true + type: string + pattern: users/[^/]+ + - name: pageSize + description: |- + Optional. The maximum number of memos to return. + The service may return fewer than this value. + If unspecified, at most 50 memos will be returned. + The maximum value is 1000; values above 1000 will be coerced to 1000. + in: query + required: false + type: integer + format: int32 + - name: pageToken + description: |- + Optional. A page token, received from a previous `ListMemos` call. + Provide this to retrieve the subsequent page. + in: query + required: false + type: string + - name: state + description: |- + Optional. The state of the memos to list. + Default to `NORMAL`. Set to `ARCHIVED` to list archived memos. + in: query + required: false + type: string + enum: + - STATE_UNSPECIFIED + - NORMAL + - ARCHIVED + default: STATE_UNSPECIFIED + - name: orderBy + description: |- + Optional. The order to sort results by. + Default to "display_time desc". + Example: "display_time desc" or "create_time asc" + in: query + required: false + type: string + - name: filter + description: |- + Optional. Filter to apply to the list results. + Filter is a CEL expression to filter memos. + Refer to `Shortcut.filter`. + in: query + required: false + type: string + - name: showDeleted + description: Optional. If true, show deleted memos in the response. + in: query + required: false + type: boolean + - name: oldFilter + description: |- + [Deprecated] Old filter contains some specific conditions to filter memos. + Format: "creator == 'users/{user}' && visibilities == ['PUBLIC', 'PROTECTED']" + in: query + required: false + type: string + tags: + - MemoService + /api/v1/{parent}/sessions: + get: + summary: ListUserSessions returns a list of active sessions for a user. + operationId: UserService_ListUserSessions + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1ListUserSessionsResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: parent + description: "Required. The resource name of the parent.\r\nFormat: users/{user}" + in: path + required: true + type: string + pattern: users/[^/]+ + tags: + - UserService + /api/v1/{parent}/shortcuts: + get: + summary: ListShortcuts returns a list of shortcuts for a user. + operationId: ShortcutService_ListShortcuts + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1ListShortcutsResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: parent + description: "Required. The parent resource where shortcuts are listed.\r\nFormat: users/{user}" + in: path + required: true + type: string + pattern: users/[^/]+ + tags: + - ShortcutService + post: + summary: CreateShortcut creates a new shortcut for a user. + operationId: ShortcutService_CreateShortcut + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/apiv1Shortcut' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: parent + description: "Required. The parent resource where this shortcut will be created.\r\nFormat: users/{user}" + in: path + required: true + type: string + pattern: users/[^/]+ + - name: shortcut + description: Required. The shortcut to create. + in: body + required: true + schema: + $ref: '#/definitions/apiv1Shortcut' + required: + - shortcut + - name: validateOnly + description: Optional. If set, validate the request, but do not actually create the shortcut. + in: query + required: false + type: boolean + tags: + - ShortcutService + /api/v1/{parent}/tags/{tag}: + delete: + summary: DeleteMemoTag deletes a tag for a memo. + operationId: MemoService_DeleteMemoTag + responses: + "200": + description: A successful response. + schema: + type: object + properties: {} + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: parent + description: |- + Required. The parent, who owns the tags. + Format: memos/{memo}. Use "memos/-" to delete all tags. + in: path + required: true + type: string + pattern: memos/[^/]+ + - name: tag + description: Required. The tag name to delete. + in: path + required: true + type: string + - name: deleteRelatedMemos + description: Optional. Whether to delete related memos. + in: query + required: false + type: boolean + tags: + - MemoService + /api/v1/{parent}/tags:rename: + patch: + summary: RenameMemoTag renames a tag for a memo. + operationId: MemoService_RenameMemoTag + responses: + "200": + description: A successful response. + schema: + type: object + properties: {} + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: parent + description: |- + Required. The parent, who owns the tags. + Format: memos/{memo}. Use "memos/-" to rename all tags. + in: path + required: true + type: string + pattern: memos/[^/]+ + - name: body + in: body + required: true + schema: + $ref: '#/definitions/MemoServiceRenameMemoTagBody' + tags: + - MemoService + /api/v1/{parent}/webhooks: + get: + summary: ListWebhooks returns a list of webhooks for a user. + operationId: WebhookService_ListWebhooks + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1ListWebhooksResponse' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: parent + description: "Required. The parent resource where webhooks are listed.\r\nFormat: users/{user}" + in: path + required: true + type: string + pattern: users/[^/]+ + tags: + - WebhookService + post: + summary: CreateWebhook creates a new webhook for a user. + operationId: WebhookService_CreateWebhook + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/apiv1Webhook' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: parent + description: "Required. The parent resource where this webhook will be created.\r\nFormat: users/{user}" + in: path + required: true + type: string + pattern: users/[^/]+ + - name: webhook + description: Required. The webhook to create. + in: body + required: true + schema: + $ref: '#/definitions/apiv1Webhook' + required: + - webhook + - name: validateOnly + description: Optional. If set, validate the request, but do not actually create the webhook. + in: query + required: false + type: boolean + tags: + - WebhookService + /api/v1/{setting.name}: + patch: + summary: Updates a workspace setting. + operationId: WorkspaceService_UpdateWorkspaceSetting + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/apiv1WorkspaceSetting' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: setting.name + description: "The name of the workspace setting.\r\nFormat: workspace/settings/{setting}" + in: path + required: true + type: string + pattern: workspace/settings/[^/]+ + - name: setting + description: The workspace setting resource which replaces the resource on the server. + in: body + required: true + schema: + type: object + properties: + generalSetting: + $ref: '#/definitions/apiv1WorkspaceGeneralSetting' + storageSetting: + $ref: '#/definitions/apiv1WorkspaceStorageSetting' + memoRelatedSetting: + $ref: '#/definitions/apiv1WorkspaceMemoRelatedSetting' + title: The workspace setting resource which replaces the resource on the server. + required: + - setting + tags: + - WorkspaceService + /api/v1/{setting.name}:updateSetting: + patch: + summary: UpdateUserSetting updates the user setting. + operationId: UserService_UpdateUserSetting + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/apiv1UserSetting' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: setting.name + description: "The resource name of the user whose setting this is.\r\nFormat: users/{user}" + in: path + required: true + type: string + pattern: users/[^/]+ + - name: setting + description: Required. The user setting to update. + in: body + required: true + schema: + type: object + properties: + locale: + type: string + description: The preferred locale of the user. + appearance: + type: string + description: The preferred appearance of the user. + memoVisibility: + type: string + description: The default visibility of the memo. + theme: + type: string + description: "The preferred theme of the user.\r\nThis references a CSS file in the web/public/themes/ directory.\r\nIf not set, the default theme will be used." + title: Required. The user setting to update. + required: + - setting + tags: + - UserService + /api/v1/{shortcut.name}: + patch: + summary: UpdateShortcut updates a shortcut for a user. + operationId: ShortcutService_UpdateShortcut + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/apiv1Shortcut' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: shortcut.name + description: "The resource name of the shortcut.\r\nFormat: users/{user}/shortcuts/{shortcut}" + in: path + required: true + type: string + pattern: users/[^/]+/shortcuts/[^/]+ + - name: shortcut + description: Required. The shortcut resource which replaces the resource on the server. + in: body + required: true + schema: + type: object + properties: + title: + type: string + description: The title of the shortcut. + filter: + type: string + description: The filter expression for the shortcut. + title: Required. The shortcut resource which replaces the resource on the server. + required: + - title + - shortcut + tags: + - ShortcutService + /api/v1/{user.name}: + patch: + summary: UpdateUser updates a user. + operationId: UserService_UpdateUser + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/v1User' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: user.name + description: "The resource name of the user.\r\nFormat: users/{user}" + in: path + required: true + type: string + pattern: users/[^/]+ + - name: user + description: Required. The user to update. + in: body + required: true + schema: + type: object + properties: + role: + $ref: '#/definitions/UserRole' + description: The role of the user. + username: + type: string + description: Required. The unique username for login. + email: + type: string + description: Optional. The email address of the user. + displayName: + type: string + description: Optional. The display name of the user. + avatarUrl: + type: string + description: Optional. The avatar URL of the user. + description: + type: string + description: Optional. The description of the user. + password: + type: string + description: Input only. The password for the user. + state: + $ref: '#/definitions/v1State' + description: The state of the user. + createTime: + type: string + format: date-time + description: Output only. The creation timestamp. + readOnly: true + updateTime: + type: string + format: date-time + description: Output only. The last update timestamp. + readOnly: true + title: Required. The user to update. + required: + - role + - username + - state + - user + - name: allowMissing + description: Optional. If set to true, allows updating sensitive fields. + in: query + required: false + type: boolean + tags: + - UserService + /api/v1/{webhook.name}: + patch: + summary: UpdateWebhook updates a webhook for a user. + operationId: WebhookService_UpdateWebhook + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/apiv1Webhook' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: webhook.name + description: "The resource name of the webhook.\r\nFormat: users/{user}/webhooks/{webhook}" + in: path + required: true + type: string + pattern: users/[^/]+/webhooks/[^/]+ + - name: webhook + description: Required. The webhook resource which replaces the resource on the server. + in: body + required: true + schema: + type: object + properties: + displayName: + type: string + description: The display name of the webhook. + url: + type: string + description: The target URL for the webhook. + title: Required. The webhook resource which replaces the resource on the server. + required: + - displayName + - url + - webhook + tags: + - WebhookService + /file/{name}/{filename}: + get: + summary: GetAttachmentBinary returns a attachment binary by name. + operationId: AttachmentService_GetAttachmentBinary + responses: + "200": + description: A successful response. + schema: + $ref: '#/definitions/apiHttpBody' + default: + description: An unexpected error response. + schema: + $ref: '#/definitions/googlerpcStatus' + parameters: + - name: name + description: "Required. The attachment name of the attachment.\r\nFormat: attachments/{attachment}" + in: path + required: true + type: string + pattern: attachments/[^/]+ + - name: filename + description: The filename of the attachment. Mainly used for downloading. + in: path + required: true + type: string + - name: thumbnail + description: Optional. A flag indicating if the thumbnail version of the attachment should be returned. + in: query + required: false + type: boolean + tags: + - AttachmentService +definitions: + ActivityLevel: + type: string + enum: + - LEVEL_UNSPECIFIED + - INFO + - WARN + - ERROR + default: LEVEL_UNSPECIFIED + description: |- + Activity levels. + + - LEVEL_UNSPECIFIED: Unspecified level. + - INFO: Info level. + - WARN: Warn level. + - ERROR: Error level. + CreateSessionRequestPasswordCredentials: + type: object + properties: + username: + type: string + description: "The username to sign in with.\r\nRequired field for password-based authentication." + password: + type: string + description: "The password to sign in with.\r\nRequired field for password-based authentication." + description: Nested message for password-based authentication credentials. + required: + - username + - password + CreateSessionRequestSSOCredentials: + type: object + properties: + idpId: + type: integer + format: int32 + description: "The ID of the SSO provider.\r\nRequired field to identify the SSO provider." + code: + type: string + description: "The authorization code from the SSO provider.\r\nRequired field for completing the SSO flow." + redirectUri: + type: string + description: "The redirect URI used in the SSO flow.\r\nRequired field for security validation." + description: Nested message for SSO authentication credentials. + required: + - idpId + - code + - redirectUri + ListNodeKind: + type: string + enum: + - KIND_UNSPECIFIED + - ORDERED + - UNORDERED + - DESCRIPTION + default: KIND_UNSPECIFIED + MemoServiceRenameMemoTagBody: + type: object + properties: + oldTag: + type: string + description: Required. The old tag name to rename. + newTag: + type: string + description: Required. The new tag name. + required: + - oldTag + - newTag + MemoServiceSetMemoAttachmentsBody: + type: object + properties: + attachments: + type: array + items: + type: object + $ref: '#/definitions/v1Attachment' + description: Required. The attachments to set for the memo. + required: + - attachments + MemoServiceSetMemoRelationsBody: + type: object + properties: + relations: + type: array + items: + type: object + $ref: '#/definitions/v1MemoRelation' + description: Required. The relations to set for the memo. + required: + - relations + MemoServiceUpsertMemoReactionBody: + type: object + properties: + reaction: + $ref: '#/definitions/v1Reaction' + description: Required. The reaction to upsert. + required: + - reaction + TableNodeRow: + type: object + properties: + cells: + type: array + items: + type: object + $ref: '#/definitions/v1Node' + UserRole: + type: string + enum: + - ROLE_UNSPECIFIED + - HOST + - ADMIN + - USER + default: ROLE_UNSPECIFIED + description: |- + User role enumeration. + + - ROLE_UNSPECIFIED: Unspecified role. + - HOST: Host role with full system access. + - ADMIN: Admin role with administrative privileges. + - USER: Regular user role. + UserStatsMemoTypeStats: + type: object + properties: + linkCount: + type: integer + format: int32 + codeCount: + type: integer + format: int32 + todoCount: + type: integer + format: int32 + undoCount: + type: integer + format: int32 + description: Memo type statistics. + WorkspaceStorageSettingS3Config: + type: object + properties: + accessKeyId: + type: string + accessKeySecret: + type: string + endpoint: + type: string + region: + type: string + bucket: + type: string + usePathStyle: + type: boolean + title: 'Reference: https://developers.cloudflare.com/r2/examples/aws/aws-sdk-go/' + apiHttpBody: + type: object + properties: + contentType: + type: string + description: The HTTP Content-Type header value specifying the content type of the body. + data: + type: string + format: byte + description: The HTTP request/response body as raw binary. + extensions: + type: array + items: + type: object + $ref: '#/definitions/protobufAny' + description: |- + Application specific response metadata. Must be set in the first response + for streaming APIs. + description: |- + Message that represents an arbitrary HTTP body. It should only be used for + payload formats that can't be represented as JSON, such as raw binary or + an HTML page. + + + This message can be used both in streaming and non-streaming API methods in + the request as well as the response. + + It can be used as a top-level request field, which is convenient if one + wants to extract parameters from either the URL or HTTP template into the + request fields and also want access to the raw HTTP body. + + Example: + + message GetResourceRequest { + // A unique request id. + string request_id = 1; + + // The raw HTTP body is bound to this field. + google.api.HttpBody http_body = 2; + + } + + service ResourceService { + rpc GetResource(GetResourceRequest) + returns (google.api.HttpBody); + rpc UpdateResource(google.api.HttpBody) + returns (google.protobuf.Empty); + + } + + Example with streaming methods: + + service CaldavService { + rpc GetCalendar(stream google.api.HttpBody) + returns (stream google.api.HttpBody); + rpc UpdateCalendar(stream google.api.HttpBody) + returns (stream google.api.HttpBody); + + } + + Use of this type only changes how the request and response bodies are + handled, all other features will continue to work unchanged. + apiv1ActivityMemoCommentPayload: + type: object + properties: + memo: + type: string + title: "The memo name of comment.\r\nFormat: memos/{memo}" + relatedMemo: + type: string + title: "The name of related memo.\r\nFormat: memos/{memo}" + description: ActivityMemoCommentPayload represents the payload of a memo comment activity. + apiv1ActivityPayload: + type: object + properties: + memoComment: + $ref: '#/definitions/apiv1ActivityMemoCommentPayload' + description: Memo comment activity payload. + apiv1FieldMapping: + type: object + properties: + identifier: + type: string + displayName: + type: string + email: + type: string + avatarUrl: + type: string + apiv1IdentityProvider: + type: object + properties: + name: + type: string + title: "The resource name of the identity provider.\r\nFormat: identityProviders/{idp}" + type: + $ref: '#/definitions/apiv1IdentityProviderType' + description: Required. The type of the identity provider. + title: + type: string + description: Required. The display title of the identity provider. + identifierFilter: + type: string + description: Optional. Filter applied to user identifiers. + config: + $ref: '#/definitions/apiv1IdentityProviderConfig' + description: Required. Configuration for the identity provider. + required: + - type + - title + - config + apiv1IdentityProviderConfig: + type: object + properties: + oauth2Config: + $ref: '#/definitions/apiv1OAuth2Config' + apiv1IdentityProviderType: + type: string + enum: + - TYPE_UNSPECIFIED + - OAUTH2 + default: TYPE_UNSPECIFIED + description: ' - OAUTH2: OAuth2 identity provider.' + apiv1Location: + type: object + properties: + placeholder: + type: string + description: A placeholder text for the location. + latitude: + type: number + format: double + description: The latitude of the location. + longitude: + type: number + format: double + description: The longitude of the location. + apiv1Memo: + type: object + properties: + name: + type: string + description: |- + The resource name of the memo. + Format: memos/{memo}, memo is the user defined id or uuid. + state: + $ref: '#/definitions/v1State' + description: The state of the memo. + creator: + type: string + title: |- + The name of the creator. + Format: users/{user} + readOnly: true + createTime: + type: string + format: date-time + description: Output only. The creation timestamp. + readOnly: true + updateTime: + type: string + format: date-time + description: Output only. The last update timestamp. + readOnly: true + displayTime: + type: string + format: date-time + description: The display timestamp of the memo. + content: + type: string + description: Required. The content of the memo in Markdown format. + nodes: + type: array + items: + type: object + $ref: '#/definitions/v1Node' + description: Output only. The parsed nodes from the content. + readOnly: true + visibility: + $ref: '#/definitions/v1Visibility' + description: The visibility of the memo. + tags: + type: array + items: + type: string + description: Output only. The tags extracted from the content. + readOnly: true + pinned: + type: boolean + description: Whether the memo is pinned. + attachments: + type: array + items: + type: object + $ref: '#/definitions/v1Attachment' + description: Optional. The attachments of the memo. + relations: + type: array + items: + type: object + $ref: '#/definitions/v1MemoRelation' + description: Optional. The relations of the memo. + reactions: + type: array + items: + type: object + $ref: '#/definitions/v1Reaction' + description: Output only. The reactions to the memo. + readOnly: true + property: + $ref: '#/definitions/v1MemoProperty' + description: Output only. The computed properties of the memo. + readOnly: true + parent: + type: string + title: |- + Output only. The name of the parent memo. + Format: memos/{memo} + readOnly: true + snippet: + type: string + description: Output only. The snippet of the memo content. Plain text only. + readOnly: true + location: + $ref: '#/definitions/apiv1Location' + description: Optional. The location of the memo. + required: + - state + - content + - visibility + apiv1OAuth2Config: + type: object + properties: + clientId: + type: string + clientSecret: + type: string + authUrl: + type: string + tokenUrl: + type: string + userInfoUrl: + type: string + scopes: + type: array + items: + type: string + fieldMapping: + $ref: '#/definitions/apiv1FieldMapping' + apiv1Shortcut: + type: object + properties: + name: + type: string + title: "The resource name of the shortcut.\r\nFormat: users/{user}/shortcuts/{shortcut}" + title: + type: string + description: The title of the shortcut. + filter: + type: string + description: The filter expression for the shortcut. + required: + - title + apiv1UserSetting: + type: object + properties: + name: + type: string + title: "The resource name of the user whose setting this is.\r\nFormat: users/{user}" + locale: + type: string + description: The preferred locale of the user. + appearance: + type: string + description: The preferred appearance of the user. + memoVisibility: + type: string + description: The default visibility of the memo. + theme: + type: string + description: "The preferred theme of the user.\r\nThis references a CSS file in the web/public/themes/ directory.\r\nIf not set, the default theme will be used." + title: User settings message + apiv1Webhook: + type: object + properties: + name: + type: string + title: "The resource name of the webhook.\r\nFormat: users/{user}/webhooks/{webhook}" + displayName: + type: string + description: The display name of the webhook. + url: + type: string + description: The target URL for the webhook. + required: + - displayName + - url + apiv1WorkspaceCustomProfile: + type: object + properties: + title: + type: string + description: + type: string + logoUrl: + type: string + locale: + type: string + appearance: + type: string + apiv1WorkspaceGeneralSetting: + type: object + properties: + theme: + type: string + description: "theme is the name of the selected theme.\r\nThis references a CSS file in the web/public/themes/ directory." + disallowUserRegistration: + type: boolean + description: disallow_user_registration disallows user registration. + disallowPasswordAuth: + type: boolean + description: disallow_password_auth disallows password authentication. + additionalScript: + type: string + description: additional_script is the additional script. + additionalStyle: + type: string + description: additional_style is the additional style. + customProfile: + $ref: '#/definitions/apiv1WorkspaceCustomProfile' + description: custom_profile is the custom profile. + weekStartDayOffset: + type: integer + format: int32 + description: "week_start_day_offset is the week start day offset from Sunday.\r\n0: Sunday, 1: Monday, 2: Tuesday, 3: Wednesday, 4: Thursday, 5: Friday, 6: Saturday\r\nDefault is Sunday." + disallowChangeUsername: + type: boolean + description: disallow_change_username disallows changing username. + disallowChangeNickname: + type: boolean + description: disallow_change_nickname disallows changing nickname. + apiv1WorkspaceMemoRelatedSetting: + type: object + properties: + disallowPublicVisibility: + type: boolean + description: disallow_public_visibility disallows set memo as public visibility. + displayWithUpdateTime: + type: boolean + description: display_with_update_time orders and displays memo with update time. + contentLengthLimit: + type: integer + format: int32 + description: content_length_limit is the limit of content length. Unit is byte. + enableDoubleClickEdit: + type: boolean + description: enable_double_click_edit enables editing on double click. + enableLinkPreview: + type: boolean + description: enable_link_preview enables links preview. + enableComment: + type: boolean + description: enable_comment enables comment. + reactions: + type: array + items: + type: string + description: reactions is the list of reactions. + disableMarkdownShortcuts: + type: boolean + description: disable_markdown_shortcuts disallow the registration of markdown shortcuts. + enableBlurNsfwContent: + type: boolean + description: enable_blur_nsfw_content enables blurring of content marked as not safe for work (NSFW). + nsfwTags: + type: array + items: + type: string + description: nsfw_tags is the list of tags that mark content as NSFW for blurring. + apiv1WorkspaceSetting: + type: object + properties: + name: + type: string + title: "The name of the workspace setting.\r\nFormat: workspace/settings/{setting}" + generalSetting: + $ref: '#/definitions/apiv1WorkspaceGeneralSetting' + storageSetting: + $ref: '#/definitions/apiv1WorkspaceStorageSetting' + memoRelatedSetting: + $ref: '#/definitions/apiv1WorkspaceMemoRelatedSetting' + description: A workspace setting resource. + apiv1WorkspaceStorageSetting: + type: object + properties: + storageType: + $ref: '#/definitions/apiv1WorkspaceStorageSettingStorageType' + description: storage_type is the storage type. + filepathTemplate: + type: string + title: "The template of file path.\r\ne.g. assets/{timestamp}_{filename}" + uploadSizeLimitMb: + type: string + format: int64 + description: The max upload size in megabytes. + s3Config: + $ref: '#/definitions/WorkspaceStorageSettingS3Config' + description: The S3 config. + apiv1WorkspaceStorageSettingStorageType: + type: string + enum: + - STORAGE_TYPE_UNSPECIFIED + - DATABASE + - LOCAL + - S3 + default: STORAGE_TYPE_UNSPECIFIED + description: |2- + - DATABASE: DATABASE is the database storage type. + - LOCAL: LOCAL is the local storage type. + - S3: S3 is the S3 storage type. + googlerpcStatus: + type: object + properties: + code: + type: integer + format: int32 + message: + type: string + details: + type: array + items: + type: object + $ref: '#/definitions/protobufAny' + protobufAny: + type: object + properties: + '@type': + type: string + description: |- + A URL/resource name that uniquely identifies the type of the serialized + protocol buffer message. This string must contain at least + one "/" character. The last segment of the URL's path must represent + the fully qualified name of the type (as in + `path/google.protobuf.Duration`). The name should be in a canonical form + (e.g., leading "." is not accepted). + + In practice, teams usually precompile into the binary all types that they + expect it to use in the context of Any. However, for URLs which use the + scheme `http`, `https`, or no scheme, one can optionally set up a type + server that maps type URLs to message definitions as follows: + + * If no scheme is provided, `https` is assumed. + * An HTTP GET on the URL must yield a [google.protobuf.Type][] + value in binary format, or produce an error. + * Applications are allowed to cache lookup results based on the + URL, or have them precompiled into a binary to avoid any + lookup. Therefore, binary compatibility needs to be preserved + on changes to types. (Use versioned type names to manage + breaking changes.) + + Note: this functionality is not currently available in the official + protobuf release, and it is not used for type URLs beginning with + type.googleapis.com. As of May 2023, there are no widely used type server + implementations and no plans to implement one. + + Schemes other than `http`, `https` (or the empty scheme) might be + used with implementation specific semantics. + additionalProperties: {} + description: |- + `Any` contains an arbitrary serialized protocol buffer message along with a + URL that describes the type of the serialized message. + + Protobuf library provides support to pack/unpack Any values in the form + of utility functions or additional generated methods of the Any type. + + Example 1: Pack and unpack a message in C++. + + Foo foo = ...; + Any any; + any.PackFrom(foo); + ... + if (any.UnpackTo(&foo)) { + ... + } + + Example 2: Pack and unpack a message in Java. + + Foo foo = ...; + Any any = Any.pack(foo); + ... + if (any.is(Foo.class)) { + foo = any.unpack(Foo.class); + } + // or ... + if (any.isSameTypeAs(Foo.getDefaultInstance())) { + foo = any.unpack(Foo.getDefaultInstance()); + } + + Example 3: Pack and unpack a message in Python. + + foo = Foo(...) + any = Any() + any.Pack(foo) + ... + if any.Is(Foo.DESCRIPTOR): + any.Unpack(foo) + ... + + Example 4: Pack and unpack a message in Go + + foo := &pb.Foo{...} + any, err := anypb.New(foo) + if err != nil { + ... + } + ... + foo := &pb.Foo{} + if err := any.UnmarshalTo(foo); err != nil { + ... + } + + The pack methods provided by protobuf library will by default use + 'type.googleapis.com/full.type.name' as the type URL and the unpack + methods only use the fully qualified type name after the last '/' + in the type URL, for example "foo.bar.com/x/y.z" will yield type + name "y.z". + + JSON + ==== + The JSON representation of an `Any` value uses the regular + representation of the deserialized, embedded message, with an + additional field `@type` which contains the type URL. Example: + + package google.profile; + message Person { + string first_name = 1; + string last_name = 2; + } + + { + "@type": "type.googleapis.com/google.profile.Person", + "firstName": , + "lastName": + } + + If the embedded message type is well-known and has a custom JSON + representation, that representation will be embedded adding a field + `value` which holds the custom JSON in addition to the `@type` + field. Example (for message [google.protobuf.Duration][]): + + { + "@type": "type.googleapis.com/google.protobuf.Duration", + "value": "1.212s" + } + v1Activity: + type: object + properties: + name: + type: string + title: "The name of the activity.\r\nFormat: activities/{id}" + readOnly: true + creator: + type: string + title: "The name of the creator.\r\nFormat: users/{user}" + readOnly: true + type: + $ref: '#/definitions/v1ActivityType' + description: The type of the activity. + readOnly: true + level: + $ref: '#/definitions/ActivityLevel' + description: The level of the activity. + readOnly: true + createTime: + type: string + format: date-time + description: The create time of the activity. + readOnly: true + payload: + $ref: '#/definitions/apiv1ActivityPayload' + description: The payload of the activity. + readOnly: true + v1ActivityType: + type: string + enum: + - TYPE_UNSPECIFIED + - MEMO_COMMENT + - VERSION_UPDATE + default: TYPE_UNSPECIFIED + description: |- + Activity types. + + - TYPE_UNSPECIFIED: Unspecified type. + - MEMO_COMMENT: Memo comment activity. + - VERSION_UPDATE: Version update activity. + v1Attachment: + type: object + properties: + name: + type: string + title: "The name of the attachment.\r\nFormat: attachments/{attachment}" + createTime: + type: string + format: date-time + description: Output only. The creation timestamp. + readOnly: true + filename: + type: string + description: The filename of the attachment. + content: + type: string + format: byte + description: Input only. The content of the attachment. + externalLink: + type: string + description: Optional. The external link of the attachment. + type: + type: string + description: The MIME type of the attachment. + size: + type: string + format: int64 + description: Output only. The size of the attachment in bytes. + readOnly: true + memo: + type: string + title: "Optional. The related memo. Refer to `Memo.name`.\r\nFormat: memos/{memo}" + required: + - filename + - type + v1AutoLinkNode: + type: object + properties: + url: + type: string + isRawText: + type: boolean + v1BlockquoteNode: + type: object + properties: + children: + type: array + items: + type: object + $ref: '#/definitions/v1Node' + v1BoldItalicNode: + type: object + properties: + symbol: + type: string + content: + type: string + v1BoldNode: + type: object + properties: + symbol: + type: string + children: + type: array + items: + type: object + $ref: '#/definitions/v1Node' + v1CodeBlockNode: + type: object + properties: + language: + type: string + content: + type: string + v1CodeNode: + type: object + properties: + content: + type: string + v1CreateSessionRequest: + type: object + properties: + passwordCredentials: + $ref: '#/definitions/CreateSessionRequestPasswordCredentials' + description: Username and password authentication method. + ssoCredentials: + $ref: '#/definitions/CreateSessionRequestSSOCredentials' + description: SSO provider authentication method. + v1CreateSessionResponse: + type: object + properties: + user: + $ref: '#/definitions/v1User' + description: The authenticated user information. + lastAccessedAt: + type: string + format: date-time + description: "Last time the session was accessed.\r\nUsed for sliding expiration calculation (last_accessed_time + 2 weeks)." + v1EmbeddedContentNode: + type: object + properties: + resourceName: + type: string + description: The resource name of the embedded content. + params: + type: string + description: Additional parameters for the embedded content. + v1EscapingCharacterNode: + type: object + properties: + symbol: + type: string + v1ExportMemosRequest: + type: object + properties: + format: + type: string + title: Optional. Format for the export (currently only "json" is supported) + filter: + type: string + title: |- + Optional. Filter to apply to memos for export + Uses the same filter format as ListMemosRequest + excludeArchived: + type: boolean + title: |- + Optional. Whether to exclude archived memos from export + Default: false (include archived memos) + includeAttachments: + type: boolean + title: |- + Optional. Whether to include attachments in the export + Default: true + includeRelations: + type: boolean + title: |- + Optional. Whether to include memo relations in the export + Default: true + v1ExportMemosResponse: + type: object + properties: + data: + type: string + format: byte + title: The exported data as bytes + format: + type: string + title: The format of the exported data + filename: + type: string + title: Suggested filename for the export + memoCount: + type: integer + format: int32 + title: Number of memos exported + sizeBytes: + type: string + format: int64 + title: Size of the export data in bytes + v1GetCurrentSessionResponse: + type: object + properties: + user: + $ref: '#/definitions/v1User' + lastAccessedAt: + type: string + format: date-time + description: "Last time the session was accessed.\r\nUsed for sliding expiration calculation (last_accessed_time + 2 weeks)." + v1HTMLElementNode: + type: object + properties: + tagName: + type: string + attributes: + type: object + additionalProperties: + type: string + v1HeadingNode: + type: object + properties: + level: + type: integer + format: int32 + children: + type: array + items: + type: object + $ref: '#/definitions/v1Node' + v1HighlightNode: + type: object + properties: + content: + type: string + v1HorizontalRuleNode: + type: object + properties: + symbol: + type: string + v1ImageNode: + type: object + properties: + altText: + type: string + url: + type: string + v1ImportMemosRequest: + type: object + properties: + data: + type: string + format: byte + title: Required. The data to import (JSON format) + format: + type: string + title: Optional. Format of the import data (currently only "json" is supported) + overwriteExisting: + type: boolean + title: |- + Optional. Whether to overwrite existing memos with the same UID + Default: false (skip existing memos) + validateOnly: + type: boolean + title: |- + Optional. Whether to validate only (dry run mode) + If true, the import will be validated but no data will be created + preserveTimestamps: + type: boolean + title: |- + Optional. Whether to preserve original timestamps + Default: true + skipAttachments: + type: boolean + title: |- + Optional. Whether to skip importing attachments + Default: false (import attachments if present) + skipRelations: + type: boolean + title: |- + Optional. Whether to skip importing memo relations + Default: false (import relations if present) + required: + - data + v1ImportMemosResponse: + type: object + properties: + importedCount: + type: integer + format: int32 + title: Number of memos successfully imported + skippedCount: + type: integer + format: int32 + title: Number of memos skipped (due to errors or existing UIDs) + validationErrors: + type: integer + format: int32 + title: Number of memos that failed validation (in validate_only mode) + errors: + type: array + items: + type: string + title: List of error messages for failed imports + warnings: + type: array + items: + type: string + title: List of warning messages for potential issues + summary: + $ref: '#/definitions/v1ImportSummary' + title: Summary of the import operation + v1ImportSummary: + type: object + properties: + totalMemos: + type: integer + format: int32 + title: Total number of memos in the import data + createdCount: + type: integer + format: int32 + title: Number of new memos created + updatedCount: + type: integer + format: int32 + title: Number of existing memos updated + attachmentsImported: + type: integer + format: int32 + title: Number of attachments imported + relationsImported: + type: integer + format: int32 + title: Number of relations imported + durationMs: + type: string + format: int64 + title: Import duration in milliseconds + v1Inbox: + type: object + properties: + name: + type: string + title: "The resource name of the inbox.\r\nFormat: inboxes/{inbox}" + sender: + type: string + title: "The sender of the inbox notification.\r\nFormat: users/{user}" + readOnly: true + receiver: + type: string + title: "The receiver of the inbox notification.\r\nFormat: users/{user}" + readOnly: true + status: + $ref: '#/definitions/v1InboxStatus' + description: The status of the inbox notification. + createTime: + type: string + format: date-time + description: Output only. The creation timestamp. + readOnly: true + type: + $ref: '#/definitions/v1InboxType' + description: The type of the inbox notification. + readOnly: true + activityId: + type: integer + format: int32 + description: Optional. The activity ID associated with this inbox notification. + v1InboxStatus: + type: string + enum: + - STATUS_UNSPECIFIED + - UNREAD + - ARCHIVED + default: STATUS_UNSPECIFIED + description: |- + Status enumeration for inbox notifications. + + - STATUS_UNSPECIFIED: Unspecified status. + - UNREAD: The notification is unread. + - ARCHIVED: The notification is archived. + v1InboxType: + type: string + enum: + - TYPE_UNSPECIFIED + - MEMO_COMMENT + - VERSION_UPDATE + default: TYPE_UNSPECIFIED + description: |- + Type enumeration for inbox notifications. + + - TYPE_UNSPECIFIED: Unspecified type. + - MEMO_COMMENT: Memo comment notification. + - VERSION_UPDATE: Version update notification. + v1ItalicNode: + type: object + properties: + symbol: + type: string + children: + type: array + items: + type: object + $ref: '#/definitions/v1Node' + v1LineBreakNode: + type: object + v1LinkMetadata: + type: object + properties: + title: + type: string + description: The title of the linked page. + description: + type: string + description: The description of the linked page. + image: + type: string + description: The URL of the preview image for the linked page. + v1LinkNode: + type: object + properties: + content: + type: array + items: + type: object + $ref: '#/definitions/v1Node' + url: + type: string + v1ListActivitiesResponse: + type: object + properties: + activities: + type: array + items: + type: object + $ref: '#/definitions/v1Activity' + description: The activities. + nextPageToken: + type: string + description: "A token to retrieve the next page of results.\r\nPass this value in the page_token field in the subsequent call to `ListActivities`\r\nmethod to retrieve the next page of results." + v1ListAllUserStatsResponse: + type: object + properties: + userStats: + type: array + items: + type: object + $ref: '#/definitions/v1UserStats' + description: The list of user statistics. + nextPageToken: + type: string + description: A token for the next page of results. + totalSize: + type: integer + format: int32 + description: The total count of user statistics. + v1ListAttachmentsResponse: + type: object + properties: + attachments: + type: array + items: + type: object + $ref: '#/definitions/v1Attachment' + description: The list of attachments. + nextPageToken: + type: string + description: "A token that can be sent as `page_token` to retrieve the next page.\r\nIf this field is omitted, there are no subsequent pages." + totalSize: + type: integer + format: int32 + description: The total count of attachments (may be approximate). + v1ListIdentityProvidersResponse: + type: object + properties: + identityProviders: + type: array + items: + type: object + $ref: '#/definitions/apiv1IdentityProvider' + description: The list of identity providers. + v1ListInboxesResponse: + type: object + properties: + inboxes: + type: array + items: + type: object + $ref: '#/definitions/v1Inbox' + description: The list of inboxes. + nextPageToken: + type: string + description: "A token that can be sent as `page_token` to retrieve the next page.\r\nIf this field is omitted, there are no subsequent pages." + totalSize: + type: integer + format: int32 + description: The total count of inboxes (may be approximate). + v1ListMemoAttachmentsResponse: + type: object + properties: + attachments: + type: array + items: + type: object + $ref: '#/definitions/v1Attachment' + description: The list of attachments. + nextPageToken: + type: string + description: A token for the next page of results. + totalSize: + type: integer + format: int32 + description: The total count of attachments. + v1ListMemoCommentsResponse: + type: object + properties: + memos: + type: array + items: + type: object + $ref: '#/definitions/apiv1Memo' + description: The list of comment memos. + nextPageToken: + type: string + description: A token for the next page of results. + totalSize: + type: integer + format: int32 + description: The total count of comments. + v1ListMemoReactionsResponse: + type: object + properties: + reactions: + type: array + items: + type: object + $ref: '#/definitions/v1Reaction' + description: The list of reactions. + nextPageToken: + type: string + description: A token for the next page of results. + totalSize: + type: integer + format: int32 + description: The total count of reactions. + v1ListMemoRelationsResponse: + type: object + properties: + relations: + type: array + items: + type: object + $ref: '#/definitions/v1MemoRelation' + description: The list of relations. + nextPageToken: + type: string + description: A token for the next page of results. + totalSize: + type: integer + format: int32 + description: The total count of relations. + v1ListMemosResponse: + type: object + properties: + memos: + type: array + items: + type: object + $ref: '#/definitions/apiv1Memo' + description: The list of memos. + nextPageToken: + type: string + description: |- + A token that can be sent as `page_token` to retrieve the next page. + If this field is omitted, there are no subsequent pages. + totalSize: + type: integer + format: int32 + description: The total count of memos (may be approximate). + v1ListNode: + type: object + properties: + kind: + $ref: '#/definitions/ListNodeKind' + indent: + type: integer + format: int32 + children: + type: array + items: + type: object + $ref: '#/definitions/v1Node' + v1ListShortcutsResponse: + type: object + properties: + shortcuts: + type: array + items: + type: object + $ref: '#/definitions/apiv1Shortcut' + description: The list of shortcuts. + v1ListUserAccessTokensResponse: + type: object + properties: + accessTokens: + type: array + items: + type: object + $ref: '#/definitions/v1UserAccessToken' + description: The list of access tokens. + nextPageToken: + type: string + description: A token for the next page of results. + totalSize: + type: integer + format: int32 + description: The total count of access tokens. + v1ListUserSessionsResponse: + type: object + properties: + sessions: + type: array + items: + type: object + $ref: '#/definitions/v1UserSession' + description: The list of user sessions. + v1ListUsersResponse: + type: object + properties: + users: + type: array + items: + type: object + $ref: '#/definitions/v1User' + description: The list of users. + nextPageToken: + type: string + description: "A token that can be sent as `page_token` to retrieve the next page.\r\nIf this field is omitted, there are no subsequent pages." + totalSize: + type: integer + format: int32 + description: The total count of users (may be approximate). + v1ListWebhooksResponse: + type: object + properties: + webhooks: + type: array + items: + type: object + $ref: '#/definitions/apiv1Webhook' + description: The list of webhooks. + v1MathBlockNode: + type: object + properties: + content: + type: string + v1MathNode: + type: object + properties: + content: + type: string + v1MemoProperty: + type: object + properties: + hasLink: + type: boolean + hasTaskList: + type: boolean + hasCode: + type: boolean + hasIncompleteTasks: + type: boolean + description: Computed properties of a memo. + v1MemoRelation: + type: object + properties: + memo: + $ref: '#/definitions/v1MemoRelationMemo' + description: The memo in the relation. + relatedMemo: + $ref: '#/definitions/v1MemoRelationMemo' + description: The related memo. + type: + $ref: '#/definitions/v1MemoRelationType' + required: + - memo + - relatedMemo + - type + v1MemoRelationMemo: + type: object + properties: + name: + type: string + title: |- + The resource name of the memo. + Format: memos/{memo} + snippet: + type: string + description: Output only. The snippet of the memo content. Plain text only. + readOnly: true + description: Memo reference in relations. + required: + - name + v1MemoRelationType: + type: string + enum: + - TYPE_UNSPECIFIED + - REFERENCE + - COMMENT + default: TYPE_UNSPECIFIED + description: The type of the relation. + v1Node: + type: object + properties: + type: + $ref: '#/definitions/v1NodeType' + lineBreakNode: + $ref: '#/definitions/v1LineBreakNode' + description: Block nodes. + paragraphNode: + $ref: '#/definitions/v1ParagraphNode' + codeBlockNode: + $ref: '#/definitions/v1CodeBlockNode' + headingNode: + $ref: '#/definitions/v1HeadingNode' + horizontalRuleNode: + $ref: '#/definitions/v1HorizontalRuleNode' + blockquoteNode: + $ref: '#/definitions/v1BlockquoteNode' + listNode: + $ref: '#/definitions/v1ListNode' + orderedListItemNode: + $ref: '#/definitions/v1OrderedListItemNode' + unorderedListItemNode: + $ref: '#/definitions/v1UnorderedListItemNode' + taskListItemNode: + $ref: '#/definitions/v1TaskListItemNode' + mathBlockNode: + $ref: '#/definitions/v1MathBlockNode' + tableNode: + $ref: '#/definitions/v1TableNode' + embeddedContentNode: + $ref: '#/definitions/v1EmbeddedContentNode' + textNode: + $ref: '#/definitions/v1TextNode' + description: Inline nodes. + boldNode: + $ref: '#/definitions/v1BoldNode' + italicNode: + $ref: '#/definitions/v1ItalicNode' + boldItalicNode: + $ref: '#/definitions/v1BoldItalicNode' + codeNode: + $ref: '#/definitions/v1CodeNode' + imageNode: + $ref: '#/definitions/v1ImageNode' + linkNode: + $ref: '#/definitions/v1LinkNode' + autoLinkNode: + $ref: '#/definitions/v1AutoLinkNode' + tagNode: + $ref: '#/definitions/v1TagNode' + strikethroughNode: + $ref: '#/definitions/v1StrikethroughNode' + escapingCharacterNode: + $ref: '#/definitions/v1EscapingCharacterNode' + mathNode: + $ref: '#/definitions/v1MathNode' + highlightNode: + $ref: '#/definitions/v1HighlightNode' + subscriptNode: + $ref: '#/definitions/v1SubscriptNode' + superscriptNode: + $ref: '#/definitions/v1SuperscriptNode' + referencedContentNode: + $ref: '#/definitions/v1ReferencedContentNode' + spoilerNode: + $ref: '#/definitions/v1SpoilerNode' + htmlElementNode: + $ref: '#/definitions/v1HTMLElementNode' + v1NodeType: + type: string + enum: + - NODE_UNSPECIFIED + - LINE_BREAK + - PARAGRAPH + - CODE_BLOCK + - HEADING + - HORIZONTAL_RULE + - BLOCKQUOTE + - LIST + - ORDERED_LIST_ITEM + - UNORDERED_LIST_ITEM + - TASK_LIST_ITEM + - MATH_BLOCK + - TABLE + - EMBEDDED_CONTENT + - TEXT + - BOLD + - ITALIC + - BOLD_ITALIC + - CODE + - IMAGE + - LINK + - AUTO_LINK + - TAG + - STRIKETHROUGH + - ESCAPING_CHARACTER + - MATH + - HIGHLIGHT + - SUBSCRIPT + - SUPERSCRIPT + - REFERENCED_CONTENT + - SPOILER + - HTML_ELEMENT + default: NODE_UNSPECIFIED + description: |2- + - LINE_BREAK: Block nodes. + - TEXT: Inline nodes. + v1OrderedListItemNode: + type: object + properties: + number: + type: string + indent: + type: integer + format: int32 + children: + type: array + items: + type: object + $ref: '#/definitions/v1Node' + v1ParagraphNode: + type: object + properties: + children: + type: array + items: + type: object + $ref: '#/definitions/v1Node' + v1ParseMarkdownRequest: + type: object + properties: + markdown: + type: string + description: The markdown content to parse. + required: + - markdown + v1ParseMarkdownResponse: + type: object + properties: + nodes: + type: array + items: + type: object + $ref: '#/definitions/v1Node' + description: The parsed markdown nodes. + v1Reaction: + type: object + properties: + name: + type: string + title: |- + The resource name of the reaction. + Format: reactions/{reaction} + readOnly: true + creator: + type: string + title: |- + The resource name of the creator. + Format: users/{user} + readOnly: true + contentId: + type: string + title: |- + The resource name of the content. + For memo reactions, this should be the memo's resource name. + Format: memos/{memo} + reactionType: + type: string + description: "Required. The type of reaction (e.g., \"\U0001F44D\", \"❤️\", \"\U0001F604\")." + createTime: + type: string + format: date-time + description: Output only. The creation timestamp. + readOnly: true + required: + - contentId + - reactionType + v1ReferencedContentNode: + type: object + properties: + resourceName: + type: string + description: The resource name of the referenced content. + params: + type: string + description: Additional parameters for the referenced content. + v1RestoreMarkdownNodesRequest: + type: object + properties: + nodes: + type: array + items: + type: object + $ref: '#/definitions/v1Node' + description: The nodes to restore to markdown content. + required: + - nodes + v1RestoreMarkdownNodesResponse: + type: object + properties: + markdown: + type: string + description: The restored markdown content. + v1SearchUsersResponse: + type: object + properties: + users: + type: array + items: + type: object + $ref: '#/definitions/v1User' + description: The list of users matching the search query. + nextPageToken: + type: string + description: A token for the next page of results. + totalSize: + type: integer + format: int32 + description: The total count of matching users. + v1SpoilerNode: + type: object + properties: + content: + type: string + v1State: + type: string + enum: + - STATE_UNSPECIFIED + - NORMAL + - ARCHIVED + default: STATE_UNSPECIFIED + v1StrikethroughNode: + type: object + properties: + content: + type: string + v1StringifyMarkdownNodesRequest: + type: object + properties: + nodes: + type: array + items: + type: object + $ref: '#/definitions/v1Node' + description: The nodes to stringify to plain text. + required: + - nodes + v1StringifyMarkdownNodesResponse: + type: object + properties: + plainText: + type: string + description: The plain text content. + v1SubscriptNode: + type: object + properties: + content: + type: string + v1SuperscriptNode: + type: object + properties: + content: + type: string + v1TableNode: + type: object + properties: + header: + type: array + items: + type: object + $ref: '#/definitions/v1Node' + delimiter: + type: array + items: + type: string + rows: + type: array + items: + type: object + $ref: '#/definitions/TableNodeRow' + v1TagNode: + type: object + properties: + content: + type: string + v1TaskListItemNode: + type: object + properties: + symbol: + type: string + indent: + type: integer + format: int32 + complete: + type: boolean + children: + type: array + items: + type: object + $ref: '#/definitions/v1Node' + v1TextNode: + type: object + properties: + content: + type: string + v1UnorderedListItemNode: + type: object + properties: + symbol: + type: string + indent: + type: integer + format: int32 + children: + type: array + items: + type: object + $ref: '#/definitions/v1Node' + v1User: + type: object + properties: + name: + type: string + title: "The resource name of the user.\r\nFormat: users/{user}" + role: + $ref: '#/definitions/UserRole' + description: The role of the user. + username: + type: string + description: Required. The unique username for login. + email: + type: string + description: Optional. The email address of the user. + displayName: + type: string + description: Optional. The display name of the user. + avatarUrl: + type: string + description: Optional. The avatar URL of the user. + description: + type: string + description: Optional. The description of the user. + password: + type: string + description: Input only. The password for the user. + state: + $ref: '#/definitions/v1State' + description: The state of the user. + createTime: + type: string + format: date-time + description: Output only. The creation timestamp. + readOnly: true + updateTime: + type: string + format: date-time + description: Output only. The last update timestamp. + readOnly: true + required: + - role + - username + - state + v1UserAccessToken: + type: object + properties: + name: + type: string + title: "The resource name of the access token.\r\nFormat: users/{user}/accessTokens/{access_token}" + accessToken: + type: string + description: Output only. The access token value. + readOnly: true + description: + type: string + description: The description of the access token. + issuedAt: + type: string + format: date-time + description: Output only. The issued timestamp. + readOnly: true + expiresAt: + type: string + format: date-time + description: Optional. The expiration timestamp. + title: User access token message + v1UserSession: + type: object + properties: + name: + type: string + title: "The resource name of the session.\r\nFormat: users/{user}/sessions/{session}" + sessionId: + type: string + description: The session ID. + readOnly: true + createTime: + type: string + format: date-time + description: The timestamp when the session was created. + readOnly: true + lastAccessedTime: + type: string + format: date-time + description: "The timestamp when the session was last accessed.\r\nUsed for sliding expiration calculation (last_accessed_time + 2 weeks)." + readOnly: true + clientInfo: + $ref: '#/definitions/v1UserSessionClientInfo' + description: Client information associated with this session. + readOnly: true + v1UserSessionClientInfo: + type: object + properties: + userAgent: + type: string + description: User agent string of the client. + ipAddress: + type: string + description: IP address of the client. + deviceType: + type: string + description: Optional. Device type (e.g., "mobile", "desktop", "tablet"). + os: + type: string + description: Optional. Operating system (e.g., "iOS 17.0", "Windows 11"). + browser: + type: string + description: Optional. Browser name and version (e.g., "Chrome 119.0"). + v1UserStats: + type: object + properties: + name: + type: string + title: "The resource name of the user whose stats these are.\r\nFormat: users/{user}" + memoDisplayTimestamps: + type: array + items: + type: string + format: date-time + description: The timestamps when the memos were displayed. + memoTypeStats: + $ref: '#/definitions/UserStatsMemoTypeStats' + description: The stats of memo types. + tagCount: + type: object + additionalProperties: + type: integer + format: int32 + description: The count of tags. + pinnedMemos: + type: array + items: + type: string + description: The pinned memos of the user. + totalMemoCount: + type: integer + format: int32 + description: Total memo count. + title: User statistics messages + v1Visibility: + type: string + enum: + - VISIBILITY_UNSPECIFIED + - PRIVATE + - PROTECTED + - PUBLIC + default: VISIBILITY_UNSPECIFIED + v1WorkspaceProfile: + type: object + properties: + owner: + type: string + title: "The name of instance owner.\r\nFormat: users/{user}" + version: + type: string + description: Version is the current version of instance. + mode: + type: string + description: Mode is the instance mode (e.g. "prod", "dev" or "demo"). + instanceUrl: + type: string + description: Instance URL is the URL of the instance. + description: Workspace profile message containing basic workspace information. diff --git a/proto/gen/store/activity.pb.go b/proto/gen/store/activity.pb.go new file mode 100644 index 0000000..6c1d9be --- /dev/null +++ b/proto/gen/store/activity.pb.go @@ -0,0 +1,180 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: store/activity.proto + +package store + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type ActivityMemoCommentPayload struct { + state protoimpl.MessageState `protogen:"open.v1"` + MemoId int32 `protobuf:"varint,1,opt,name=memo_id,json=memoId,proto3" json:"memo_id,omitempty"` + RelatedMemoId int32 `protobuf:"varint,2,opt,name=related_memo_id,json=relatedMemoId,proto3" json:"related_memo_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ActivityMemoCommentPayload) Reset() { + *x = ActivityMemoCommentPayload{} + mi := &file_store_activity_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ActivityMemoCommentPayload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ActivityMemoCommentPayload) ProtoMessage() {} + +func (x *ActivityMemoCommentPayload) ProtoReflect() protoreflect.Message { + mi := &file_store_activity_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ActivityMemoCommentPayload.ProtoReflect.Descriptor instead. +func (*ActivityMemoCommentPayload) Descriptor() ([]byte, []int) { + return file_store_activity_proto_rawDescGZIP(), []int{0} +} + +func (x *ActivityMemoCommentPayload) GetMemoId() int32 { + if x != nil { + return x.MemoId + } + return 0 +} + +func (x *ActivityMemoCommentPayload) GetRelatedMemoId() int32 { + if x != nil { + return x.RelatedMemoId + } + return 0 +} + +type ActivityPayload struct { + state protoimpl.MessageState `protogen:"open.v1"` + MemoComment *ActivityMemoCommentPayload `protobuf:"bytes,1,opt,name=memo_comment,json=memoComment,proto3" json:"memo_comment,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ActivityPayload) Reset() { + *x = ActivityPayload{} + mi := &file_store_activity_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ActivityPayload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ActivityPayload) ProtoMessage() {} + +func (x *ActivityPayload) ProtoReflect() protoreflect.Message { + mi := &file_store_activity_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ActivityPayload.ProtoReflect.Descriptor instead. +func (*ActivityPayload) Descriptor() ([]byte, []int) { + return file_store_activity_proto_rawDescGZIP(), []int{1} +} + +func (x *ActivityPayload) GetMemoComment() *ActivityMemoCommentPayload { + if x != nil { + return x.MemoComment + } + return nil +} + +var File_store_activity_proto protoreflect.FileDescriptor + +const file_store_activity_proto_rawDesc = "" + + "\n" + + "\x14store/activity.proto\x12\vmemos.store\"]\n" + + "\x1aActivityMemoCommentPayload\x12\x17\n" + + "\amemo_id\x18\x01 \x01(\x05R\x06memoId\x12&\n" + + "\x0frelated_memo_id\x18\x02 \x01(\x05R\rrelatedMemoId\"]\n" + + "\x0fActivityPayload\x12J\n" + + "\fmemo_comment\x18\x01 \x01(\v2'.memos.store.ActivityMemoCommentPayloadR\vmemoCommentB\x98\x01\n" + + "\x0fcom.memos.storeB\rActivityProtoP\x01Z)github.com/usememos/memos/proto/gen/store\xa2\x02\x03MSX\xaa\x02\vMemos.Store\xca\x02\vMemos\\Store\xe2\x02\x17Memos\\Store\\GPBMetadata\xea\x02\fMemos::Storeb\x06proto3" + +var ( + file_store_activity_proto_rawDescOnce sync.Once + file_store_activity_proto_rawDescData []byte +) + +func file_store_activity_proto_rawDescGZIP() []byte { + file_store_activity_proto_rawDescOnce.Do(func() { + file_store_activity_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_store_activity_proto_rawDesc), len(file_store_activity_proto_rawDesc))) + }) + return file_store_activity_proto_rawDescData +} + +var file_store_activity_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_store_activity_proto_goTypes = []any{ + (*ActivityMemoCommentPayload)(nil), // 0: memos.store.ActivityMemoCommentPayload + (*ActivityPayload)(nil), // 1: memos.store.ActivityPayload +} +var file_store_activity_proto_depIdxs = []int32{ + 0, // 0: memos.store.ActivityPayload.memo_comment:type_name -> memos.store.ActivityMemoCommentPayload + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_store_activity_proto_init() } +func file_store_activity_proto_init() { + if File_store_activity_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_store_activity_proto_rawDesc), len(file_store_activity_proto_rawDesc)), + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_store_activity_proto_goTypes, + DependencyIndexes: file_store_activity_proto_depIdxs, + MessageInfos: file_store_activity_proto_msgTypes, + }.Build() + File_store_activity_proto = out.File + file_store_activity_proto_goTypes = nil + file_store_activity_proto_depIdxs = nil +} diff --git a/proto/gen/store/attachment.pb.go b/proto/gen/store/attachment.pb.go new file mode 100644 index 0000000..1b70d40 --- /dev/null +++ b/proto/gen/store/attachment.pb.go @@ -0,0 +1,287 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: store/attachment.proto + +package store + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type AttachmentStorageType int32 + +const ( + AttachmentStorageType_ATTACHMENT_STORAGE_TYPE_UNSPECIFIED AttachmentStorageType = 0 + // Attachment is stored locally. AKA, local file system. + AttachmentStorageType_LOCAL AttachmentStorageType = 1 + // Attachment is stored in S3. + AttachmentStorageType_S3 AttachmentStorageType = 2 + // Attachment is stored in an external storage. The reference is a URL. + AttachmentStorageType_EXTERNAL AttachmentStorageType = 3 +) + +// Enum value maps for AttachmentStorageType. +var ( + AttachmentStorageType_name = map[int32]string{ + 0: "ATTACHMENT_STORAGE_TYPE_UNSPECIFIED", + 1: "LOCAL", + 2: "S3", + 3: "EXTERNAL", + } + AttachmentStorageType_value = map[string]int32{ + "ATTACHMENT_STORAGE_TYPE_UNSPECIFIED": 0, + "LOCAL": 1, + "S3": 2, + "EXTERNAL": 3, + } +) + +func (x AttachmentStorageType) Enum() *AttachmentStorageType { + p := new(AttachmentStorageType) + *p = x + return p +} + +func (x AttachmentStorageType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (AttachmentStorageType) Descriptor() protoreflect.EnumDescriptor { + return file_store_attachment_proto_enumTypes[0].Descriptor() +} + +func (AttachmentStorageType) Type() protoreflect.EnumType { + return &file_store_attachment_proto_enumTypes[0] +} + +func (x AttachmentStorageType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use AttachmentStorageType.Descriptor instead. +func (AttachmentStorageType) EnumDescriptor() ([]byte, []int) { + return file_store_attachment_proto_rawDescGZIP(), []int{0} +} + +type AttachmentPayload struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Payload: + // + // *AttachmentPayload_S3Object_ + Payload isAttachmentPayload_Payload `protobuf_oneof:"payload"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AttachmentPayload) Reset() { + *x = AttachmentPayload{} + mi := &file_store_attachment_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AttachmentPayload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AttachmentPayload) ProtoMessage() {} + +func (x *AttachmentPayload) ProtoReflect() protoreflect.Message { + mi := &file_store_attachment_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AttachmentPayload.ProtoReflect.Descriptor instead. +func (*AttachmentPayload) Descriptor() ([]byte, []int) { + return file_store_attachment_proto_rawDescGZIP(), []int{0} +} + +func (x *AttachmentPayload) GetPayload() isAttachmentPayload_Payload { + if x != nil { + return x.Payload + } + return nil +} + +func (x *AttachmentPayload) GetS3Object() *AttachmentPayload_S3Object { + if x != nil { + if x, ok := x.Payload.(*AttachmentPayload_S3Object_); ok { + return x.S3Object + } + } + return nil +} + +type isAttachmentPayload_Payload interface { + isAttachmentPayload_Payload() +} + +type AttachmentPayload_S3Object_ struct { + S3Object *AttachmentPayload_S3Object `protobuf:"bytes,1,opt,name=s3_object,json=s3Object,proto3,oneof"` +} + +func (*AttachmentPayload_S3Object_) isAttachmentPayload_Payload() {} + +type AttachmentPayload_S3Object struct { + state protoimpl.MessageState `protogen:"open.v1"` + S3Config *StorageS3Config `protobuf:"bytes,1,opt,name=s3_config,json=s3Config,proto3" json:"s3_config,omitempty"` + // key is the S3 object key. + Key string `protobuf:"bytes,2,opt,name=key,proto3" json:"key,omitempty"` + // last_presigned_time is the last time the object was presigned. + // This is used to determine if the presigned URL is still valid. + LastPresignedTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=last_presigned_time,json=lastPresignedTime,proto3" json:"last_presigned_time,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AttachmentPayload_S3Object) Reset() { + *x = AttachmentPayload_S3Object{} + mi := &file_store_attachment_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AttachmentPayload_S3Object) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AttachmentPayload_S3Object) ProtoMessage() {} + +func (x *AttachmentPayload_S3Object) ProtoReflect() protoreflect.Message { + mi := &file_store_attachment_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AttachmentPayload_S3Object.ProtoReflect.Descriptor instead. +func (*AttachmentPayload_S3Object) Descriptor() ([]byte, []int) { + return file_store_attachment_proto_rawDescGZIP(), []int{0, 0} +} + +func (x *AttachmentPayload_S3Object) GetS3Config() *StorageS3Config { + if x != nil { + return x.S3Config + } + return nil +} + +func (x *AttachmentPayload_S3Object) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *AttachmentPayload_S3Object) GetLastPresignedTime() *timestamppb.Timestamp { + if x != nil { + return x.LastPresignedTime + } + return nil +} + +var File_store_attachment_proto protoreflect.FileDescriptor + +const file_store_attachment_proto_rawDesc = "" + + "\n" + + "\x16store/attachment.proto\x12\vmemos.store\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1dstore/workspace_setting.proto\"\x8c\x02\n" + + "\x11AttachmentPayload\x12F\n" + + "\ts3_object\x18\x01 \x01(\v2'.memos.store.AttachmentPayload.S3ObjectH\x00R\bs3Object\x1a\xa3\x01\n" + + "\bS3Object\x129\n" + + "\ts3_config\x18\x01 \x01(\v2\x1c.memos.store.StorageS3ConfigR\bs3Config\x12\x10\n" + + "\x03key\x18\x02 \x01(\tR\x03key\x12J\n" + + "\x13last_presigned_time\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x11lastPresignedTimeB\t\n" + + "\apayload*a\n" + + "\x15AttachmentStorageType\x12'\n" + + "#ATTACHMENT_STORAGE_TYPE_UNSPECIFIED\x10\x00\x12\t\n" + + "\x05LOCAL\x10\x01\x12\x06\n" + + "\x02S3\x10\x02\x12\f\n" + + "\bEXTERNAL\x10\x03B\x9a\x01\n" + + "\x0fcom.memos.storeB\x0fAttachmentProtoP\x01Z)github.com/usememos/memos/proto/gen/store\xa2\x02\x03MSX\xaa\x02\vMemos.Store\xca\x02\vMemos\\Store\xe2\x02\x17Memos\\Store\\GPBMetadata\xea\x02\fMemos::Storeb\x06proto3" + +var ( + file_store_attachment_proto_rawDescOnce sync.Once + file_store_attachment_proto_rawDescData []byte +) + +func file_store_attachment_proto_rawDescGZIP() []byte { + file_store_attachment_proto_rawDescOnce.Do(func() { + file_store_attachment_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_store_attachment_proto_rawDesc), len(file_store_attachment_proto_rawDesc))) + }) + return file_store_attachment_proto_rawDescData +} + +var file_store_attachment_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_store_attachment_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_store_attachment_proto_goTypes = []any{ + (AttachmentStorageType)(0), // 0: memos.store.AttachmentStorageType + (*AttachmentPayload)(nil), // 1: memos.store.AttachmentPayload + (*AttachmentPayload_S3Object)(nil), // 2: memos.store.AttachmentPayload.S3Object + (*StorageS3Config)(nil), // 3: memos.store.StorageS3Config + (*timestamppb.Timestamp)(nil), // 4: google.protobuf.Timestamp +} +var file_store_attachment_proto_depIdxs = []int32{ + 2, // 0: memos.store.AttachmentPayload.s3_object:type_name -> memos.store.AttachmentPayload.S3Object + 3, // 1: memos.store.AttachmentPayload.S3Object.s3_config:type_name -> memos.store.StorageS3Config + 4, // 2: memos.store.AttachmentPayload.S3Object.last_presigned_time:type_name -> google.protobuf.Timestamp + 3, // [3:3] is the sub-list for method output_type + 3, // [3:3] is the sub-list for method input_type + 3, // [3:3] is the sub-list for extension type_name + 3, // [3:3] is the sub-list for extension extendee + 0, // [0:3] is the sub-list for field type_name +} + +func init() { file_store_attachment_proto_init() } +func file_store_attachment_proto_init() { + if File_store_attachment_proto != nil { + return + } + file_store_workspace_setting_proto_init() + file_store_attachment_proto_msgTypes[0].OneofWrappers = []any{ + (*AttachmentPayload_S3Object_)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_store_attachment_proto_rawDesc), len(file_store_attachment_proto_rawDesc)), + NumEnums: 1, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_store_attachment_proto_goTypes, + DependencyIndexes: file_store_attachment_proto_depIdxs, + EnumInfos: file_store_attachment_proto_enumTypes, + MessageInfos: file_store_attachment_proto_msgTypes, + }.Build() + File_store_attachment_proto = out.File + file_store_attachment_proto_goTypes = nil + file_store_attachment_proto_depIdxs = nil +} diff --git a/proto/gen/store/idp.pb.go b/proto/gen/store/idp.pb.go new file mode 100644 index 0000000..28f5e80 --- /dev/null +++ b/proto/gen/store/idp.pb.go @@ -0,0 +1,467 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: store/idp.proto + +package store + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type IdentityProvider_Type int32 + +const ( + IdentityProvider_TYPE_UNSPECIFIED IdentityProvider_Type = 0 + IdentityProvider_OAUTH2 IdentityProvider_Type = 1 +) + +// Enum value maps for IdentityProvider_Type. +var ( + IdentityProvider_Type_name = map[int32]string{ + 0: "TYPE_UNSPECIFIED", + 1: "OAUTH2", + } + IdentityProvider_Type_value = map[string]int32{ + "TYPE_UNSPECIFIED": 0, + "OAUTH2": 1, + } +) + +func (x IdentityProvider_Type) Enum() *IdentityProvider_Type { + p := new(IdentityProvider_Type) + *p = x + return p +} + +func (x IdentityProvider_Type) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (IdentityProvider_Type) Descriptor() protoreflect.EnumDescriptor { + return file_store_idp_proto_enumTypes[0].Descriptor() +} + +func (IdentityProvider_Type) Type() protoreflect.EnumType { + return &file_store_idp_proto_enumTypes[0] +} + +func (x IdentityProvider_Type) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use IdentityProvider_Type.Descriptor instead. +func (IdentityProvider_Type) EnumDescriptor() ([]byte, []int) { + return file_store_idp_proto_rawDescGZIP(), []int{0, 0} +} + +type IdentityProvider struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Type IdentityProvider_Type `protobuf:"varint,3,opt,name=type,proto3,enum=memos.store.IdentityProvider_Type" json:"type,omitempty"` + IdentifierFilter string `protobuf:"bytes,4,opt,name=identifier_filter,json=identifierFilter,proto3" json:"identifier_filter,omitempty"` + Config *IdentityProviderConfig `protobuf:"bytes,5,opt,name=config,proto3" json:"config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IdentityProvider) Reset() { + *x = IdentityProvider{} + mi := &file_store_idp_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IdentityProvider) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IdentityProvider) ProtoMessage() {} + +func (x *IdentityProvider) ProtoReflect() protoreflect.Message { + mi := &file_store_idp_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IdentityProvider.ProtoReflect.Descriptor instead. +func (*IdentityProvider) Descriptor() ([]byte, []int) { + return file_store_idp_proto_rawDescGZIP(), []int{0} +} + +func (x *IdentityProvider) GetId() int32 { + if x != nil { + return x.Id + } + return 0 +} + +func (x *IdentityProvider) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *IdentityProvider) GetType() IdentityProvider_Type { + if x != nil { + return x.Type + } + return IdentityProvider_TYPE_UNSPECIFIED +} + +func (x *IdentityProvider) GetIdentifierFilter() string { + if x != nil { + return x.IdentifierFilter + } + return "" +} + +func (x *IdentityProvider) GetConfig() *IdentityProviderConfig { + if x != nil { + return x.Config + } + return nil +} + +type IdentityProviderConfig struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Types that are valid to be assigned to Config: + // + // *IdentityProviderConfig_Oauth2Config + Config isIdentityProviderConfig_Config `protobuf_oneof:"config"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *IdentityProviderConfig) Reset() { + *x = IdentityProviderConfig{} + mi := &file_store_idp_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *IdentityProviderConfig) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*IdentityProviderConfig) ProtoMessage() {} + +func (x *IdentityProviderConfig) ProtoReflect() protoreflect.Message { + mi := &file_store_idp_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use IdentityProviderConfig.ProtoReflect.Descriptor instead. +func (*IdentityProviderConfig) Descriptor() ([]byte, []int) { + return file_store_idp_proto_rawDescGZIP(), []int{1} +} + +func (x *IdentityProviderConfig) GetConfig() isIdentityProviderConfig_Config { + if x != nil { + return x.Config + } + return nil +} + +func (x *IdentityProviderConfig) GetOauth2Config() *OAuth2Config { + if x != nil { + if x, ok := x.Config.(*IdentityProviderConfig_Oauth2Config); ok { + return x.Oauth2Config + } + } + return nil +} + +type isIdentityProviderConfig_Config interface { + isIdentityProviderConfig_Config() +} + +type IdentityProviderConfig_Oauth2Config struct { + Oauth2Config *OAuth2Config `protobuf:"bytes,1,opt,name=oauth2_config,json=oauth2Config,proto3,oneof"` +} + +func (*IdentityProviderConfig_Oauth2Config) isIdentityProviderConfig_Config() {} + +type FieldMapping struct { + state protoimpl.MessageState `protogen:"open.v1"` + Identifier string `protobuf:"bytes,1,opt,name=identifier,proto3" json:"identifier,omitempty"` + DisplayName string `protobuf:"bytes,2,opt,name=display_name,json=displayName,proto3" json:"display_name,omitempty"` + Email string `protobuf:"bytes,3,opt,name=email,proto3" json:"email,omitempty"` + AvatarUrl string `protobuf:"bytes,4,opt,name=avatar_url,json=avatarUrl,proto3" json:"avatar_url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *FieldMapping) Reset() { + *x = FieldMapping{} + mi := &file_store_idp_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *FieldMapping) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FieldMapping) ProtoMessage() {} + +func (x *FieldMapping) ProtoReflect() protoreflect.Message { + mi := &file_store_idp_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FieldMapping.ProtoReflect.Descriptor instead. +func (*FieldMapping) Descriptor() ([]byte, []int) { + return file_store_idp_proto_rawDescGZIP(), []int{2} +} + +func (x *FieldMapping) GetIdentifier() string { + if x != nil { + return x.Identifier + } + return "" +} + +func (x *FieldMapping) GetDisplayName() string { + if x != nil { + return x.DisplayName + } + return "" +} + +func (x *FieldMapping) GetEmail() string { + if x != nil { + return x.Email + } + return "" +} + +func (x *FieldMapping) GetAvatarUrl() string { + if x != nil { + return x.AvatarUrl + } + return "" +} + +type OAuth2Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + ClientId string `protobuf:"bytes,1,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` + ClientSecret string `protobuf:"bytes,2,opt,name=client_secret,json=clientSecret,proto3" json:"client_secret,omitempty"` + AuthUrl string `protobuf:"bytes,3,opt,name=auth_url,json=authUrl,proto3" json:"auth_url,omitempty"` + TokenUrl string `protobuf:"bytes,4,opt,name=token_url,json=tokenUrl,proto3" json:"token_url,omitempty"` + UserInfoUrl string `protobuf:"bytes,5,opt,name=user_info_url,json=userInfoUrl,proto3" json:"user_info_url,omitempty"` + Scopes []string `protobuf:"bytes,6,rep,name=scopes,proto3" json:"scopes,omitempty"` + FieldMapping *FieldMapping `protobuf:"bytes,7,opt,name=field_mapping,json=fieldMapping,proto3" json:"field_mapping,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *OAuth2Config) Reset() { + *x = OAuth2Config{} + mi := &file_store_idp_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *OAuth2Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*OAuth2Config) ProtoMessage() {} + +func (x *OAuth2Config) ProtoReflect() protoreflect.Message { + mi := &file_store_idp_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use OAuth2Config.ProtoReflect.Descriptor instead. +func (*OAuth2Config) Descriptor() ([]byte, []int) { + return file_store_idp_proto_rawDescGZIP(), []int{3} +} + +func (x *OAuth2Config) GetClientId() string { + if x != nil { + return x.ClientId + } + return "" +} + +func (x *OAuth2Config) GetClientSecret() string { + if x != nil { + return x.ClientSecret + } + return "" +} + +func (x *OAuth2Config) GetAuthUrl() string { + if x != nil { + return x.AuthUrl + } + return "" +} + +func (x *OAuth2Config) GetTokenUrl() string { + if x != nil { + return x.TokenUrl + } + return "" +} + +func (x *OAuth2Config) GetUserInfoUrl() string { + if x != nil { + return x.UserInfoUrl + } + return "" +} + +func (x *OAuth2Config) GetScopes() []string { + if x != nil { + return x.Scopes + } + return nil +} + +func (x *OAuth2Config) GetFieldMapping() *FieldMapping { + if x != nil { + return x.FieldMapping + } + return nil +} + +var File_store_idp_proto protoreflect.FileDescriptor + +const file_store_idp_proto_rawDesc = "" + + "\n" + + "\x0fstore/idp.proto\x12\vmemos.store\"\x82\x02\n" + + "\x10IdentityProvider\x12\x0e\n" + + "\x02id\x18\x01 \x01(\x05R\x02id\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x126\n" + + "\x04type\x18\x03 \x01(\x0e2\".memos.store.IdentityProvider.TypeR\x04type\x12+\n" + + "\x11identifier_filter\x18\x04 \x01(\tR\x10identifierFilter\x12;\n" + + "\x06config\x18\x05 \x01(\v2#.memos.store.IdentityProviderConfigR\x06config\"(\n" + + "\x04Type\x12\x14\n" + + "\x10TYPE_UNSPECIFIED\x10\x00\x12\n" + + "\n" + + "\x06OAUTH2\x10\x01\"d\n" + + "\x16IdentityProviderConfig\x12@\n" + + "\roauth2_config\x18\x01 \x01(\v2\x19.memos.store.OAuth2ConfigH\x00R\foauth2ConfigB\b\n" + + "\x06config\"\x86\x01\n" + + "\fFieldMapping\x12\x1e\n" + + "\n" + + "identifier\x18\x01 \x01(\tR\n" + + "identifier\x12!\n" + + "\fdisplay_name\x18\x02 \x01(\tR\vdisplayName\x12\x14\n" + + "\x05email\x18\x03 \x01(\tR\x05email\x12\x1d\n" + + "\n" + + "avatar_url\x18\x04 \x01(\tR\tavatarUrl\"\x84\x02\n" + + "\fOAuth2Config\x12\x1b\n" + + "\tclient_id\x18\x01 \x01(\tR\bclientId\x12#\n" + + "\rclient_secret\x18\x02 \x01(\tR\fclientSecret\x12\x19\n" + + "\bauth_url\x18\x03 \x01(\tR\aauthUrl\x12\x1b\n" + + "\ttoken_url\x18\x04 \x01(\tR\btokenUrl\x12\"\n" + + "\ruser_info_url\x18\x05 \x01(\tR\vuserInfoUrl\x12\x16\n" + + "\x06scopes\x18\x06 \x03(\tR\x06scopes\x12>\n" + + "\rfield_mapping\x18\a \x01(\v2\x19.memos.store.FieldMappingR\ffieldMappingB\x93\x01\n" + + "\x0fcom.memos.storeB\bIdpProtoP\x01Z)github.com/usememos/memos/proto/gen/store\xa2\x02\x03MSX\xaa\x02\vMemos.Store\xca\x02\vMemos\\Store\xe2\x02\x17Memos\\Store\\GPBMetadata\xea\x02\fMemos::Storeb\x06proto3" + +var ( + file_store_idp_proto_rawDescOnce sync.Once + file_store_idp_proto_rawDescData []byte +) + +func file_store_idp_proto_rawDescGZIP() []byte { + file_store_idp_proto_rawDescOnce.Do(func() { + file_store_idp_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_store_idp_proto_rawDesc), len(file_store_idp_proto_rawDesc))) + }) + return file_store_idp_proto_rawDescData +} + +var file_store_idp_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_store_idp_proto_msgTypes = make([]protoimpl.MessageInfo, 4) +var file_store_idp_proto_goTypes = []any{ + (IdentityProvider_Type)(0), // 0: memos.store.IdentityProvider.Type + (*IdentityProvider)(nil), // 1: memos.store.IdentityProvider + (*IdentityProviderConfig)(nil), // 2: memos.store.IdentityProviderConfig + (*FieldMapping)(nil), // 3: memos.store.FieldMapping + (*OAuth2Config)(nil), // 4: memos.store.OAuth2Config +} +var file_store_idp_proto_depIdxs = []int32{ + 0, // 0: memos.store.IdentityProvider.type:type_name -> memos.store.IdentityProvider.Type + 2, // 1: memos.store.IdentityProvider.config:type_name -> memos.store.IdentityProviderConfig + 4, // 2: memos.store.IdentityProviderConfig.oauth2_config:type_name -> memos.store.OAuth2Config + 3, // 3: memos.store.OAuth2Config.field_mapping:type_name -> memos.store.FieldMapping + 4, // [4:4] is the sub-list for method output_type + 4, // [4:4] is the sub-list for method input_type + 4, // [4:4] is the sub-list for extension type_name + 4, // [4:4] is the sub-list for extension extendee + 0, // [0:4] is the sub-list for field type_name +} + +func init() { file_store_idp_proto_init() } +func file_store_idp_proto_init() { + if File_store_idp_proto != nil { + return + } + file_store_idp_proto_msgTypes[1].OneofWrappers = []any{ + (*IdentityProviderConfig_Oauth2Config)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_store_idp_proto_rawDesc), len(file_store_idp_proto_rawDesc)), + NumEnums: 1, + NumMessages: 4, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_store_idp_proto_goTypes, + DependencyIndexes: file_store_idp_proto_depIdxs, + EnumInfos: file_store_idp_proto_enumTypes, + MessageInfos: file_store_idp_proto_msgTypes, + }.Build() + File_store_idp_proto = out.File + file_store_idp_proto_goTypes = nil + file_store_idp_proto_depIdxs = nil +} diff --git a/proto/gen/store/inbox.pb.go b/proto/gen/store/inbox.pb.go new file mode 100644 index 0000000..8d4ad85 --- /dev/null +++ b/proto/gen/store/inbox.pb.go @@ -0,0 +1,193 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: store/inbox.proto + +package store + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type InboxMessage_Type int32 + +const ( + InboxMessage_TYPE_UNSPECIFIED InboxMessage_Type = 0 + InboxMessage_MEMO_COMMENT InboxMessage_Type = 1 + InboxMessage_VERSION_UPDATE InboxMessage_Type = 2 +) + +// Enum value maps for InboxMessage_Type. +var ( + InboxMessage_Type_name = map[int32]string{ + 0: "TYPE_UNSPECIFIED", + 1: "MEMO_COMMENT", + 2: "VERSION_UPDATE", + } + InboxMessage_Type_value = map[string]int32{ + "TYPE_UNSPECIFIED": 0, + "MEMO_COMMENT": 1, + "VERSION_UPDATE": 2, + } +) + +func (x InboxMessage_Type) Enum() *InboxMessage_Type { + p := new(InboxMessage_Type) + *p = x + return p +} + +func (x InboxMessage_Type) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (InboxMessage_Type) Descriptor() protoreflect.EnumDescriptor { + return file_store_inbox_proto_enumTypes[0].Descriptor() +} + +func (InboxMessage_Type) Type() protoreflect.EnumType { + return &file_store_inbox_proto_enumTypes[0] +} + +func (x InboxMessage_Type) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use InboxMessage_Type.Descriptor instead. +func (InboxMessage_Type) EnumDescriptor() ([]byte, []int) { + return file_store_inbox_proto_rawDescGZIP(), []int{0, 0} +} + +type InboxMessage struct { + state protoimpl.MessageState `protogen:"open.v1"` + Type InboxMessage_Type `protobuf:"varint,1,opt,name=type,proto3,enum=memos.store.InboxMessage_Type" json:"type,omitempty"` + ActivityId *int32 `protobuf:"varint,2,opt,name=activity_id,json=activityId,proto3,oneof" json:"activity_id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *InboxMessage) Reset() { + *x = InboxMessage{} + mi := &file_store_inbox_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *InboxMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*InboxMessage) ProtoMessage() {} + +func (x *InboxMessage) ProtoReflect() protoreflect.Message { + mi := &file_store_inbox_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use InboxMessage.ProtoReflect.Descriptor instead. +func (*InboxMessage) Descriptor() ([]byte, []int) { + return file_store_inbox_proto_rawDescGZIP(), []int{0} +} + +func (x *InboxMessage) GetType() InboxMessage_Type { + if x != nil { + return x.Type + } + return InboxMessage_TYPE_UNSPECIFIED +} + +func (x *InboxMessage) GetActivityId() int32 { + if x != nil && x.ActivityId != nil { + return *x.ActivityId + } + return 0 +} + +var File_store_inbox_proto protoreflect.FileDescriptor + +const file_store_inbox_proto_rawDesc = "" + + "\n" + + "\x11store/inbox.proto\x12\vmemos.store\"\xbc\x01\n" + + "\fInboxMessage\x122\n" + + "\x04type\x18\x01 \x01(\x0e2\x1e.memos.store.InboxMessage.TypeR\x04type\x12$\n" + + "\vactivity_id\x18\x02 \x01(\x05H\x00R\n" + + "activityId\x88\x01\x01\"B\n" + + "\x04Type\x12\x14\n" + + "\x10TYPE_UNSPECIFIED\x10\x00\x12\x10\n" + + "\fMEMO_COMMENT\x10\x01\x12\x12\n" + + "\x0eVERSION_UPDATE\x10\x02B\x0e\n" + + "\f_activity_idB\x95\x01\n" + + "\x0fcom.memos.storeB\n" + + "InboxProtoP\x01Z)github.com/usememos/memos/proto/gen/store\xa2\x02\x03MSX\xaa\x02\vMemos.Store\xca\x02\vMemos\\Store\xe2\x02\x17Memos\\Store\\GPBMetadata\xea\x02\fMemos::Storeb\x06proto3" + +var ( + file_store_inbox_proto_rawDescOnce sync.Once + file_store_inbox_proto_rawDescData []byte +) + +func file_store_inbox_proto_rawDescGZIP() []byte { + file_store_inbox_proto_rawDescOnce.Do(func() { + file_store_inbox_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_store_inbox_proto_rawDesc), len(file_store_inbox_proto_rawDesc))) + }) + return file_store_inbox_proto_rawDescData +} + +var file_store_inbox_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_store_inbox_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_store_inbox_proto_goTypes = []any{ + (InboxMessage_Type)(0), // 0: memos.store.InboxMessage.Type + (*InboxMessage)(nil), // 1: memos.store.InboxMessage +} +var file_store_inbox_proto_depIdxs = []int32{ + 0, // 0: memos.store.InboxMessage.type:type_name -> memos.store.InboxMessage.Type + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_store_inbox_proto_init() } +func file_store_inbox_proto_init() { + if File_store_inbox_proto != nil { + return + } + file_store_inbox_proto_msgTypes[0].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_store_inbox_proto_rawDesc), len(file_store_inbox_proto_rawDesc)), + NumEnums: 1, + NumMessages: 1, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_store_inbox_proto_goTypes, + DependencyIndexes: file_store_inbox_proto_depIdxs, + EnumInfos: file_store_inbox_proto_enumTypes, + MessageInfos: file_store_inbox_proto_msgTypes, + }.Build() + File_store_inbox_proto = out.File + file_store_inbox_proto_goTypes = nil + file_store_inbox_proto_depIdxs = nil +} diff --git a/proto/gen/store/memo.pb.go b/proto/gen/store/memo.pb.go new file mode 100644 index 0000000..8306367 --- /dev/null +++ b/proto/gen/store/memo.pb.go @@ -0,0 +1,295 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: store/memo.proto + +package store + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type MemoPayload struct { + state protoimpl.MessageState `protogen:"open.v1"` + Property *MemoPayload_Property `protobuf:"bytes,1,opt,name=property,proto3" json:"property,omitempty"` + Location *MemoPayload_Location `protobuf:"bytes,2,opt,name=location,proto3" json:"location,omitempty"` + Tags []string `protobuf:"bytes,3,rep,name=tags,proto3" json:"tags,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MemoPayload) Reset() { + *x = MemoPayload{} + mi := &file_store_memo_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MemoPayload) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MemoPayload) ProtoMessage() {} + +func (x *MemoPayload) ProtoReflect() protoreflect.Message { + mi := &file_store_memo_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MemoPayload.ProtoReflect.Descriptor instead. +func (*MemoPayload) Descriptor() ([]byte, []int) { + return file_store_memo_proto_rawDescGZIP(), []int{0} +} + +func (x *MemoPayload) GetProperty() *MemoPayload_Property { + if x != nil { + return x.Property + } + return nil +} + +func (x *MemoPayload) GetLocation() *MemoPayload_Location { + if x != nil { + return x.Location + } + return nil +} + +func (x *MemoPayload) GetTags() []string { + if x != nil { + return x.Tags + } + return nil +} + +// The calculated properties from the memo content. +type MemoPayload_Property struct { + state protoimpl.MessageState `protogen:"open.v1"` + HasLink bool `protobuf:"varint,1,opt,name=has_link,json=hasLink,proto3" json:"has_link,omitempty"` + HasTaskList bool `protobuf:"varint,2,opt,name=has_task_list,json=hasTaskList,proto3" json:"has_task_list,omitempty"` + HasCode bool `protobuf:"varint,3,opt,name=has_code,json=hasCode,proto3" json:"has_code,omitempty"` + HasIncompleteTasks bool `protobuf:"varint,4,opt,name=has_incomplete_tasks,json=hasIncompleteTasks,proto3" json:"has_incomplete_tasks,omitempty"` + // The references of the memo. Should be a list of uuid. + References []string `protobuf:"bytes,5,rep,name=references,proto3" json:"references,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MemoPayload_Property) Reset() { + *x = MemoPayload_Property{} + mi := &file_store_memo_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MemoPayload_Property) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MemoPayload_Property) ProtoMessage() {} + +func (x *MemoPayload_Property) ProtoReflect() protoreflect.Message { + mi := &file_store_memo_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MemoPayload_Property.ProtoReflect.Descriptor instead. +func (*MemoPayload_Property) Descriptor() ([]byte, []int) { + return file_store_memo_proto_rawDescGZIP(), []int{0, 0} +} + +func (x *MemoPayload_Property) GetHasLink() bool { + if x != nil { + return x.HasLink + } + return false +} + +func (x *MemoPayload_Property) GetHasTaskList() bool { + if x != nil { + return x.HasTaskList + } + return false +} + +func (x *MemoPayload_Property) GetHasCode() bool { + if x != nil { + return x.HasCode + } + return false +} + +func (x *MemoPayload_Property) GetHasIncompleteTasks() bool { + if x != nil { + return x.HasIncompleteTasks + } + return false +} + +func (x *MemoPayload_Property) GetReferences() []string { + if x != nil { + return x.References + } + return nil +} + +type MemoPayload_Location struct { + state protoimpl.MessageState `protogen:"open.v1"` + Placeholder string `protobuf:"bytes,1,opt,name=placeholder,proto3" json:"placeholder,omitempty"` + Latitude float64 `protobuf:"fixed64,2,opt,name=latitude,proto3" json:"latitude,omitempty"` + Longitude float64 `protobuf:"fixed64,3,opt,name=longitude,proto3" json:"longitude,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *MemoPayload_Location) Reset() { + *x = MemoPayload_Location{} + mi := &file_store_memo_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *MemoPayload_Location) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*MemoPayload_Location) ProtoMessage() {} + +func (x *MemoPayload_Location) ProtoReflect() protoreflect.Message { + mi := &file_store_memo_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use MemoPayload_Location.ProtoReflect.Descriptor instead. +func (*MemoPayload_Location) Descriptor() ([]byte, []int) { + return file_store_memo_proto_rawDescGZIP(), []int{0, 1} +} + +func (x *MemoPayload_Location) GetPlaceholder() string { + if x != nil { + return x.Placeholder + } + return "" +} + +func (x *MemoPayload_Location) GetLatitude() float64 { + if x != nil { + return x.Latitude + } + return 0 +} + +func (x *MemoPayload_Location) GetLongitude() float64 { + if x != nil { + return x.Longitude + } + return 0 +} + +var File_store_memo_proto protoreflect.FileDescriptor + +const file_store_memo_proto_rawDesc = "" + + "\n" + + "\x10store/memo.proto\x12\vmemos.store\"\xc0\x03\n" + + "\vMemoPayload\x12=\n" + + "\bproperty\x18\x01 \x01(\v2!.memos.store.MemoPayload.PropertyR\bproperty\x12=\n" + + "\blocation\x18\x02 \x01(\v2!.memos.store.MemoPayload.LocationR\blocation\x12\x12\n" + + "\x04tags\x18\x03 \x03(\tR\x04tags\x1a\xb6\x01\n" + + "\bProperty\x12\x19\n" + + "\bhas_link\x18\x01 \x01(\bR\ahasLink\x12\"\n" + + "\rhas_task_list\x18\x02 \x01(\bR\vhasTaskList\x12\x19\n" + + "\bhas_code\x18\x03 \x01(\bR\ahasCode\x120\n" + + "\x14has_incomplete_tasks\x18\x04 \x01(\bR\x12hasIncompleteTasks\x12\x1e\n" + + "\n" + + "references\x18\x05 \x03(\tR\n" + + "references\x1af\n" + + "\bLocation\x12 \n" + + "\vplaceholder\x18\x01 \x01(\tR\vplaceholder\x12\x1a\n" + + "\blatitude\x18\x02 \x01(\x01R\blatitude\x12\x1c\n" + + "\tlongitude\x18\x03 \x01(\x01R\tlongitudeB\x94\x01\n" + + "\x0fcom.memos.storeB\tMemoProtoP\x01Z)github.com/usememos/memos/proto/gen/store\xa2\x02\x03MSX\xaa\x02\vMemos.Store\xca\x02\vMemos\\Store\xe2\x02\x17Memos\\Store\\GPBMetadata\xea\x02\fMemos::Storeb\x06proto3" + +var ( + file_store_memo_proto_rawDescOnce sync.Once + file_store_memo_proto_rawDescData []byte +) + +func file_store_memo_proto_rawDescGZIP() []byte { + file_store_memo_proto_rawDescOnce.Do(func() { + file_store_memo_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_store_memo_proto_rawDesc), len(file_store_memo_proto_rawDesc))) + }) + return file_store_memo_proto_rawDescData +} + +var file_store_memo_proto_msgTypes = make([]protoimpl.MessageInfo, 3) +var file_store_memo_proto_goTypes = []any{ + (*MemoPayload)(nil), // 0: memos.store.MemoPayload + (*MemoPayload_Property)(nil), // 1: memos.store.MemoPayload.Property + (*MemoPayload_Location)(nil), // 2: memos.store.MemoPayload.Location +} +var file_store_memo_proto_depIdxs = []int32{ + 1, // 0: memos.store.MemoPayload.property:type_name -> memos.store.MemoPayload.Property + 2, // 1: memos.store.MemoPayload.location:type_name -> memos.store.MemoPayload.Location + 2, // [2:2] is the sub-list for method output_type + 2, // [2:2] is the sub-list for method input_type + 2, // [2:2] is the sub-list for extension type_name + 2, // [2:2] is the sub-list for extension extendee + 0, // [0:2] is the sub-list for field type_name +} + +func init() { file_store_memo_proto_init() } +func file_store_memo_proto_init() { + if File_store_memo_proto != nil { + return + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_store_memo_proto_rawDesc), len(file_store_memo_proto_rawDesc)), + NumEnums: 0, + NumMessages: 3, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_store_memo_proto_goTypes, + DependencyIndexes: file_store_memo_proto_depIdxs, + MessageInfos: file_store_memo_proto_msgTypes, + }.Build() + File_store_memo_proto = out.File + file_store_memo_proto_goTypes = nil + file_store_memo_proto_depIdxs = nil +} diff --git a/proto/gen/store/user_setting.pb.go b/proto/gen/store/user_setting.pb.go new file mode 100644 index 0000000..894aee7 --- /dev/null +++ b/proto/gen/store/user_setting.pb.go @@ -0,0 +1,962 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: store/user_setting.proto + +package store + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type UserSetting_Key int32 + +const ( + UserSetting_KEY_UNSPECIFIED UserSetting_Key = 0 + // General user settings. + UserSetting_GENERAL UserSetting_Key = 1 + // User authentication sessions. + UserSetting_SESSIONS UserSetting_Key = 2 + // Access tokens for the user. + UserSetting_ACCESS_TOKENS UserSetting_Key = 3 + // The shortcuts of the user. + UserSetting_SHORTCUTS UserSetting_Key = 4 + // The webhooks of the user. + UserSetting_WEBHOOKS UserSetting_Key = 5 +) + +// Enum value maps for UserSetting_Key. +var ( + UserSetting_Key_name = map[int32]string{ + 0: "KEY_UNSPECIFIED", + 1: "GENERAL", + 2: "SESSIONS", + 3: "ACCESS_TOKENS", + 4: "SHORTCUTS", + 5: "WEBHOOKS", + } + UserSetting_Key_value = map[string]int32{ + "KEY_UNSPECIFIED": 0, + "GENERAL": 1, + "SESSIONS": 2, + "ACCESS_TOKENS": 3, + "SHORTCUTS": 4, + "WEBHOOKS": 5, + } +) + +func (x UserSetting_Key) Enum() *UserSetting_Key { + p := new(UserSetting_Key) + *p = x + return p +} + +func (x UserSetting_Key) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (UserSetting_Key) Descriptor() protoreflect.EnumDescriptor { + return file_store_user_setting_proto_enumTypes[0].Descriptor() +} + +func (UserSetting_Key) Type() protoreflect.EnumType { + return &file_store_user_setting_proto_enumTypes[0] +} + +func (x UserSetting_Key) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use UserSetting_Key.Descriptor instead. +func (UserSetting_Key) EnumDescriptor() ([]byte, []int) { + return file_store_user_setting_proto_rawDescGZIP(), []int{0, 0} +} + +type UserSetting struct { + state protoimpl.MessageState `protogen:"open.v1"` + UserId int32 `protobuf:"varint,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` + Key UserSetting_Key `protobuf:"varint,2,opt,name=key,proto3,enum=memos.store.UserSetting_Key" json:"key,omitempty"` + // Types that are valid to be assigned to Value: + // + // *UserSetting_General + // *UserSetting_Sessions + // *UserSetting_AccessTokens + // *UserSetting_Shortcuts + // *UserSetting_Webhooks + Value isUserSetting_Value `protobuf_oneof:"value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UserSetting) Reset() { + *x = UserSetting{} + mi := &file_store_user_setting_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UserSetting) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserSetting) ProtoMessage() {} + +func (x *UserSetting) ProtoReflect() protoreflect.Message { + mi := &file_store_user_setting_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserSetting.ProtoReflect.Descriptor instead. +func (*UserSetting) Descriptor() ([]byte, []int) { + return file_store_user_setting_proto_rawDescGZIP(), []int{0} +} + +func (x *UserSetting) GetUserId() int32 { + if x != nil { + return x.UserId + } + return 0 +} + +func (x *UserSetting) GetKey() UserSetting_Key { + if x != nil { + return x.Key + } + return UserSetting_KEY_UNSPECIFIED +} + +func (x *UserSetting) GetValue() isUserSetting_Value { + if x != nil { + return x.Value + } + return nil +} + +func (x *UserSetting) GetGeneral() *GeneralUserSetting { + if x != nil { + if x, ok := x.Value.(*UserSetting_General); ok { + return x.General + } + } + return nil +} + +func (x *UserSetting) GetSessions() *SessionsUserSetting { + if x != nil { + if x, ok := x.Value.(*UserSetting_Sessions); ok { + return x.Sessions + } + } + return nil +} + +func (x *UserSetting) GetAccessTokens() *AccessTokensUserSetting { + if x != nil { + if x, ok := x.Value.(*UserSetting_AccessTokens); ok { + return x.AccessTokens + } + } + return nil +} + +func (x *UserSetting) GetShortcuts() *ShortcutsUserSetting { + if x != nil { + if x, ok := x.Value.(*UserSetting_Shortcuts); ok { + return x.Shortcuts + } + } + return nil +} + +func (x *UserSetting) GetWebhooks() *WebhooksUserSetting { + if x != nil { + if x, ok := x.Value.(*UserSetting_Webhooks); ok { + return x.Webhooks + } + } + return nil +} + +type isUserSetting_Value interface { + isUserSetting_Value() +} + +type UserSetting_General struct { + General *GeneralUserSetting `protobuf:"bytes,3,opt,name=general,proto3,oneof"` +} + +type UserSetting_Sessions struct { + Sessions *SessionsUserSetting `protobuf:"bytes,4,opt,name=sessions,proto3,oneof"` +} + +type UserSetting_AccessTokens struct { + AccessTokens *AccessTokensUserSetting `protobuf:"bytes,5,opt,name=access_tokens,json=accessTokens,proto3,oneof"` +} + +type UserSetting_Shortcuts struct { + Shortcuts *ShortcutsUserSetting `protobuf:"bytes,6,opt,name=shortcuts,proto3,oneof"` +} + +type UserSetting_Webhooks struct { + Webhooks *WebhooksUserSetting `protobuf:"bytes,7,opt,name=webhooks,proto3,oneof"` +} + +func (*UserSetting_General) isUserSetting_Value() {} + +func (*UserSetting_Sessions) isUserSetting_Value() {} + +func (*UserSetting_AccessTokens) isUserSetting_Value() {} + +func (*UserSetting_Shortcuts) isUserSetting_Value() {} + +func (*UserSetting_Webhooks) isUserSetting_Value() {} + +type GeneralUserSetting struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The user's locale. + Locale string `protobuf:"bytes,1,opt,name=locale,proto3" json:"locale,omitempty"` + // The user's appearance setting. + Appearance string `protobuf:"bytes,2,opt,name=appearance,proto3" json:"appearance,omitempty"` + // The user's memo visibility setting. + MemoVisibility string `protobuf:"bytes,3,opt,name=memo_visibility,json=memoVisibility,proto3" json:"memo_visibility,omitempty"` + // The user's theme preference. + // This references a CSS file in the web/public/themes/ directory. + Theme string `protobuf:"bytes,4,opt,name=theme,proto3" json:"theme,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GeneralUserSetting) Reset() { + *x = GeneralUserSetting{} + mi := &file_store_user_setting_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GeneralUserSetting) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GeneralUserSetting) ProtoMessage() {} + +func (x *GeneralUserSetting) ProtoReflect() protoreflect.Message { + mi := &file_store_user_setting_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GeneralUserSetting.ProtoReflect.Descriptor instead. +func (*GeneralUserSetting) Descriptor() ([]byte, []int) { + return file_store_user_setting_proto_rawDescGZIP(), []int{1} +} + +func (x *GeneralUserSetting) GetLocale() string { + if x != nil { + return x.Locale + } + return "" +} + +func (x *GeneralUserSetting) GetAppearance() string { + if x != nil { + return x.Appearance + } + return "" +} + +func (x *GeneralUserSetting) GetMemoVisibility() string { + if x != nil { + return x.MemoVisibility + } + return "" +} + +func (x *GeneralUserSetting) GetTheme() string { + if x != nil { + return x.Theme + } + return "" +} + +type SessionsUserSetting struct { + state protoimpl.MessageState `protogen:"open.v1"` + Sessions []*SessionsUserSetting_Session `protobuf:"bytes,1,rep,name=sessions,proto3" json:"sessions,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SessionsUserSetting) Reset() { + *x = SessionsUserSetting{} + mi := &file_store_user_setting_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SessionsUserSetting) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SessionsUserSetting) ProtoMessage() {} + +func (x *SessionsUserSetting) ProtoReflect() protoreflect.Message { + mi := &file_store_user_setting_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SessionsUserSetting.ProtoReflect.Descriptor instead. +func (*SessionsUserSetting) Descriptor() ([]byte, []int) { + return file_store_user_setting_proto_rawDescGZIP(), []int{2} +} + +func (x *SessionsUserSetting) GetSessions() []*SessionsUserSetting_Session { + if x != nil { + return x.Sessions + } + return nil +} + +type AccessTokensUserSetting struct { + state protoimpl.MessageState `protogen:"open.v1"` + AccessTokens []*AccessTokensUserSetting_AccessToken `protobuf:"bytes,1,rep,name=access_tokens,json=accessTokens,proto3" json:"access_tokens,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AccessTokensUserSetting) Reset() { + *x = AccessTokensUserSetting{} + mi := &file_store_user_setting_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AccessTokensUserSetting) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AccessTokensUserSetting) ProtoMessage() {} + +func (x *AccessTokensUserSetting) ProtoReflect() protoreflect.Message { + mi := &file_store_user_setting_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AccessTokensUserSetting.ProtoReflect.Descriptor instead. +func (*AccessTokensUserSetting) Descriptor() ([]byte, []int) { + return file_store_user_setting_proto_rawDescGZIP(), []int{3} +} + +func (x *AccessTokensUserSetting) GetAccessTokens() []*AccessTokensUserSetting_AccessToken { + if x != nil { + return x.AccessTokens + } + return nil +} + +type ShortcutsUserSetting struct { + state protoimpl.MessageState `protogen:"open.v1"` + Shortcuts []*ShortcutsUserSetting_Shortcut `protobuf:"bytes,1,rep,name=shortcuts,proto3" json:"shortcuts,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ShortcutsUserSetting) Reset() { + *x = ShortcutsUserSetting{} + mi := &file_store_user_setting_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ShortcutsUserSetting) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ShortcutsUserSetting) ProtoMessage() {} + +func (x *ShortcutsUserSetting) ProtoReflect() protoreflect.Message { + mi := &file_store_user_setting_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ShortcutsUserSetting.ProtoReflect.Descriptor instead. +func (*ShortcutsUserSetting) Descriptor() ([]byte, []int) { + return file_store_user_setting_proto_rawDescGZIP(), []int{4} +} + +func (x *ShortcutsUserSetting) GetShortcuts() []*ShortcutsUserSetting_Shortcut { + if x != nil { + return x.Shortcuts + } + return nil +} + +type WebhooksUserSetting struct { + state protoimpl.MessageState `protogen:"open.v1"` + Webhooks []*WebhooksUserSetting_Webhook `protobuf:"bytes,1,rep,name=webhooks,proto3" json:"webhooks,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WebhooksUserSetting) Reset() { + *x = WebhooksUserSetting{} + mi := &file_store_user_setting_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WebhooksUserSetting) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WebhooksUserSetting) ProtoMessage() {} + +func (x *WebhooksUserSetting) ProtoReflect() protoreflect.Message { + mi := &file_store_user_setting_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WebhooksUserSetting.ProtoReflect.Descriptor instead. +func (*WebhooksUserSetting) Descriptor() ([]byte, []int) { + return file_store_user_setting_proto_rawDescGZIP(), []int{5} +} + +func (x *WebhooksUserSetting) GetWebhooks() []*WebhooksUserSetting_Webhook { + if x != nil { + return x.Webhooks + } + return nil +} + +type SessionsUserSetting_Session struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Unique session identifier. + SessionId string `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"` + // Timestamp when the session was created. + CreateTime *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=create_time,json=createTime,proto3" json:"create_time,omitempty"` + // Timestamp when the session was last accessed. + // Used for sliding expiration calculation (last_accessed_time + 2 weeks). + LastAccessedTime *timestamppb.Timestamp `protobuf:"bytes,3,opt,name=last_accessed_time,json=lastAccessedTime,proto3" json:"last_accessed_time,omitempty"` + // Client information associated with this session. + ClientInfo *SessionsUserSetting_ClientInfo `protobuf:"bytes,4,opt,name=client_info,json=clientInfo,proto3" json:"client_info,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SessionsUserSetting_Session) Reset() { + *x = SessionsUserSetting_Session{} + mi := &file_store_user_setting_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SessionsUserSetting_Session) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SessionsUserSetting_Session) ProtoMessage() {} + +func (x *SessionsUserSetting_Session) ProtoReflect() protoreflect.Message { + mi := &file_store_user_setting_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SessionsUserSetting_Session.ProtoReflect.Descriptor instead. +func (*SessionsUserSetting_Session) Descriptor() ([]byte, []int) { + return file_store_user_setting_proto_rawDescGZIP(), []int{2, 0} +} + +func (x *SessionsUserSetting_Session) GetSessionId() string { + if x != nil { + return x.SessionId + } + return "" +} + +func (x *SessionsUserSetting_Session) GetCreateTime() *timestamppb.Timestamp { + if x != nil { + return x.CreateTime + } + return nil +} + +func (x *SessionsUserSetting_Session) GetLastAccessedTime() *timestamppb.Timestamp { + if x != nil { + return x.LastAccessedTime + } + return nil +} + +func (x *SessionsUserSetting_Session) GetClientInfo() *SessionsUserSetting_ClientInfo { + if x != nil { + return x.ClientInfo + } + return nil +} + +type SessionsUserSetting_ClientInfo struct { + state protoimpl.MessageState `protogen:"open.v1"` + // User agent string of the client. + UserAgent string `protobuf:"bytes,1,opt,name=user_agent,json=userAgent,proto3" json:"user_agent,omitempty"` + // IP address of the client. + IpAddress string `protobuf:"bytes,2,opt,name=ip_address,json=ipAddress,proto3" json:"ip_address,omitempty"` + // Optional. Device type (e.g., "mobile", "desktop", "tablet"). + DeviceType string `protobuf:"bytes,3,opt,name=device_type,json=deviceType,proto3" json:"device_type,omitempty"` + // Optional. Operating system (e.g., "iOS 17.0", "Windows 11"). + Os string `protobuf:"bytes,4,opt,name=os,proto3" json:"os,omitempty"` + // Optional. Browser name and version (e.g., "Chrome 119.0"). + Browser string `protobuf:"bytes,5,opt,name=browser,proto3" json:"browser,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *SessionsUserSetting_ClientInfo) Reset() { + *x = SessionsUserSetting_ClientInfo{} + mi := &file_store_user_setting_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *SessionsUserSetting_ClientInfo) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SessionsUserSetting_ClientInfo) ProtoMessage() {} + +func (x *SessionsUserSetting_ClientInfo) ProtoReflect() protoreflect.Message { + mi := &file_store_user_setting_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use SessionsUserSetting_ClientInfo.ProtoReflect.Descriptor instead. +func (*SessionsUserSetting_ClientInfo) Descriptor() ([]byte, []int) { + return file_store_user_setting_proto_rawDescGZIP(), []int{2, 1} +} + +func (x *SessionsUserSetting_ClientInfo) GetUserAgent() string { + if x != nil { + return x.UserAgent + } + return "" +} + +func (x *SessionsUserSetting_ClientInfo) GetIpAddress() string { + if x != nil { + return x.IpAddress + } + return "" +} + +func (x *SessionsUserSetting_ClientInfo) GetDeviceType() string { + if x != nil { + return x.DeviceType + } + return "" +} + +func (x *SessionsUserSetting_ClientInfo) GetOs() string { + if x != nil { + return x.Os + } + return "" +} + +func (x *SessionsUserSetting_ClientInfo) GetBrowser() string { + if x != nil { + return x.Browser + } + return "" +} + +type AccessTokensUserSetting_AccessToken struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The access token is a JWT token. + // Including expiration time, issuer, etc. + AccessToken string `protobuf:"bytes,1,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"` + // A description for the access token. + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *AccessTokensUserSetting_AccessToken) Reset() { + *x = AccessTokensUserSetting_AccessToken{} + mi := &file_store_user_setting_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *AccessTokensUserSetting_AccessToken) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AccessTokensUserSetting_AccessToken) ProtoMessage() {} + +func (x *AccessTokensUserSetting_AccessToken) ProtoReflect() protoreflect.Message { + mi := &file_store_user_setting_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AccessTokensUserSetting_AccessToken.ProtoReflect.Descriptor instead. +func (*AccessTokensUserSetting_AccessToken) Descriptor() ([]byte, []int) { + return file_store_user_setting_proto_rawDescGZIP(), []int{3, 0} +} + +func (x *AccessTokensUserSetting_AccessToken) GetAccessToken() string { + if x != nil { + return x.AccessToken + } + return "" +} + +func (x *AccessTokensUserSetting_AccessToken) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +type ShortcutsUserSetting_Shortcut struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + Filter string `protobuf:"bytes,3,opt,name=filter,proto3" json:"filter,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ShortcutsUserSetting_Shortcut) Reset() { + *x = ShortcutsUserSetting_Shortcut{} + mi := &file_store_user_setting_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ShortcutsUserSetting_Shortcut) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ShortcutsUserSetting_Shortcut) ProtoMessage() {} + +func (x *ShortcutsUserSetting_Shortcut) ProtoReflect() protoreflect.Message { + mi := &file_store_user_setting_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ShortcutsUserSetting_Shortcut.ProtoReflect.Descriptor instead. +func (*ShortcutsUserSetting_Shortcut) Descriptor() ([]byte, []int) { + return file_store_user_setting_proto_rawDescGZIP(), []int{4, 0} +} + +func (x *ShortcutsUserSetting_Shortcut) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *ShortcutsUserSetting_Shortcut) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *ShortcutsUserSetting_Shortcut) GetFilter() string { + if x != nil { + return x.Filter + } + return "" +} + +type WebhooksUserSetting_Webhook struct { + state protoimpl.MessageState `protogen:"open.v1"` + // Unique identifier for the webhook + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + // Descriptive title for the webhook + Title string `protobuf:"bytes,2,opt,name=title,proto3" json:"title,omitempty"` + // The webhook URL endpoint + Url string `protobuf:"bytes,3,opt,name=url,proto3" json:"url,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WebhooksUserSetting_Webhook) Reset() { + *x = WebhooksUserSetting_Webhook{} + mi := &file_store_user_setting_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WebhooksUserSetting_Webhook) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WebhooksUserSetting_Webhook) ProtoMessage() {} + +func (x *WebhooksUserSetting_Webhook) ProtoReflect() protoreflect.Message { + mi := &file_store_user_setting_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WebhooksUserSetting_Webhook.ProtoReflect.Descriptor instead. +func (*WebhooksUserSetting_Webhook) Descriptor() ([]byte, []int) { + return file_store_user_setting_proto_rawDescGZIP(), []int{5, 0} +} + +func (x *WebhooksUserSetting_Webhook) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *WebhooksUserSetting_Webhook) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *WebhooksUserSetting_Webhook) GetUrl() string { + if x != nil { + return x.Url + } + return "" +} + +var File_store_user_setting_proto protoreflect.FileDescriptor + +const file_store_user_setting_proto_rawDesc = "" + + "\n" + + "\x18store/user_setting.proto\x12\vmemos.store\x1a\x1fgoogle/protobuf/timestamp.proto\"\x93\x04\n" + + "\vUserSetting\x12\x17\n" + + "\auser_id\x18\x01 \x01(\x05R\x06userId\x12.\n" + + "\x03key\x18\x02 \x01(\x0e2\x1c.memos.store.UserSetting.KeyR\x03key\x12;\n" + + "\ageneral\x18\x03 \x01(\v2\x1f.memos.store.GeneralUserSettingH\x00R\ageneral\x12>\n" + + "\bsessions\x18\x04 \x01(\v2 .memos.store.SessionsUserSettingH\x00R\bsessions\x12K\n" + + "\raccess_tokens\x18\x05 \x01(\v2$.memos.store.AccessTokensUserSettingH\x00R\faccessTokens\x12A\n" + + "\tshortcuts\x18\x06 \x01(\v2!.memos.store.ShortcutsUserSettingH\x00R\tshortcuts\x12>\n" + + "\bwebhooks\x18\a \x01(\v2 .memos.store.WebhooksUserSettingH\x00R\bwebhooks\"e\n" + + "\x03Key\x12\x13\n" + + "\x0fKEY_UNSPECIFIED\x10\x00\x12\v\n" + + "\aGENERAL\x10\x01\x12\f\n" + + "\bSESSIONS\x10\x02\x12\x11\n" + + "\rACCESS_TOKENS\x10\x03\x12\r\n" + + "\tSHORTCUTS\x10\x04\x12\f\n" + + "\bWEBHOOKS\x10\x05B\a\n" + + "\x05value\"\x8b\x01\n" + + "\x12GeneralUserSetting\x12\x16\n" + + "\x06locale\x18\x01 \x01(\tR\x06locale\x12\x1e\n" + + "\n" + + "appearance\x18\x02 \x01(\tR\n" + + "appearance\x12'\n" + + "\x0fmemo_visibility\x18\x03 \x01(\tR\x0ememoVisibility\x12\x14\n" + + "\x05theme\x18\x04 \x01(\tR\x05theme\"\xf3\x03\n" + + "\x13SessionsUserSetting\x12D\n" + + "\bsessions\x18\x01 \x03(\v2(.memos.store.SessionsUserSetting.SessionR\bsessions\x1a\xfd\x01\n" + + "\aSession\x12\x1d\n" + + "\n" + + "session_id\x18\x01 \x01(\tR\tsessionId\x12;\n" + + "\vcreate_time\x18\x02 \x01(\v2\x1a.google.protobuf.TimestampR\n" + + "createTime\x12H\n" + + "\x12last_accessed_time\x18\x03 \x01(\v2\x1a.google.protobuf.TimestampR\x10lastAccessedTime\x12L\n" + + "\vclient_info\x18\x04 \x01(\v2+.memos.store.SessionsUserSetting.ClientInfoR\n" + + "clientInfo\x1a\x95\x01\n" + + "\n" + + "ClientInfo\x12\x1d\n" + + "\n" + + "user_agent\x18\x01 \x01(\tR\tuserAgent\x12\x1d\n" + + "\n" + + "ip_address\x18\x02 \x01(\tR\tipAddress\x12\x1f\n" + + "\vdevice_type\x18\x03 \x01(\tR\n" + + "deviceType\x12\x0e\n" + + "\x02os\x18\x04 \x01(\tR\x02os\x12\x18\n" + + "\abrowser\x18\x05 \x01(\tR\abrowser\"\xc4\x01\n" + + "\x17AccessTokensUserSetting\x12U\n" + + "\raccess_tokens\x18\x01 \x03(\v20.memos.store.AccessTokensUserSetting.AccessTokenR\faccessTokens\x1aR\n" + + "\vAccessToken\x12!\n" + + "\faccess_token\x18\x01 \x01(\tR\vaccessToken\x12 \n" + + "\vdescription\x18\x02 \x01(\tR\vdescription\"\xaa\x01\n" + + "\x14ShortcutsUserSetting\x12H\n" + + "\tshortcuts\x18\x01 \x03(\v2*.memos.store.ShortcutsUserSetting.ShortcutR\tshortcuts\x1aH\n" + + "\bShortcut\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" + + "\x05title\x18\x02 \x01(\tR\x05title\x12\x16\n" + + "\x06filter\x18\x03 \x01(\tR\x06filter\"\x9e\x01\n" + + "\x13WebhooksUserSetting\x12D\n" + + "\bwebhooks\x18\x01 \x03(\v2(.memos.store.WebhooksUserSetting.WebhookR\bwebhooks\x1aA\n" + + "\aWebhook\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" + + "\x05title\x18\x02 \x01(\tR\x05title\x12\x10\n" + + "\x03url\x18\x03 \x01(\tR\x03urlB\x9b\x01\n" + + "\x0fcom.memos.storeB\x10UserSettingProtoP\x01Z)github.com/usememos/memos/proto/gen/store\xa2\x02\x03MSX\xaa\x02\vMemos.Store\xca\x02\vMemos\\Store\xe2\x02\x17Memos\\Store\\GPBMetadata\xea\x02\fMemos::Storeb\x06proto3" + +var ( + file_store_user_setting_proto_rawDescOnce sync.Once + file_store_user_setting_proto_rawDescData []byte +) + +func file_store_user_setting_proto_rawDescGZIP() []byte { + file_store_user_setting_proto_rawDescOnce.Do(func() { + file_store_user_setting_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_store_user_setting_proto_rawDesc), len(file_store_user_setting_proto_rawDesc))) + }) + return file_store_user_setting_proto_rawDescData +} + +var file_store_user_setting_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_store_user_setting_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_store_user_setting_proto_goTypes = []any{ + (UserSetting_Key)(0), // 0: memos.store.UserSetting.Key + (*UserSetting)(nil), // 1: memos.store.UserSetting + (*GeneralUserSetting)(nil), // 2: memos.store.GeneralUserSetting + (*SessionsUserSetting)(nil), // 3: memos.store.SessionsUserSetting + (*AccessTokensUserSetting)(nil), // 4: memos.store.AccessTokensUserSetting + (*ShortcutsUserSetting)(nil), // 5: memos.store.ShortcutsUserSetting + (*WebhooksUserSetting)(nil), // 6: memos.store.WebhooksUserSetting + (*SessionsUserSetting_Session)(nil), // 7: memos.store.SessionsUserSetting.Session + (*SessionsUserSetting_ClientInfo)(nil), // 8: memos.store.SessionsUserSetting.ClientInfo + (*AccessTokensUserSetting_AccessToken)(nil), // 9: memos.store.AccessTokensUserSetting.AccessToken + (*ShortcutsUserSetting_Shortcut)(nil), // 10: memos.store.ShortcutsUserSetting.Shortcut + (*WebhooksUserSetting_Webhook)(nil), // 11: memos.store.WebhooksUserSetting.Webhook + (*timestamppb.Timestamp)(nil), // 12: google.protobuf.Timestamp +} +var file_store_user_setting_proto_depIdxs = []int32{ + 0, // 0: memos.store.UserSetting.key:type_name -> memos.store.UserSetting.Key + 2, // 1: memos.store.UserSetting.general:type_name -> memos.store.GeneralUserSetting + 3, // 2: memos.store.UserSetting.sessions:type_name -> memos.store.SessionsUserSetting + 4, // 3: memos.store.UserSetting.access_tokens:type_name -> memos.store.AccessTokensUserSetting + 5, // 4: memos.store.UserSetting.shortcuts:type_name -> memos.store.ShortcutsUserSetting + 6, // 5: memos.store.UserSetting.webhooks:type_name -> memos.store.WebhooksUserSetting + 7, // 6: memos.store.SessionsUserSetting.sessions:type_name -> memos.store.SessionsUserSetting.Session + 9, // 7: memos.store.AccessTokensUserSetting.access_tokens:type_name -> memos.store.AccessTokensUserSetting.AccessToken + 10, // 8: memos.store.ShortcutsUserSetting.shortcuts:type_name -> memos.store.ShortcutsUserSetting.Shortcut + 11, // 9: memos.store.WebhooksUserSetting.webhooks:type_name -> memos.store.WebhooksUserSetting.Webhook + 12, // 10: memos.store.SessionsUserSetting.Session.create_time:type_name -> google.protobuf.Timestamp + 12, // 11: memos.store.SessionsUserSetting.Session.last_accessed_time:type_name -> google.protobuf.Timestamp + 8, // 12: memos.store.SessionsUserSetting.Session.client_info:type_name -> memos.store.SessionsUserSetting.ClientInfo + 13, // [13:13] is the sub-list for method output_type + 13, // [13:13] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name +} + +func init() { file_store_user_setting_proto_init() } +func file_store_user_setting_proto_init() { + if File_store_user_setting_proto != nil { + return + } + file_store_user_setting_proto_msgTypes[0].OneofWrappers = []any{ + (*UserSetting_General)(nil), + (*UserSetting_Sessions)(nil), + (*UserSetting_AccessTokens)(nil), + (*UserSetting_Shortcuts)(nil), + (*UserSetting_Webhooks)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_store_user_setting_proto_rawDesc), len(file_store_user_setting_proto_rawDesc)), + NumEnums: 1, + NumMessages: 11, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_store_user_setting_proto_goTypes, + DependencyIndexes: file_store_user_setting_proto_depIdxs, + EnumInfos: file_store_user_setting_proto_enumTypes, + MessageInfos: file_store_user_setting_proto_msgTypes, + }.Build() + File_store_user_setting_proto = out.File + file_store_user_setting_proto_goTypes = nil + file_store_user_setting_proto_depIdxs = nil +} diff --git a/proto/gen/store/workspace_setting.pb.go b/proto/gen/store/workspace_setting.pb.go new file mode 100644 index 0000000..ac097b3 --- /dev/null +++ b/proto/gen/store/workspace_setting.pb.go @@ -0,0 +1,935 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.6 +// protoc (unknown) +// source: store/workspace_setting.proto + +package store + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type WorkspaceSettingKey int32 + +const ( + WorkspaceSettingKey_WORKSPACE_SETTING_KEY_UNSPECIFIED WorkspaceSettingKey = 0 + // BASIC is the key for basic settings. + WorkspaceSettingKey_BASIC WorkspaceSettingKey = 1 + // GENERAL is the key for general settings. + WorkspaceSettingKey_GENERAL WorkspaceSettingKey = 2 + // STORAGE is the key for storage settings. + WorkspaceSettingKey_STORAGE WorkspaceSettingKey = 3 + // MEMO_RELATED is the key for memo related settings. + WorkspaceSettingKey_MEMO_RELATED WorkspaceSettingKey = 4 +) + +// Enum value maps for WorkspaceSettingKey. +var ( + WorkspaceSettingKey_name = map[int32]string{ + 0: "WORKSPACE_SETTING_KEY_UNSPECIFIED", + 1: "BASIC", + 2: "GENERAL", + 3: "STORAGE", + 4: "MEMO_RELATED", + } + WorkspaceSettingKey_value = map[string]int32{ + "WORKSPACE_SETTING_KEY_UNSPECIFIED": 0, + "BASIC": 1, + "GENERAL": 2, + "STORAGE": 3, + "MEMO_RELATED": 4, + } +) + +func (x WorkspaceSettingKey) Enum() *WorkspaceSettingKey { + p := new(WorkspaceSettingKey) + *p = x + return p +} + +func (x WorkspaceSettingKey) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (WorkspaceSettingKey) Descriptor() protoreflect.EnumDescriptor { + return file_store_workspace_setting_proto_enumTypes[0].Descriptor() +} + +func (WorkspaceSettingKey) Type() protoreflect.EnumType { + return &file_store_workspace_setting_proto_enumTypes[0] +} + +func (x WorkspaceSettingKey) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use WorkspaceSettingKey.Descriptor instead. +func (WorkspaceSettingKey) EnumDescriptor() ([]byte, []int) { + return file_store_workspace_setting_proto_rawDescGZIP(), []int{0} +} + +type WorkspaceStorageSetting_StorageType int32 + +const ( + WorkspaceStorageSetting_STORAGE_TYPE_UNSPECIFIED WorkspaceStorageSetting_StorageType = 0 + // STORAGE_TYPE_DATABASE is the database storage type. + WorkspaceStorageSetting_DATABASE WorkspaceStorageSetting_StorageType = 1 + // STORAGE_TYPE_LOCAL is the local storage type. + WorkspaceStorageSetting_LOCAL WorkspaceStorageSetting_StorageType = 2 + // STORAGE_TYPE_S3 is the S3 storage type. + WorkspaceStorageSetting_S3 WorkspaceStorageSetting_StorageType = 3 +) + +// Enum value maps for WorkspaceStorageSetting_StorageType. +var ( + WorkspaceStorageSetting_StorageType_name = map[int32]string{ + 0: "STORAGE_TYPE_UNSPECIFIED", + 1: "DATABASE", + 2: "LOCAL", + 3: "S3", + } + WorkspaceStorageSetting_StorageType_value = map[string]int32{ + "STORAGE_TYPE_UNSPECIFIED": 0, + "DATABASE": 1, + "LOCAL": 2, + "S3": 3, + } +) + +func (x WorkspaceStorageSetting_StorageType) Enum() *WorkspaceStorageSetting_StorageType { + p := new(WorkspaceStorageSetting_StorageType) + *p = x + return p +} + +func (x WorkspaceStorageSetting_StorageType) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (WorkspaceStorageSetting_StorageType) Descriptor() protoreflect.EnumDescriptor { + return file_store_workspace_setting_proto_enumTypes[1].Descriptor() +} + +func (WorkspaceStorageSetting_StorageType) Type() protoreflect.EnumType { + return &file_store_workspace_setting_proto_enumTypes[1] +} + +func (x WorkspaceStorageSetting_StorageType) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use WorkspaceStorageSetting_StorageType.Descriptor instead. +func (WorkspaceStorageSetting_StorageType) EnumDescriptor() ([]byte, []int) { + return file_store_workspace_setting_proto_rawDescGZIP(), []int{4, 0} +} + +type WorkspaceSetting struct { + state protoimpl.MessageState `protogen:"open.v1"` + Key WorkspaceSettingKey `protobuf:"varint,1,opt,name=key,proto3,enum=memos.store.WorkspaceSettingKey" json:"key,omitempty"` + // Types that are valid to be assigned to Value: + // + // *WorkspaceSetting_BasicSetting + // *WorkspaceSetting_GeneralSetting + // *WorkspaceSetting_StorageSetting + // *WorkspaceSetting_MemoRelatedSetting + Value isWorkspaceSetting_Value `protobuf_oneof:"value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkspaceSetting) Reset() { + *x = WorkspaceSetting{} + mi := &file_store_workspace_setting_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkspaceSetting) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkspaceSetting) ProtoMessage() {} + +func (x *WorkspaceSetting) ProtoReflect() protoreflect.Message { + mi := &file_store_workspace_setting_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkspaceSetting.ProtoReflect.Descriptor instead. +func (*WorkspaceSetting) Descriptor() ([]byte, []int) { + return file_store_workspace_setting_proto_rawDescGZIP(), []int{0} +} + +func (x *WorkspaceSetting) GetKey() WorkspaceSettingKey { + if x != nil { + return x.Key + } + return WorkspaceSettingKey_WORKSPACE_SETTING_KEY_UNSPECIFIED +} + +func (x *WorkspaceSetting) GetValue() isWorkspaceSetting_Value { + if x != nil { + return x.Value + } + return nil +} + +func (x *WorkspaceSetting) GetBasicSetting() *WorkspaceBasicSetting { + if x != nil { + if x, ok := x.Value.(*WorkspaceSetting_BasicSetting); ok { + return x.BasicSetting + } + } + return nil +} + +func (x *WorkspaceSetting) GetGeneralSetting() *WorkspaceGeneralSetting { + if x != nil { + if x, ok := x.Value.(*WorkspaceSetting_GeneralSetting); ok { + return x.GeneralSetting + } + } + return nil +} + +func (x *WorkspaceSetting) GetStorageSetting() *WorkspaceStorageSetting { + if x != nil { + if x, ok := x.Value.(*WorkspaceSetting_StorageSetting); ok { + return x.StorageSetting + } + } + return nil +} + +func (x *WorkspaceSetting) GetMemoRelatedSetting() *WorkspaceMemoRelatedSetting { + if x != nil { + if x, ok := x.Value.(*WorkspaceSetting_MemoRelatedSetting); ok { + return x.MemoRelatedSetting + } + } + return nil +} + +type isWorkspaceSetting_Value interface { + isWorkspaceSetting_Value() +} + +type WorkspaceSetting_BasicSetting struct { + BasicSetting *WorkspaceBasicSetting `protobuf:"bytes,2,opt,name=basic_setting,json=basicSetting,proto3,oneof"` +} + +type WorkspaceSetting_GeneralSetting struct { + GeneralSetting *WorkspaceGeneralSetting `protobuf:"bytes,3,opt,name=general_setting,json=generalSetting,proto3,oneof"` +} + +type WorkspaceSetting_StorageSetting struct { + StorageSetting *WorkspaceStorageSetting `protobuf:"bytes,4,opt,name=storage_setting,json=storageSetting,proto3,oneof"` +} + +type WorkspaceSetting_MemoRelatedSetting struct { + MemoRelatedSetting *WorkspaceMemoRelatedSetting `protobuf:"bytes,5,opt,name=memo_related_setting,json=memoRelatedSetting,proto3,oneof"` +} + +func (*WorkspaceSetting_BasicSetting) isWorkspaceSetting_Value() {} + +func (*WorkspaceSetting_GeneralSetting) isWorkspaceSetting_Value() {} + +func (*WorkspaceSetting_StorageSetting) isWorkspaceSetting_Value() {} + +func (*WorkspaceSetting_MemoRelatedSetting) isWorkspaceSetting_Value() {} + +type WorkspaceBasicSetting struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The secret key for workspace. Mainly used for session management. + SecretKey string `protobuf:"bytes,1,opt,name=secret_key,json=secretKey,proto3" json:"secret_key,omitempty"` + // The current schema version of database. + SchemaVersion string `protobuf:"bytes,2,opt,name=schema_version,json=schemaVersion,proto3" json:"schema_version,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkspaceBasicSetting) Reset() { + *x = WorkspaceBasicSetting{} + mi := &file_store_workspace_setting_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkspaceBasicSetting) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkspaceBasicSetting) ProtoMessage() {} + +func (x *WorkspaceBasicSetting) ProtoReflect() protoreflect.Message { + mi := &file_store_workspace_setting_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkspaceBasicSetting.ProtoReflect.Descriptor instead. +func (*WorkspaceBasicSetting) Descriptor() ([]byte, []int) { + return file_store_workspace_setting_proto_rawDescGZIP(), []int{1} +} + +func (x *WorkspaceBasicSetting) GetSecretKey() string { + if x != nil { + return x.SecretKey + } + return "" +} + +func (x *WorkspaceBasicSetting) GetSchemaVersion() string { + if x != nil { + return x.SchemaVersion + } + return "" +} + +type WorkspaceGeneralSetting struct { + state protoimpl.MessageState `protogen:"open.v1"` + // theme is the name of the selected theme. + // This references a CSS file in the web/public/themes/ directory. + Theme string `protobuf:"bytes,1,opt,name=theme,proto3" json:"theme,omitempty"` + // disallow_user_registration disallows user registration. + DisallowUserRegistration bool `protobuf:"varint,2,opt,name=disallow_user_registration,json=disallowUserRegistration,proto3" json:"disallow_user_registration,omitempty"` + // disallow_password_auth disallows password authentication. + DisallowPasswordAuth bool `protobuf:"varint,3,opt,name=disallow_password_auth,json=disallowPasswordAuth,proto3" json:"disallow_password_auth,omitempty"` + // additional_script is the additional script. + AdditionalScript string `protobuf:"bytes,4,opt,name=additional_script,json=additionalScript,proto3" json:"additional_script,omitempty"` + // additional_style is the additional style. + AdditionalStyle string `protobuf:"bytes,5,opt,name=additional_style,json=additionalStyle,proto3" json:"additional_style,omitempty"` + // custom_profile is the custom profile. + CustomProfile *WorkspaceCustomProfile `protobuf:"bytes,6,opt,name=custom_profile,json=customProfile,proto3" json:"custom_profile,omitempty"` + // week_start_day_offset is the week start day offset from Sunday. + // 0: Sunday, 1: Monday, 2: Tuesday, 3: Wednesday, 4: Thursday, 5: Friday, 6: Saturday + // Default is Sunday. + WeekStartDayOffset int32 `protobuf:"varint,7,opt,name=week_start_day_offset,json=weekStartDayOffset,proto3" json:"week_start_day_offset,omitempty"` + // disallow_change_username disallows changing username. + DisallowChangeUsername bool `protobuf:"varint,8,opt,name=disallow_change_username,json=disallowChangeUsername,proto3" json:"disallow_change_username,omitempty"` + // disallow_change_nickname disallows changing nickname. + DisallowChangeNickname bool `protobuf:"varint,9,opt,name=disallow_change_nickname,json=disallowChangeNickname,proto3" json:"disallow_change_nickname,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkspaceGeneralSetting) Reset() { + *x = WorkspaceGeneralSetting{} + mi := &file_store_workspace_setting_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkspaceGeneralSetting) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkspaceGeneralSetting) ProtoMessage() {} + +func (x *WorkspaceGeneralSetting) ProtoReflect() protoreflect.Message { + mi := &file_store_workspace_setting_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkspaceGeneralSetting.ProtoReflect.Descriptor instead. +func (*WorkspaceGeneralSetting) Descriptor() ([]byte, []int) { + return file_store_workspace_setting_proto_rawDescGZIP(), []int{2} +} + +func (x *WorkspaceGeneralSetting) GetTheme() string { + if x != nil { + return x.Theme + } + return "" +} + +func (x *WorkspaceGeneralSetting) GetDisallowUserRegistration() bool { + if x != nil { + return x.DisallowUserRegistration + } + return false +} + +func (x *WorkspaceGeneralSetting) GetDisallowPasswordAuth() bool { + if x != nil { + return x.DisallowPasswordAuth + } + return false +} + +func (x *WorkspaceGeneralSetting) GetAdditionalScript() string { + if x != nil { + return x.AdditionalScript + } + return "" +} + +func (x *WorkspaceGeneralSetting) GetAdditionalStyle() string { + if x != nil { + return x.AdditionalStyle + } + return "" +} + +func (x *WorkspaceGeneralSetting) GetCustomProfile() *WorkspaceCustomProfile { + if x != nil { + return x.CustomProfile + } + return nil +} + +func (x *WorkspaceGeneralSetting) GetWeekStartDayOffset() int32 { + if x != nil { + return x.WeekStartDayOffset + } + return 0 +} + +func (x *WorkspaceGeneralSetting) GetDisallowChangeUsername() bool { + if x != nil { + return x.DisallowChangeUsername + } + return false +} + +func (x *WorkspaceGeneralSetting) GetDisallowChangeNickname() bool { + if x != nil { + return x.DisallowChangeNickname + } + return false +} + +type WorkspaceCustomProfile struct { + state protoimpl.MessageState `protogen:"open.v1"` + Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"` + Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` + LogoUrl string `protobuf:"bytes,3,opt,name=logo_url,json=logoUrl,proto3" json:"logo_url,omitempty"` + Locale string `protobuf:"bytes,4,opt,name=locale,proto3" json:"locale,omitempty"` + Appearance string `protobuf:"bytes,5,opt,name=appearance,proto3" json:"appearance,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkspaceCustomProfile) Reset() { + *x = WorkspaceCustomProfile{} + mi := &file_store_workspace_setting_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkspaceCustomProfile) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkspaceCustomProfile) ProtoMessage() {} + +func (x *WorkspaceCustomProfile) ProtoReflect() protoreflect.Message { + mi := &file_store_workspace_setting_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkspaceCustomProfile.ProtoReflect.Descriptor instead. +func (*WorkspaceCustomProfile) Descriptor() ([]byte, []int) { + return file_store_workspace_setting_proto_rawDescGZIP(), []int{3} +} + +func (x *WorkspaceCustomProfile) GetTitle() string { + if x != nil { + return x.Title + } + return "" +} + +func (x *WorkspaceCustomProfile) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *WorkspaceCustomProfile) GetLogoUrl() string { + if x != nil { + return x.LogoUrl + } + return "" +} + +func (x *WorkspaceCustomProfile) GetLocale() string { + if x != nil { + return x.Locale + } + return "" +} + +func (x *WorkspaceCustomProfile) GetAppearance() string { + if x != nil { + return x.Appearance + } + return "" +} + +type WorkspaceStorageSetting struct { + state protoimpl.MessageState `protogen:"open.v1"` + // storage_type is the storage type. + StorageType WorkspaceStorageSetting_StorageType `protobuf:"varint,1,opt,name=storage_type,json=storageType,proto3,enum=memos.store.WorkspaceStorageSetting_StorageType" json:"storage_type,omitempty"` + // The template of file path. + // e.g. assets/{timestamp}_{filename} + FilepathTemplate string `protobuf:"bytes,2,opt,name=filepath_template,json=filepathTemplate,proto3" json:"filepath_template,omitempty"` + // The max upload size in megabytes. + UploadSizeLimitMb int64 `protobuf:"varint,3,opt,name=upload_size_limit_mb,json=uploadSizeLimitMb,proto3" json:"upload_size_limit_mb,omitempty"` + // The S3 config. + S3Config *StorageS3Config `protobuf:"bytes,4,opt,name=s3_config,json=s3Config,proto3" json:"s3_config,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkspaceStorageSetting) Reset() { + *x = WorkspaceStorageSetting{} + mi := &file_store_workspace_setting_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkspaceStorageSetting) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkspaceStorageSetting) ProtoMessage() {} + +func (x *WorkspaceStorageSetting) ProtoReflect() protoreflect.Message { + mi := &file_store_workspace_setting_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkspaceStorageSetting.ProtoReflect.Descriptor instead. +func (*WorkspaceStorageSetting) Descriptor() ([]byte, []int) { + return file_store_workspace_setting_proto_rawDescGZIP(), []int{4} +} + +func (x *WorkspaceStorageSetting) GetStorageType() WorkspaceStorageSetting_StorageType { + if x != nil { + return x.StorageType + } + return WorkspaceStorageSetting_STORAGE_TYPE_UNSPECIFIED +} + +func (x *WorkspaceStorageSetting) GetFilepathTemplate() string { + if x != nil { + return x.FilepathTemplate + } + return "" +} + +func (x *WorkspaceStorageSetting) GetUploadSizeLimitMb() int64 { + if x != nil { + return x.UploadSizeLimitMb + } + return 0 +} + +func (x *WorkspaceStorageSetting) GetS3Config() *StorageS3Config { + if x != nil { + return x.S3Config + } + return nil +} + +// Reference: https://developers.cloudflare.com/r2/examples/aws/aws-sdk-go/ +type StorageS3Config struct { + state protoimpl.MessageState `protogen:"open.v1"` + AccessKeyId string `protobuf:"bytes,1,opt,name=access_key_id,json=accessKeyId,proto3" json:"access_key_id,omitempty"` + AccessKeySecret string `protobuf:"bytes,2,opt,name=access_key_secret,json=accessKeySecret,proto3" json:"access_key_secret,omitempty"` + Endpoint string `protobuf:"bytes,3,opt,name=endpoint,proto3" json:"endpoint,omitempty"` + Region string `protobuf:"bytes,4,opt,name=region,proto3" json:"region,omitempty"` + Bucket string `protobuf:"bytes,5,opt,name=bucket,proto3" json:"bucket,omitempty"` + UsePathStyle bool `protobuf:"varint,6,opt,name=use_path_style,json=usePathStyle,proto3" json:"use_path_style,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StorageS3Config) Reset() { + *x = StorageS3Config{} + mi := &file_store_workspace_setting_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StorageS3Config) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StorageS3Config) ProtoMessage() {} + +func (x *StorageS3Config) ProtoReflect() protoreflect.Message { + mi := &file_store_workspace_setting_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StorageS3Config.ProtoReflect.Descriptor instead. +func (*StorageS3Config) Descriptor() ([]byte, []int) { + return file_store_workspace_setting_proto_rawDescGZIP(), []int{5} +} + +func (x *StorageS3Config) GetAccessKeyId() string { + if x != nil { + return x.AccessKeyId + } + return "" +} + +func (x *StorageS3Config) GetAccessKeySecret() string { + if x != nil { + return x.AccessKeySecret + } + return "" +} + +func (x *StorageS3Config) GetEndpoint() string { + if x != nil { + return x.Endpoint + } + return "" +} + +func (x *StorageS3Config) GetRegion() string { + if x != nil { + return x.Region + } + return "" +} + +func (x *StorageS3Config) GetBucket() string { + if x != nil { + return x.Bucket + } + return "" +} + +func (x *StorageS3Config) GetUsePathStyle() bool { + if x != nil { + return x.UsePathStyle + } + return false +} + +type WorkspaceMemoRelatedSetting struct { + state protoimpl.MessageState `protogen:"open.v1"` + // disallow_public_visibility disallows set memo as public visibility. + DisallowPublicVisibility bool `protobuf:"varint,1,opt,name=disallow_public_visibility,json=disallowPublicVisibility,proto3" json:"disallow_public_visibility,omitempty"` + // display_with_update_time orders and displays memo with update time. + DisplayWithUpdateTime bool `protobuf:"varint,2,opt,name=display_with_update_time,json=displayWithUpdateTime,proto3" json:"display_with_update_time,omitempty"` + // content_length_limit is the limit of content length. Unit is byte. + ContentLengthLimit int32 `protobuf:"varint,3,opt,name=content_length_limit,json=contentLengthLimit,proto3" json:"content_length_limit,omitempty"` + // enable_double_click_edit enables editing on double click. + EnableDoubleClickEdit bool `protobuf:"varint,4,opt,name=enable_double_click_edit,json=enableDoubleClickEdit,proto3" json:"enable_double_click_edit,omitempty"` + // enable_link_preview enables links preview. + EnableLinkPreview bool `protobuf:"varint,5,opt,name=enable_link_preview,json=enableLinkPreview,proto3" json:"enable_link_preview,omitempty"` + // enable_comment enables comment. + EnableComment bool `protobuf:"varint,6,opt,name=enable_comment,json=enableComment,proto3" json:"enable_comment,omitempty"` + // reactions is the list of reactions. + Reactions []string `protobuf:"bytes,7,rep,name=reactions,proto3" json:"reactions,omitempty"` + // disable markdown shortcuts + DisableMarkdownShortcuts bool `protobuf:"varint,8,opt,name=disable_markdown_shortcuts,json=disableMarkdownShortcuts,proto3" json:"disable_markdown_shortcuts,omitempty"` + // enable_blur_nsfw_content enables blurring of content marked as not safe for work (NSFW). + EnableBlurNsfwContent bool `protobuf:"varint,9,opt,name=enable_blur_nsfw_content,json=enableBlurNsfwContent,proto3" json:"enable_blur_nsfw_content,omitempty"` + // nsfw_tags is the list of tags that mark content as NSFW for blurring. + NsfwTags []string `protobuf:"bytes,10,rep,name=nsfw_tags,json=nsfwTags,proto3" json:"nsfw_tags,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *WorkspaceMemoRelatedSetting) Reset() { + *x = WorkspaceMemoRelatedSetting{} + mi := &file_store_workspace_setting_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *WorkspaceMemoRelatedSetting) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*WorkspaceMemoRelatedSetting) ProtoMessage() {} + +func (x *WorkspaceMemoRelatedSetting) ProtoReflect() protoreflect.Message { + mi := &file_store_workspace_setting_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use WorkspaceMemoRelatedSetting.ProtoReflect.Descriptor instead. +func (*WorkspaceMemoRelatedSetting) Descriptor() ([]byte, []int) { + return file_store_workspace_setting_proto_rawDescGZIP(), []int{6} +} + +func (x *WorkspaceMemoRelatedSetting) GetDisallowPublicVisibility() bool { + if x != nil { + return x.DisallowPublicVisibility + } + return false +} + +func (x *WorkspaceMemoRelatedSetting) GetDisplayWithUpdateTime() bool { + if x != nil { + return x.DisplayWithUpdateTime + } + return false +} + +func (x *WorkspaceMemoRelatedSetting) GetContentLengthLimit() int32 { + if x != nil { + return x.ContentLengthLimit + } + return 0 +} + +func (x *WorkspaceMemoRelatedSetting) GetEnableDoubleClickEdit() bool { + if x != nil { + return x.EnableDoubleClickEdit + } + return false +} + +func (x *WorkspaceMemoRelatedSetting) GetEnableLinkPreview() bool { + if x != nil { + return x.EnableLinkPreview + } + return false +} + +func (x *WorkspaceMemoRelatedSetting) GetEnableComment() bool { + if x != nil { + return x.EnableComment + } + return false +} + +func (x *WorkspaceMemoRelatedSetting) GetReactions() []string { + if x != nil { + return x.Reactions + } + return nil +} + +func (x *WorkspaceMemoRelatedSetting) GetDisableMarkdownShortcuts() bool { + if x != nil { + return x.DisableMarkdownShortcuts + } + return false +} + +func (x *WorkspaceMemoRelatedSetting) GetEnableBlurNsfwContent() bool { + if x != nil { + return x.EnableBlurNsfwContent + } + return false +} + +func (x *WorkspaceMemoRelatedSetting) GetNsfwTags() []string { + if x != nil { + return x.NsfwTags + } + return nil +} + +var File_store_workspace_setting_proto protoreflect.FileDescriptor + +const file_store_workspace_setting_proto_rawDesc = "" + + "\n" + + "\x1dstore/workspace_setting.proto\x12\vmemos.store\"\x9a\x03\n" + + "\x10WorkspaceSetting\x122\n" + + "\x03key\x18\x01 \x01(\x0e2 .memos.store.WorkspaceSettingKeyR\x03key\x12I\n" + + "\rbasic_setting\x18\x02 \x01(\v2\".memos.store.WorkspaceBasicSettingH\x00R\fbasicSetting\x12O\n" + + "\x0fgeneral_setting\x18\x03 \x01(\v2$.memos.store.WorkspaceGeneralSettingH\x00R\x0egeneralSetting\x12O\n" + + "\x0fstorage_setting\x18\x04 \x01(\v2$.memos.store.WorkspaceStorageSettingH\x00R\x0estorageSetting\x12\\\n" + + "\x14memo_related_setting\x18\x05 \x01(\v2(.memos.store.WorkspaceMemoRelatedSettingH\x00R\x12memoRelatedSettingB\a\n" + + "\x05value\"]\n" + + "\x15WorkspaceBasicSetting\x12\x1d\n" + + "\n" + + "secret_key\x18\x01 \x01(\tR\tsecretKey\x12%\n" + + "\x0eschema_version\x18\x02 \x01(\tR\rschemaVersion\"\xee\x03\n" + + "\x17WorkspaceGeneralSetting\x12\x14\n" + + "\x05theme\x18\x01 \x01(\tR\x05theme\x12<\n" + + "\x1adisallow_user_registration\x18\x02 \x01(\bR\x18disallowUserRegistration\x124\n" + + "\x16disallow_password_auth\x18\x03 \x01(\bR\x14disallowPasswordAuth\x12+\n" + + "\x11additional_script\x18\x04 \x01(\tR\x10additionalScript\x12)\n" + + "\x10additional_style\x18\x05 \x01(\tR\x0fadditionalStyle\x12J\n" + + "\x0ecustom_profile\x18\x06 \x01(\v2#.memos.store.WorkspaceCustomProfileR\rcustomProfile\x121\n" + + "\x15week_start_day_offset\x18\a \x01(\x05R\x12weekStartDayOffset\x128\n" + + "\x18disallow_change_username\x18\b \x01(\bR\x16disallowChangeUsername\x128\n" + + "\x18disallow_change_nickname\x18\t \x01(\bR\x16disallowChangeNickname\"\xa3\x01\n" + + "\x16WorkspaceCustomProfile\x12\x14\n" + + "\x05title\x18\x01 \x01(\tR\x05title\x12 \n" + + "\vdescription\x18\x02 \x01(\tR\vdescription\x12\x19\n" + + "\blogo_url\x18\x03 \x01(\tR\alogoUrl\x12\x16\n" + + "\x06locale\x18\x04 \x01(\tR\x06locale\x12\x1e\n" + + "\n" + + "appearance\x18\x05 \x01(\tR\n" + + "appearance\"\xd5\x02\n" + + "\x17WorkspaceStorageSetting\x12S\n" + + "\fstorage_type\x18\x01 \x01(\x0e20.memos.store.WorkspaceStorageSetting.StorageTypeR\vstorageType\x12+\n" + + "\x11filepath_template\x18\x02 \x01(\tR\x10filepathTemplate\x12/\n" + + "\x14upload_size_limit_mb\x18\x03 \x01(\x03R\x11uploadSizeLimitMb\x129\n" + + "\ts3_config\x18\x04 \x01(\v2\x1c.memos.store.StorageS3ConfigR\bs3Config\"L\n" + + "\vStorageType\x12\x1c\n" + + "\x18STORAGE_TYPE_UNSPECIFIED\x10\x00\x12\f\n" + + "\bDATABASE\x10\x01\x12\t\n" + + "\x05LOCAL\x10\x02\x12\x06\n" + + "\x02S3\x10\x03\"\xd3\x01\n" + + "\x0fStorageS3Config\x12\"\n" + + "\raccess_key_id\x18\x01 \x01(\tR\vaccessKeyId\x12*\n" + + "\x11access_key_secret\x18\x02 \x01(\tR\x0faccessKeySecret\x12\x1a\n" + + "\bendpoint\x18\x03 \x01(\tR\bendpoint\x12\x16\n" + + "\x06region\x18\x04 \x01(\tR\x06region\x12\x16\n" + + "\x06bucket\x18\x05 \x01(\tR\x06bucket\x12$\n" + + "\x0euse_path_style\x18\x06 \x01(\bR\fusePathStyle\"\x88\x04\n" + + "\x1bWorkspaceMemoRelatedSetting\x12<\n" + + "\x1adisallow_public_visibility\x18\x01 \x01(\bR\x18disallowPublicVisibility\x127\n" + + "\x18display_with_update_time\x18\x02 \x01(\bR\x15displayWithUpdateTime\x120\n" + + "\x14content_length_limit\x18\x03 \x01(\x05R\x12contentLengthLimit\x127\n" + + "\x18enable_double_click_edit\x18\x04 \x01(\bR\x15enableDoubleClickEdit\x12.\n" + + "\x13enable_link_preview\x18\x05 \x01(\bR\x11enableLinkPreview\x12%\n" + + "\x0eenable_comment\x18\x06 \x01(\bR\renableComment\x12\x1c\n" + + "\treactions\x18\a \x03(\tR\treactions\x12<\n" + + "\x1adisable_markdown_shortcuts\x18\b \x01(\bR\x18disableMarkdownShortcuts\x127\n" + + "\x18enable_blur_nsfw_content\x18\t \x01(\bR\x15enableBlurNsfwContent\x12\x1b\n" + + "\tnsfw_tags\x18\n" + + " \x03(\tR\bnsfwTags*s\n" + + "\x13WorkspaceSettingKey\x12%\n" + + "!WORKSPACE_SETTING_KEY_UNSPECIFIED\x10\x00\x12\t\n" + + "\x05BASIC\x10\x01\x12\v\n" + + "\aGENERAL\x10\x02\x12\v\n" + + "\aSTORAGE\x10\x03\x12\x10\n" + + "\fMEMO_RELATED\x10\x04B\xa0\x01\n" + + "\x0fcom.memos.storeB\x15WorkspaceSettingProtoP\x01Z)github.com/usememos/memos/proto/gen/store\xa2\x02\x03MSX\xaa\x02\vMemos.Store\xca\x02\vMemos\\Store\xe2\x02\x17Memos\\Store\\GPBMetadata\xea\x02\fMemos::Storeb\x06proto3" + +var ( + file_store_workspace_setting_proto_rawDescOnce sync.Once + file_store_workspace_setting_proto_rawDescData []byte +) + +func file_store_workspace_setting_proto_rawDescGZIP() []byte { + file_store_workspace_setting_proto_rawDescOnce.Do(func() { + file_store_workspace_setting_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_store_workspace_setting_proto_rawDesc), len(file_store_workspace_setting_proto_rawDesc))) + }) + return file_store_workspace_setting_proto_rawDescData +} + +var file_store_workspace_setting_proto_enumTypes = make([]protoimpl.EnumInfo, 2) +var file_store_workspace_setting_proto_msgTypes = make([]protoimpl.MessageInfo, 7) +var file_store_workspace_setting_proto_goTypes = []any{ + (WorkspaceSettingKey)(0), // 0: memos.store.WorkspaceSettingKey + (WorkspaceStorageSetting_StorageType)(0), // 1: memos.store.WorkspaceStorageSetting.StorageType + (*WorkspaceSetting)(nil), // 2: memos.store.WorkspaceSetting + (*WorkspaceBasicSetting)(nil), // 3: memos.store.WorkspaceBasicSetting + (*WorkspaceGeneralSetting)(nil), // 4: memos.store.WorkspaceGeneralSetting + (*WorkspaceCustomProfile)(nil), // 5: memos.store.WorkspaceCustomProfile + (*WorkspaceStorageSetting)(nil), // 6: memos.store.WorkspaceStorageSetting + (*StorageS3Config)(nil), // 7: memos.store.StorageS3Config + (*WorkspaceMemoRelatedSetting)(nil), // 8: memos.store.WorkspaceMemoRelatedSetting +} +var file_store_workspace_setting_proto_depIdxs = []int32{ + 0, // 0: memos.store.WorkspaceSetting.key:type_name -> memos.store.WorkspaceSettingKey + 3, // 1: memos.store.WorkspaceSetting.basic_setting:type_name -> memos.store.WorkspaceBasicSetting + 4, // 2: memos.store.WorkspaceSetting.general_setting:type_name -> memos.store.WorkspaceGeneralSetting + 6, // 3: memos.store.WorkspaceSetting.storage_setting:type_name -> memos.store.WorkspaceStorageSetting + 8, // 4: memos.store.WorkspaceSetting.memo_related_setting:type_name -> memos.store.WorkspaceMemoRelatedSetting + 5, // 5: memos.store.WorkspaceGeneralSetting.custom_profile:type_name -> memos.store.WorkspaceCustomProfile + 1, // 6: memos.store.WorkspaceStorageSetting.storage_type:type_name -> memos.store.WorkspaceStorageSetting.StorageType + 7, // 7: memos.store.WorkspaceStorageSetting.s3_config:type_name -> memos.store.StorageS3Config + 8, // [8:8] is the sub-list for method output_type + 8, // [8:8] is the sub-list for method input_type + 8, // [8:8] is the sub-list for extension type_name + 8, // [8:8] is the sub-list for extension extendee + 0, // [0:8] is the sub-list for field type_name +} + +func init() { file_store_workspace_setting_proto_init() } +func file_store_workspace_setting_proto_init() { + if File_store_workspace_setting_proto != nil { + return + } + file_store_workspace_setting_proto_msgTypes[0].OneofWrappers = []any{ + (*WorkspaceSetting_BasicSetting)(nil), + (*WorkspaceSetting_GeneralSetting)(nil), + (*WorkspaceSetting_StorageSetting)(nil), + (*WorkspaceSetting_MemoRelatedSetting)(nil), + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_store_workspace_setting_proto_rawDesc), len(file_store_workspace_setting_proto_rawDesc)), + NumEnums: 2, + NumMessages: 7, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_store_workspace_setting_proto_goTypes, + DependencyIndexes: file_store_workspace_setting_proto_depIdxs, + EnumInfos: file_store_workspace_setting_proto_enumTypes, + MessageInfos: file_store_workspace_setting_proto_msgTypes, + }.Build() + File_store_workspace_setting_proto = out.File + file_store_workspace_setting_proto_goTypes = nil + file_store_workspace_setting_proto_depIdxs = nil +} diff --git a/proto/store/activity.proto b/proto/store/activity.proto new file mode 100644 index 0000000..d6613bd --- /dev/null +++ b/proto/store/activity.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +package memos.store; + +option go_package = "gen/store"; + +message ActivityMemoCommentPayload { + int32 memo_id = 1; + int32 related_memo_id = 2; +} + +message ActivityPayload { + ActivityMemoCommentPayload memo_comment = 1; +} diff --git a/proto/store/attachment.proto b/proto/store/attachment.proto new file mode 100644 index 0000000..4527a8a --- /dev/null +++ b/proto/store/attachment.proto @@ -0,0 +1,33 @@ +syntax = "proto3"; + +package memos.store; + +import "google/protobuf/timestamp.proto"; +import "store/workspace_setting.proto"; + +option go_package = "gen/store"; + +enum AttachmentStorageType { + ATTACHMENT_STORAGE_TYPE_UNSPECIFIED = 0; + // Attachment is stored locally. AKA, local file system. + LOCAL = 1; + // Attachment is stored in S3. + S3 = 2; + // Attachment is stored in an external storage. The reference is a URL. + EXTERNAL = 3; +} + +message AttachmentPayload { + oneof payload { + S3Object s3_object = 1; + } + + message S3Object { + StorageS3Config s3_config = 1; + // key is the S3 object key. + string key = 2; + // last_presigned_time is the last time the object was presigned. + // This is used to determine if the presigned URL is still valid. + google.protobuf.Timestamp last_presigned_time = 3; + } +} diff --git a/proto/store/idp.proto b/proto/store/idp.proto new file mode 100644 index 0000000..990376c --- /dev/null +++ b/proto/store/idp.proto @@ -0,0 +1,41 @@ +syntax = "proto3"; + +package memos.store; + +option go_package = "gen/store"; + +message IdentityProvider { + int32 id = 1; + string name = 2; + + enum Type { + TYPE_UNSPECIFIED = 0; + OAUTH2 = 1; + } + Type type = 3; + string identifier_filter = 4; + IdentityProviderConfig config = 5; +} + +message IdentityProviderConfig { + oneof config { + OAuth2Config oauth2_config = 1; + } +} + +message FieldMapping { + string identifier = 1; + string display_name = 2; + string email = 3; + string avatar_url = 4; +} + +message OAuth2Config { + string client_id = 1; + string client_secret = 2; + string auth_url = 3; + string token_url = 4; + string user_info_url = 5; + repeated string scopes = 6; + FieldMapping field_mapping = 7; +} diff --git a/proto/store/inbox.proto b/proto/store/inbox.proto new file mode 100644 index 0000000..f7f7082 --- /dev/null +++ b/proto/store/inbox.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package memos.store; + +option go_package = "gen/store"; + +message InboxMessage { + enum Type { + TYPE_UNSPECIFIED = 0; + MEMO_COMMENT = 1; + VERSION_UPDATE = 2; + } + Type type = 1; + optional int32 activity_id = 2; +} diff --git a/proto/store/memo.proto b/proto/store/memo.proto new file mode 100644 index 0000000..56e7488 --- /dev/null +++ b/proto/store/memo.proto @@ -0,0 +1,29 @@ +syntax = "proto3"; + +package memos.store; + +option go_package = "gen/store"; + +message MemoPayload { + Property property = 1; + + Location location = 2; + + repeated string tags = 3; + + // The calculated properties from the memo content. + message Property { + bool has_link = 1; + bool has_task_list = 2; + bool has_code = 3; + bool has_incomplete_tasks = 4; + // The references of the memo. Should be a list of uuid. + repeated string references = 5; + } + + message Location { + string placeholder = 1; + double latitude = 2; + double longitude = 3; + } +} diff --git a/proto/store/user_setting.proto b/proto/store/user_setting.proto new file mode 100644 index 0000000..7ec03b0 --- /dev/null +++ b/proto/store/user_setting.proto @@ -0,0 +1,107 @@ +syntax = "proto3"; + +package memos.store; + +import "google/protobuf/timestamp.proto"; + +option go_package = "gen/store"; + +message UserSetting { + enum Key { + KEY_UNSPECIFIED = 0; + // General user settings. + GENERAL = 1; + // User authentication sessions. + SESSIONS = 2; + // Access tokens for the user. + ACCESS_TOKENS = 3; + // The shortcuts of the user. + SHORTCUTS = 4; + // The webhooks of the user. + WEBHOOKS = 5; + } + + int32 user_id = 1; + + Key key = 2; + oneof value { + GeneralUserSetting general = 3; + SessionsUserSetting sessions = 4; + AccessTokensUserSetting access_tokens = 5; + ShortcutsUserSetting shortcuts = 6; + WebhooksUserSetting webhooks = 7; + } +} + +message GeneralUserSetting { + // The user's locale. + string locale = 1; + // The user's appearance setting. + string appearance = 2; + // The user's memo visibility setting. + string memo_visibility = 3; + // The user's theme preference. + // This references a CSS file in the web/public/themes/ directory. + string theme = 4; +} + +message SessionsUserSetting { + message Session { + // Unique session identifier. + string session_id = 1; + // Timestamp when the session was created. + google.protobuf.Timestamp create_time = 2; + // Timestamp when the session was last accessed. + // Used for sliding expiration calculation (last_accessed_time + 2 weeks). + google.protobuf.Timestamp last_accessed_time = 3; + // Client information associated with this session. + ClientInfo client_info = 4; + } + + message ClientInfo { + // User agent string of the client. + string user_agent = 1; + // IP address of the client. + string ip_address = 2; + // Optional. Device type (e.g., "mobile", "desktop", "tablet"). + string device_type = 3; + // Optional. Operating system (e.g., "iOS 17.0", "Windows 11"). + string os = 4; + // Optional. Browser name and version (e.g., "Chrome 119.0"). + string browser = 5; + } + + repeated Session sessions = 1; +} + +message AccessTokensUserSetting { + message AccessToken { + // The access token is a JWT token. + // Including expiration time, issuer, etc. + string access_token = 1; + // A description for the access token. + string description = 2; + } + repeated AccessToken access_tokens = 1; +} + +message ShortcutsUserSetting { + message Shortcut { + string id = 1; + string title = 2; + string filter = 3; + } + repeated Shortcut shortcuts = 1; +} + +message WebhooksUserSetting { + message Webhook { + // Unique identifier for the webhook + string id = 1; + // Descriptive title for the webhook + string title = 2; + // The webhook URL endpoint + string url = 3; + } + repeated Webhook webhooks = 1; +} diff --git a/proto/store/workspace_setting.proto b/proto/store/workspace_setting.proto new file mode 100644 index 0000000..73ea8fa --- /dev/null +++ b/proto/store/workspace_setting.proto @@ -0,0 +1,120 @@ +syntax = "proto3"; + +package memos.store; + +option go_package = "gen/store"; + +enum WorkspaceSettingKey { + WORKSPACE_SETTING_KEY_UNSPECIFIED = 0; + // BASIC is the key for basic settings. + BASIC = 1; + // GENERAL is the key for general settings. + GENERAL = 2; + // STORAGE is the key for storage settings. + STORAGE = 3; + // MEMO_RELATED is the key for memo related settings. + MEMO_RELATED = 4; +} + +message WorkspaceSetting { + WorkspaceSettingKey key = 1; + oneof value { + WorkspaceBasicSetting basic_setting = 2; + WorkspaceGeneralSetting general_setting = 3; + WorkspaceStorageSetting storage_setting = 4; + WorkspaceMemoRelatedSetting memo_related_setting = 5; + } +} + +message WorkspaceBasicSetting { + // The secret key for workspace. Mainly used for session management. + string secret_key = 1; + // The current schema version of database. + string schema_version = 2; +} + +message WorkspaceGeneralSetting { + // theme is the name of the selected theme. + // This references a CSS file in the web/public/themes/ directory. + string theme = 1; + // disallow_user_registration disallows user registration. + bool disallow_user_registration = 2; + // disallow_password_auth disallows password authentication. + bool disallow_password_auth = 3; + // additional_script is the additional script. + string additional_script = 4; + // additional_style is the additional style. + string additional_style = 5; + // custom_profile is the custom profile. + WorkspaceCustomProfile custom_profile = 6; + // week_start_day_offset is the week start day offset from Sunday. + // 0: Sunday, 1: Monday, 2: Tuesday, 3: Wednesday, 4: Thursday, 5: Friday, 6: Saturday + // Default is Sunday. + int32 week_start_day_offset = 7; + // disallow_change_username disallows changing username. + bool disallow_change_username = 8; + // disallow_change_nickname disallows changing nickname. + bool disallow_change_nickname = 9; +} + +message WorkspaceCustomProfile { + string title = 1; + string description = 2; + string logo_url = 3; + string locale = 4; + string appearance = 5; +} + +message WorkspaceStorageSetting { + enum StorageType { + STORAGE_TYPE_UNSPECIFIED = 0; + // STORAGE_TYPE_DATABASE is the database storage type. + DATABASE = 1; + // STORAGE_TYPE_LOCAL is the local storage type. + LOCAL = 2; + // STORAGE_TYPE_S3 is the S3 storage type. + S3 = 3; + } + // storage_type is the storage type. + StorageType storage_type = 1; + // The template of file path. + // e.g. assets/{timestamp}_{filename} + string filepath_template = 2; + // The max upload size in megabytes. + int64 upload_size_limit_mb = 3; + // The S3 config. + StorageS3Config s3_config = 4; +} + +// Reference: https://developers.cloudflare.com/r2/examples/aws/aws-sdk-go/ +message StorageS3Config { + string access_key_id = 1; + string access_key_secret = 2; + string endpoint = 3; + string region = 4; + string bucket = 5; + bool use_path_style = 6; +} + +message WorkspaceMemoRelatedSetting { + // disallow_public_visibility disallows set memo as public visibility. + bool disallow_public_visibility = 1; + // display_with_update_time orders and displays memo with update time. + bool display_with_update_time = 2; + // content_length_limit is the limit of content length. Unit is byte. + int32 content_length_limit = 3; + // enable_double_click_edit enables editing on double click. + bool enable_double_click_edit = 4; + // enable_link_preview enables links preview. + bool enable_link_preview = 5; + // enable_comment enables comment. + bool enable_comment = 6; + // reactions is the list of reactions. + repeated string reactions = 7; + // disable markdown shortcuts + bool disable_markdown_shortcuts = 8; + // enable_blur_nsfw_content enables blurring of content marked as not safe for work (NSFW). + bool enable_blur_nsfw_content = 9; + // nsfw_tags is the list of tags that mark content as NSFW for blurring. + repeated string nsfw_tags = 10; +} diff --git a/scripts/Dockerfile b/scripts/Dockerfile new file mode 100644 index 0000000..0a884a1 --- /dev/null +++ b/scripts/Dockerfile @@ -0,0 +1,31 @@ +FROM golang:1.24-alpine AS backend +WORKDIR /backend-build +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +# Please build frontend first, so that the static files are available. +# Refer to `pnpm release` in package.json for the build command. +RUN --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + go build -ldflags="-s -w" -o memos ./bin/memos/main.go + +# Make workspace with above generated files. +FROM alpine:latest AS monolithic +WORKDIR /usr/local/memos + +RUN apk add --no-cache tzdata +ENV TZ="UTC" + +COPY --from=backend /backend-build/memos /usr/local/memos/ +COPY ./scripts/entrypoint.sh /usr/local/memos/ + +EXPOSE 5230 + +# Directory to store the data, which can be referenced as the mounting point. +RUN mkdir -p /var/opt/memos +VOLUME /var/opt/memos + +ENV MEMOS_MODE="prod" +ENV MEMOS_PORT="5230" + +ENTRYPOINT ["./entrypoint.sh", "./memos"] diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 0000000..5340941 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +# Exit when any command fails +set -e + +# Get the script directory and change to the project root +cd "$(dirname "$0")/../" + +# Detect the operating system +OS=$(uname -s) + +# Set output file name based on the OS +if [[ "$OS" == *"CYGWIN"* || "$OS" == *"MINGW"* || "$OS" == *"MSYS"* ]]; then + OUTPUT="./build/memos.exe" +else + OUTPUT="./build/memos" +fi + +echo "Building for $OS..." + +# Build the executable +go build -o "$OUTPUT" ./bin/memos/main.go + +# Output the success message +echo "Build successful!" + +# Output the command to run +echo "To run the application, execute the following command:" +echo "$OUTPUT --mode dev" diff --git a/scripts/compose.yaml b/scripts/compose.yaml new file mode 100644 index 0000000..d5e33c3 --- /dev/null +++ b/scripts/compose.yaml @@ -0,0 +1,8 @@ +services: + memos: + image: neosmemo/memos:latest + container_name: memos + volumes: + - ~/.memos/:/var/opt/memos + ports: + - 5230:5230 diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh new file mode 100644 index 0000000..a73f401 --- /dev/null +++ b/scripts/entrypoint.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env sh + +file_env() { + var="$1" + fileVar="${var}_FILE" + + val_var="$(printenv "$var")" + val_fileVar="$(printenv "$fileVar")" + + if [ -n "$val_var" ] && [ -n "$val_fileVar" ]; then + echo "error: both $var and $fileVar are set (but are exclusive)" >&2 + exit 1 + fi + + if [ -n "$val_var" ]; then + val="$val_var" + elif [ -n "$val_fileVar" ]; then + val="$(cat "$val_fileVar")" + fi + + export "$var"="$val" + unset "$fileVar" +} + +file_env "MEMOS_DSN" + +exec "$@" diff --git a/server/profiler/profiler.go b/server/profiler/profiler.go new file mode 100644 index 0000000..4e09571 --- /dev/null +++ b/server/profiler/profiler.go @@ -0,0 +1,120 @@ +package profiler + +import ( + "context" + "fmt" + "log/slog" + "net/http" + "net/http/pprof" + "runtime" + "time" + + "github.com/labstack/echo/v4" +) + +// Profiler provides HTTP endpoints for memory profiling. +type Profiler struct { + memStatsLogInterval time.Duration +} + +// NewProfiler creates a new profiler. +func NewProfiler() *Profiler { + return &Profiler{ + memStatsLogInterval: 1 * time.Minute, + } +} + +// RegisterRoutes adds profiling endpoints to the Echo server. +func (*Profiler) RegisterRoutes(e *echo.Echo) { + // Register pprof handlers + g := e.Group("/debug/pprof") + g.GET("", echo.WrapHandler(http.HandlerFunc(pprof.Index))) + g.GET("/cmdline", echo.WrapHandler(http.HandlerFunc(pprof.Cmdline))) + g.GET("/profile", echo.WrapHandler(http.HandlerFunc(pprof.Profile))) + g.POST("/symbol", echo.WrapHandler(http.HandlerFunc(pprof.Symbol))) + g.GET("/symbol", echo.WrapHandler(http.HandlerFunc(pprof.Symbol))) + g.GET("/trace", echo.WrapHandler(http.HandlerFunc(pprof.Trace))) + g.GET("/allocs", echo.WrapHandler(http.HandlerFunc(pprof.Handler("allocs").ServeHTTP))) + g.GET("/block", echo.WrapHandler(http.HandlerFunc(pprof.Handler("block").ServeHTTP))) + g.GET("/goroutine", echo.WrapHandler(http.HandlerFunc(pprof.Handler("goroutine").ServeHTTP))) + g.GET("/heap", echo.WrapHandler(http.HandlerFunc(pprof.Handler("heap").ServeHTTP))) + g.GET("/mutex", echo.WrapHandler(http.HandlerFunc(pprof.Handler("mutex").ServeHTTP))) + g.GET("/threadcreate", echo.WrapHandler(http.HandlerFunc(pprof.Handler("threadcreate").ServeHTTP))) + + // Add a custom memory stats endpoint. + g.GET("/memstats", func(c echo.Context) error { + var m runtime.MemStats + runtime.ReadMemStats(&m) + return c.JSON(http.StatusOK, map[string]interface{}{ + "alloc": m.Alloc, + "totalAlloc": m.TotalAlloc, + "sys": m.Sys, + "numGC": m.NumGC, + "heapAlloc": m.HeapAlloc, + "heapSys": m.HeapSys, + "heapInuse": m.HeapInuse, + "heapObjects": m.HeapObjects, + }) + }) +} + +// StartMemoryMonitor starts a goroutine that periodically logs memory stats. +func (p *Profiler) StartMemoryMonitor(ctx context.Context) { + go func() { + ticker := time.NewTicker(p.memStatsLogInterval) + defer ticker.Stop() + + // Store previous heap allocation to track growth. + var lastHeapAlloc uint64 + var lastNumGC uint32 + + for { + select { + case <-ticker.C: + var m runtime.MemStats + runtime.ReadMemStats(&m) + + // Calculate heap growth since last check. + heapGrowth := int64(m.HeapAlloc) - int64(lastHeapAlloc) + gcCount := m.NumGC - lastNumGC + + slog.Info("memory stats", + "heapAlloc", byteCountIEC(m.HeapAlloc), + "heapSys", byteCountIEC(m.HeapSys), + "heapObjects", m.HeapObjects, + "heapGrowth", byteCountIEC(uint64(heapGrowth)), + "numGoroutine", runtime.NumGoroutine(), + "numGC", m.NumGC, + "gcSince", gcCount, + "nextGC", byteCountIEC(m.NextGC), + "gcPause", time.Duration(m.PauseNs[(m.NumGC+255)%256]).String(), + ) + + // Track values for next iteration. + lastHeapAlloc = m.HeapAlloc + lastNumGC = m.NumGC + + // Force GC if memory usage is high to see if objects can be reclaimed. + if m.HeapAlloc > 500*1024*1024 { // 500 MB threshold + slog.Info("forcing garbage collection due to high memory usage") + } + case <-ctx.Done(): + return + } + } + }() +} + +// byteCountIEC converts bytes to a human-readable string (MiB, GiB). +func byteCountIEC(b uint64) string { + const unit = 1024 + if b < unit { + return fmt.Sprintf("%d B", b) + } + div, exp := uint64(unit), 0 + for n := b / unit; n >= unit; n /= unit { + div *= unit + exp++ + } + return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp]) +} diff --git a/server/router/api/v1/acl.go b/server/router/api/v1/acl.go new file mode 100644 index 0000000..4b8d5a0 --- /dev/null +++ b/server/router/api/v1/acl.go @@ -0,0 +1,262 @@ +package v1 + +import ( + "context" + "net/http" + "strings" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/pkg/errors" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/usememos/memos/internal/util" + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +// ContextKey is the key type of context value. +type ContextKey int + +const ( + // The key name used to store user's ID in the context (for user-based auth). + userIDContextKey ContextKey = iota + // The key name used to store session ID in the context (for session-based auth). + sessionIDContextKey + // The key name used to store access token in the context (for token-based auth). + accessTokenContextKey +) + +// GRPCAuthInterceptor is the auth interceptor for gRPC server. +type GRPCAuthInterceptor struct { + Store *store.Store + secret string +} + +// NewGRPCAuthInterceptor returns a new API auth interceptor. +func NewGRPCAuthInterceptor(store *store.Store, secret string) *GRPCAuthInterceptor { + return &GRPCAuthInterceptor{ + Store: store, + secret: secret, + } +} + +// AuthenticationInterceptor is the unary interceptor for gRPC API. +func (in *GRPCAuthInterceptor) AuthenticationInterceptor(ctx context.Context, request any, serverInfo *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return nil, status.Errorf(codes.Unauthenticated, "failed to parse metadata from incoming context") + } + + // Try to authenticate via session ID (from cookie) first + if sessionCookieValue, err := getSessionIDFromMetadata(md); err == nil && sessionCookieValue != "" { + user, err := in.authenticateBySession(ctx, sessionCookieValue) + if err == nil && user != nil { + // Extract just the sessionID part for context storage + _, sessionID, parseErr := ParseSessionCookieValue(sessionCookieValue) + if parseErr != nil { + return nil, status.Errorf(codes.Internal, "failed to parse session cookie: %v", parseErr) + } + return in.handleAuthenticatedRequest(ctx, request, serverInfo, handler, user, sessionID, "") + } + } + + // Try to authenticate via JWT access token (from Authorization header) + if accessToken, err := getAccessTokenFromMetadata(md); err == nil && accessToken != "" { + user, err := in.authenticateByJWT(ctx, accessToken) + if err == nil && user != nil { + return in.handleAuthenticatedRequest(ctx, request, serverInfo, handler, user, "", accessToken) + } + } + + // If no valid authentication found, check if this method is in the allowlist (public endpoints) + if isUnauthorizeAllowedMethod(serverInfo.FullMethod) { + return handler(ctx, request) + } + + // If authentication is required but not found, reject the request + return nil, status.Errorf(codes.Unauthenticated, "authentication required") +} + +// handleAuthenticatedRequest processes an authenticated request with the given user and auth info. +func (in *GRPCAuthInterceptor) handleAuthenticatedRequest(ctx context.Context, request any, serverInfo *grpc.UnaryServerInfo, handler grpc.UnaryHandler, user *store.User, sessionID, accessToken string) (any, error) { + // Check user status + if user.RowStatus == store.Archived { + return nil, errors.Errorf("user %q is archived", user.Username) + } + if isOnlyForAdminAllowedMethod(serverInfo.FullMethod) && user.Role != store.RoleHost && user.Role != store.RoleAdmin { + return nil, errors.Errorf("user %q is not admin", user.Username) + } + + // Set context values + ctx = context.WithValue(ctx, userIDContextKey, user.ID) + + if sessionID != "" { + // Session-based authentication + ctx = context.WithValue(ctx, sessionIDContextKey, sessionID) + // Update session last accessed time + _ = in.updateSessionLastAccessed(ctx, user.ID, sessionID) + } else if accessToken != "" { + // JWT access token-based authentication + ctx = context.WithValue(ctx, accessTokenContextKey, accessToken) + } + + return handler(ctx, request) +} + +// authenticateByJWT authenticates a user using JWT access token from Authorization header. +func (in *GRPCAuthInterceptor) authenticateByJWT(ctx context.Context, accessToken string) (*store.User, error) { + if accessToken == "" { + return nil, status.Errorf(codes.Unauthenticated, "access token not found") + } + claims := &ClaimsMessage{} + _, err := jwt.ParseWithClaims(accessToken, claims, func(t *jwt.Token) (any, error) { + if t.Method.Alg() != jwt.SigningMethodHS256.Name { + return nil, status.Errorf(codes.Unauthenticated, "unexpected access token signing method=%v, expect %v", t.Header["alg"], jwt.SigningMethodHS256) + } + if kid, ok := t.Header["kid"].(string); ok { + if kid == "v1" { + return []byte(in.secret), nil + } + } + return nil, status.Errorf(codes.Unauthenticated, "unexpected access token kid=%v", t.Header["kid"]) + }) + if err != nil { + return nil, status.Errorf(codes.Unauthenticated, "Invalid or expired access token") + } + + // Get user from JWT claims + userID, err := util.ConvertStringToInt32(claims.Subject) + if err != nil { + return nil, errors.Wrap(err, "malformed ID in the token") + } + user, err := in.Store.GetUser(ctx, &store.FindUser{ + ID: &userID, + }) + if err != nil { + return nil, errors.Wrap(err, "failed to get user") + } + if user == nil { + return nil, errors.Errorf("user %q not exists", userID) + } + if user.RowStatus == store.Archived { + return nil, errors.Errorf("user %q is archived", userID) + } + + // Validate that this access token exists in the user's access tokens + accessTokens, err := in.Store.GetUserAccessTokens(ctx, user.ID) + if err != nil { + return nil, errors.Wrapf(err, "failed to get user access tokens") + } + if !validateAccessToken(accessToken, accessTokens) { + return nil, status.Errorf(codes.Unauthenticated, "invalid access token") + } + + return user, nil +} + +// authenticateBySession authenticates a user using session ID from cookie. +func (in *GRPCAuthInterceptor) authenticateBySession(ctx context.Context, sessionCookieValue string) (*store.User, error) { + if sessionCookieValue == "" { + return nil, status.Errorf(codes.Unauthenticated, "session cookie value not found") + } + + // Parse the cookie value to extract userID and sessionID + userID, sessionID, err := ParseSessionCookieValue(sessionCookieValue) + if err != nil { + return nil, status.Errorf(codes.Unauthenticated, "invalid session cookie format: %v", err) + } + + // Get the user directly using the userID from the cookie + user, err := in.Store.GetUser(ctx, &store.FindUser{ + ID: &userID, + }) + if err != nil { + return nil, errors.Wrap(err, "failed to get user") + } + if user == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not found") + } + if user.RowStatus == store.Archived { + return nil, status.Errorf(codes.Unauthenticated, "user is archived") + } + + // Get user sessions and validate the sessionID + sessions, err := in.Store.GetUserSessions(ctx, userID) + if err != nil { + return nil, errors.Wrap(err, "failed to get user sessions") + } + + if !validateUserSession(sessionID, sessions) { + return nil, status.Errorf(codes.Unauthenticated, "invalid or expired session") + } + + return user, nil +} + +// updateSessionLastAccessed updates the last accessed time for a user session. +func (in *GRPCAuthInterceptor) updateSessionLastAccessed(ctx context.Context, userID int32, sessionID string) error { + return in.Store.UpdateUserSessionLastAccessed(ctx, userID, sessionID, timestamppb.Now()) +} + +// validateUserSession checks if a session exists and is still valid using sliding expiration. +func validateUserSession(sessionID string, userSessions []*storepb.SessionsUserSetting_Session) bool { + for _, session := range userSessions { + if sessionID == session.SessionId { + // Use sliding expiration: check if last_accessed_time + 2 weeks > current_time + if session.LastAccessedTime != nil { + expirationTime := session.LastAccessedTime.AsTime().Add(SessionSlidingDuration) + if expirationTime.Before(time.Now()) { + return false + } + } + return true + } + } + return false +} + +// getSessionIDFromMetadata extracts session cookie value from cookie. +func getSessionIDFromMetadata(md metadata.MD) (string, error) { + // Check the cookie header for session cookie value + var sessionCookieValue string + for _, t := range append(md.Get("grpcgateway-cookie"), md.Get("cookie")...) { + header := http.Header{} + header.Add("Cookie", t) + request := http.Request{Header: header} + if v, _ := request.Cookie(SessionCookieName); v != nil { + sessionCookieValue = v.Value + } + } + if sessionCookieValue == "" { + return "", errors.New("session cookie not found") + } + return sessionCookieValue, nil +} + +// getAccessTokenFromMetadata extracts access token from Authorization header. +func getAccessTokenFromMetadata(md metadata.MD) (string, error) { + // Check the HTTP request Authorization header. + authorizationHeaders := md.Get("Authorization") + if len(authorizationHeaders) == 0 { + return "", errors.New("authorization header not found") + } + authHeaderParts := strings.Fields(authorizationHeaders[0]) + if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" { + return "", errors.New("authorization header format must be Bearer {token}") + } + return authHeaderParts[1], nil +} + +func validateAccessToken(accessTokenString string, userAccessTokens []*storepb.AccessTokensUserSetting_AccessToken) bool { + for _, userAccessToken := range userAccessTokens { + if accessTokenString == userAccessToken.AccessToken { + return true + } + } + return false +} diff --git a/server/router/api/v1/acl_config.go b/server/router/api/v1/acl_config.go new file mode 100644 index 0000000..c9f64ac --- /dev/null +++ b/server/router/api/v1/acl_config.go @@ -0,0 +1,34 @@ +package v1 + +var authenticationAllowlistMethods = map[string]bool{ + "/memos.api.v1.WorkspaceService/GetWorkspaceProfile": true, + "/memos.api.v1.WorkspaceService/GetWorkspaceSetting": true, + "/memos.api.v1.IdentityProviderService/ListIdentityProviders": true, + "/memos.api.v1.AuthService/CreateSession": true, + "/memos.api.v1.AuthService/GetCurrentSession": true, + "/memos.api.v1.UserService/CreateUser": true, + "/memos.api.v1.UserService/GetUser": true, + "/memos.api.v1.UserService/GetUserAvatar": true, + "/memos.api.v1.UserService/GetUserStats": true, + "/memos.api.v1.UserService/ListAllUserStats": true, + "/memos.api.v1.UserService/SearchUsers": true, + "/memos.api.v1.MemoService/GetMemo": true, + "/memos.api.v1.MemoService/ListMemos": true, + "/memos.api.v1.MarkdownService/GetLinkMetadata": true, + "/memos.api.v1.AttachmentService/GetAttachmentBinary": true, +} + +// isUnauthorizeAllowedMethod returns whether the method is exempted from authentication. +func isUnauthorizeAllowedMethod(fullMethodName string) bool { + return authenticationAllowlistMethods[fullMethodName] +} + +var allowedMethodsOnlyForAdmin = map[string]bool{ + "/memos.api.v1.UserService/CreateUser": true, + "/memos.api.v1.WorkspaceService/UpdateWorkspaceSetting": true, +} + +// isOnlyForAdminAllowedMethod returns true if the method is allowed to be called only by admin. +func isOnlyForAdminAllowedMethod(methodName string) bool { + return allowedMethodsOnlyForAdmin[methodName] +} diff --git a/server/router/api/v1/activity_service.go b/server/router/api/v1/activity_service.go new file mode 100644 index 0000000..1731cf2 --- /dev/null +++ b/server/router/api/v1/activity_service.go @@ -0,0 +1,126 @@ +package v1 + +import ( + "context" + "fmt" + "time" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" + + v1pb "github.com/usememos/memos/proto/gen/api/v1" + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func (s *APIV1Service) ListActivities(ctx context.Context, request *v1pb.ListActivitiesRequest) (*v1pb.ListActivitiesResponse, error) { + // Set default page size if not specified + pageSize := request.PageSize + if pageSize <= 0 || pageSize > 1000 { + pageSize = 100 + } + + // TODO: Implement pagination with page_token and use pageSize for limiting + // For now, we'll fetch all activities and the pageSize will be used in future pagination implementation + _ = pageSize // Acknowledge pageSize variable to avoid linter warning + + activities, err := s.Store.ListActivities(ctx, &store.FindActivity{}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list activities: %v", err) + } + + var activityMessages []*v1pb.Activity + for _, activity := range activities { + activityMessage, err := s.convertActivityFromStore(ctx, activity) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to convert activity from store: %v", err) + } + activityMessages = append(activityMessages, activityMessage) + } + + return &v1pb.ListActivitiesResponse{ + Activities: activityMessages, + // TODO: Implement next_page_token for pagination + }, nil +} + +func (s *APIV1Service) GetActivity(ctx context.Context, request *v1pb.GetActivityRequest) (*v1pb.Activity, error) { + activityID, err := ExtractActivityIDFromName(request.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid activity name: %v", err) + } + activity, err := s.Store.GetActivity(ctx, &store.FindActivity{ + ID: &activityID, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get activity: %v", err) + } + + activityMessage, err := s.convertActivityFromStore(ctx, activity) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to convert activity from store: %v", err) + } + return activityMessage, nil +} + +func (s *APIV1Service) convertActivityFromStore(ctx context.Context, activity *store.Activity) (*v1pb.Activity, error) { + payload, err := s.convertActivityPayloadFromStore(ctx, activity.Payload) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to convert activity payload from store: %v", err) + } + + // Convert store activity type to proto enum + var activityType v1pb.Activity_Type + switch activity.Type { + case store.ActivityTypeMemoComment: + activityType = v1pb.Activity_MEMO_COMMENT + default: + activityType = v1pb.Activity_TYPE_UNSPECIFIED + } + + // Convert store activity level to proto enum + var activityLevel v1pb.Activity_Level + switch activity.Level { + case store.ActivityLevelInfo: + activityLevel = v1pb.Activity_INFO + default: + activityLevel = v1pb.Activity_LEVEL_UNSPECIFIED + } + + return &v1pb.Activity{ + Name: fmt.Sprintf("%s%d", ActivityNamePrefix, activity.ID), + Creator: fmt.Sprintf("%s%d", UserNamePrefix, activity.CreatorID), + Type: activityType, + Level: activityLevel, + CreateTime: timestamppb.New(time.Unix(activity.CreatedTs, 0)), + Payload: payload, + }, nil +} + +func (s *APIV1Service) convertActivityPayloadFromStore(ctx context.Context, payload *storepb.ActivityPayload) (*v1pb.ActivityPayload, error) { + v2Payload := &v1pb.ActivityPayload{} + if payload.MemoComment != nil { + memo, err := s.Store.GetMemo(ctx, &store.FindMemo{ + ID: &payload.MemoComment.MemoId, + ExcludeContent: true, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get memo: %v", err) + } + relatedMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{ + ID: &payload.MemoComment.RelatedMemoId, + ExcludeContent: true, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get related memo: %v", err) + } + v2Payload.Payload = &v1pb.ActivityPayload_MemoComment{ + MemoComment: &v1pb.ActivityMemoCommentPayload{ + Memo: fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID), + RelatedMemo: fmt.Sprintf("%s%s", MemoNamePrefix, relatedMemo.UID), + }, + } + } + return v2Payload, nil +} diff --git a/server/router/api/v1/attachment_service.go b/server/router/api/v1/attachment_service.go new file mode 100644 index 0000000..07e29da --- /dev/null +++ b/server/router/api/v1/attachment_service.go @@ -0,0 +1,673 @@ +package v1 + +import ( + "bytes" + "context" + "encoding/binary" + "fmt" + "io" + "log/slog" + "os" + "path/filepath" + "regexp" + "strconv" + "strings" + "time" + + "github.com/disintegration/imaging" + "github.com/lithammer/shortuuid/v4" + "github.com/pkg/errors" + "google.golang.org/genproto/googleapis/api/httpbody" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/usememos/memos/internal/profile" + "github.com/usememos/memos/internal/util" + "github.com/usememos/memos/plugin/storage/s3" + v1pb "github.com/usememos/memos/proto/gen/api/v1" + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +const ( + // The upload memory buffer is 32 MiB. + // It should be kept low, so RAM usage doesn't get out of control. + // This is unrelated to maximum upload size limit, which is now set through system setting. + MaxUploadBufferSizeBytes = 32 << 20 + MebiByte = 1024 * 1024 + // ThumbnailCacheFolder is the folder name where the thumbnail images are stored. + ThumbnailCacheFolder = ".thumbnail_cache" +) + +var SupportedThumbnailMimeTypes = []string{ + "image/png", + "image/jpeg", +} + +func (s *APIV1Service) CreateAttachment(ctx context.Context, request *v1pb.CreateAttachmentRequest) (*v1pb.Attachment, error) { + user, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) + } + + // Validate required fields + if request.Attachment == nil { + return nil, status.Errorf(codes.InvalidArgument, "attachment is required") + } + if request.Attachment.Filename == "" { + return nil, status.Errorf(codes.InvalidArgument, "filename is required") + } + if request.Attachment.Type == "" { + return nil, status.Errorf(codes.InvalidArgument, "type is required") + } + + // Use provided attachment_id or generate a new one + attachmentUID := request.AttachmentId + if attachmentUID == "" { + attachmentUID = shortuuid.New() + } + + create := &store.Attachment{ + UID: attachmentUID, + CreatorID: user.ID, + Filename: request.Attachment.Filename, + Type: request.Attachment.Type, + } + + workspaceStorageSetting, err := s.Store.GetWorkspaceStorageSetting(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get workspace storage setting: %v", err) + } + size := binary.Size(request.Attachment.Content) + uploadSizeLimit := int(workspaceStorageSetting.UploadSizeLimitMb) * MebiByte + if uploadSizeLimit == 0 { + uploadSizeLimit = MaxUploadBufferSizeBytes + } + if size > uploadSizeLimit { + return nil, status.Errorf(codes.InvalidArgument, "file size exceeds the limit") + } + create.Size = int64(size) + create.Blob = request.Attachment.Content + + if err := SaveAttachmentBlob(ctx, s.Profile, s.Store, create); err != nil { + return nil, status.Errorf(codes.Internal, "failed to save attachment blob: %v", err) + } + + if request.Attachment.Memo != nil { + memoUID, err := ExtractMemoUIDFromName(*request.Attachment.Memo) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err) + } + memo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to find memo: %v", err) + } + if memo == nil { + return nil, status.Errorf(codes.NotFound, "memo not found: %s", *request.Attachment.Memo) + } + create.MemoID = &memo.ID + } + attachment, err := s.Store.CreateAttachment(ctx, create) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to create attachment: %v", err) + } + + return s.convertAttachmentFromStore(ctx, attachment), nil +} + +func (s *APIV1Service) ListAttachments(ctx context.Context, request *v1pb.ListAttachmentsRequest) (*v1pb.ListAttachmentsResponse, error) { + user, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) + } + + // Set default page size + pageSize := int(request.PageSize) + if pageSize <= 0 { + pageSize = 50 + } + if pageSize > 1000 { + pageSize = 1000 + } + + // Parse page token for offset + offset := 0 + if request.PageToken != "" { + // Simple implementation: page token is the offset as string + // In production, you might want to use encrypted tokens + if parsed, err := fmt.Sscanf(request.PageToken, "%d", &offset); err != nil || parsed != 1 { + return nil, status.Errorf(codes.InvalidArgument, "invalid page token") + } + } + + findAttachment := &store.FindAttachment{ + CreatorID: &user.ID, + Limit: &pageSize, + Offset: &offset, + } + + // Basic filter support for common cases + if request.Filter != "" { + // Simple filter parsing - can be enhanced later + // For now, support basic type filtering: "type=image/png" + if strings.HasPrefix(request.Filter, "type=") { + filterType := strings.TrimPrefix(request.Filter, "type=") + // Create a temporary struct to hold type filter + // Since FindAttachment doesn't have Type field, we'll apply this post-query + _ = filterType // We'll filter after getting results + } + } + + attachments, err := s.Store.ListAttachments(ctx, findAttachment) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list attachments: %v", err) + } + + // Apply type filter if specified + if request.Filter != "" && strings.HasPrefix(request.Filter, "type=") { + filterType := strings.TrimPrefix(request.Filter, "type=") + filteredAttachments := make([]*store.Attachment, 0) + for _, attachment := range attachments { + if attachment.Type == filterType { + filteredAttachments = append(filteredAttachments, attachment) + } + } + attachments = filteredAttachments + } + + response := &v1pb.ListAttachmentsResponse{} + + for _, attachment := range attachments { + response.Attachments = append(response.Attachments, s.convertAttachmentFromStore(ctx, attachment)) + } + + // For simplicity, set total size to the number of returned attachments. + // In a full implementation, you'd want a separate count query + response.TotalSize = int32(len(response.Attachments)) + + // Set next page token if we got the full page size (indicating there might be more) + if len(attachments) == pageSize { + response.NextPageToken = fmt.Sprintf("%d", offset+pageSize) + } + + return response, nil +} + +func (s *APIV1Service) GetAttachment(ctx context.Context, request *v1pb.GetAttachmentRequest) (*v1pb.Attachment, error) { + attachmentUID, err := ExtractAttachmentUIDFromName(request.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid attachment id: %v", err) + } + attachment, err := s.Store.GetAttachment(ctx, &store.FindAttachment{UID: &attachmentUID}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get attachment: %v", err) + } + if attachment == nil { + return nil, status.Errorf(codes.NotFound, "attachment not found") + } + return s.convertAttachmentFromStore(ctx, attachment), nil +} + +func (s *APIV1Service) GetAttachmentBinary(ctx context.Context, request *v1pb.GetAttachmentBinaryRequest) (*httpbody.HttpBody, error) { + attachmentUID, err := ExtractAttachmentUIDFromName(request.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid attachment id: %v", err) + } + attachment, err := s.Store.GetAttachment(ctx, &store.FindAttachment{ + GetBlob: true, + UID: &attachmentUID, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get attachment: %v", err) + } + if attachment == nil { + return nil, status.Errorf(codes.NotFound, "attachment not found") + } + // Check the related memo visibility. + if attachment.MemoID != nil { + memo, err := s.Store.GetMemo(ctx, &store.FindMemo{ + ID: attachment.MemoID, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to find memo by ID: %v", attachment.MemoID) + } + if memo != nil && memo.Visibility != store.Public { + user, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) + } + if user == nil { + return nil, status.Errorf(codes.Unauthenticated, "unauthorized access") + } + if memo.Visibility == store.Private && user.ID != attachment.CreatorID { + return nil, status.Errorf(codes.Unauthenticated, "unauthorized access") + } + } + } + + if request.Thumbnail && util.HasPrefixes(attachment.Type, SupportedThumbnailMimeTypes...) { + thumbnailBlob, err := s.getOrGenerateThumbnail(attachment) + if err != nil { + // thumbnail failures are logged as warnings and not cosidered critical failures as + // a attachment image can be used in its place. + slog.Warn("failed to get attachment thumbnail image", slog.Any("error", err)) + } else { + return &httpbody.HttpBody{ + ContentType: attachment.Type, + Data: thumbnailBlob, + }, nil + } + } + + blob, err := s.GetAttachmentBlob(attachment) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get attachment blob: %v", err) + } + + contentType := attachment.Type + if strings.HasPrefix(contentType, "text/") { + contentType += "; charset=utf-8" + } + // Prevent XSS attacks by serving potentially unsafe files with a content type that prevents script execution. + if strings.EqualFold(contentType, "image/svg+xml") || + strings.EqualFold(contentType, "text/html") || + strings.EqualFold(contentType, "application/xhtml+xml") { + contentType = "application/octet-stream" + } + + // Extract range header from gRPC metadata for iOS Safari video support + var rangeHeader string + if md, ok := metadata.FromIncomingContext(ctx); ok { + // Check for range header from gRPC-Gateway + if ranges := md.Get("grpcgateway-range"); len(ranges) > 0 { + rangeHeader = ranges[0] + } else if ranges := md.Get("range"); len(ranges) > 0 { + rangeHeader = ranges[0] + } + + // Log for debugging iOS Safari issues + if userAgents := md.Get("user-agent"); len(userAgents) > 0 { + userAgent := userAgents[0] + if strings.Contains(strings.ToLower(userAgent), "safari") && rangeHeader != "" { + slog.Debug("Safari range request detected", + slog.String("range", rangeHeader), + slog.String("user-agent", userAgent), + slog.String("content-type", contentType)) + } + } + } + + // Handle range requests for video/audio streaming (iOS Safari requirement) + if rangeHeader != "" && (strings.HasPrefix(contentType, "video/") || strings.HasPrefix(contentType, "audio/")) { + return s.handleRangeRequest(ctx, blob, rangeHeader, contentType) + } + + // Set headers for streaming support + if strings.HasPrefix(contentType, "video/") || strings.HasPrefix(contentType, "audio/") { + if err := setResponseHeaders(ctx, map[string]string{ + "accept-ranges": "bytes", + "content-length": fmt.Sprintf("%d", len(blob)), + "cache-control": "public, max-age=3600", // 1 hour cache + }); err != nil { + slog.Warn("failed to set streaming headers", slog.Any("error", err)) + } + } + + return &httpbody.HttpBody{ + ContentType: contentType, + Data: blob, + }, nil +} + +func (s *APIV1Service) UpdateAttachment(ctx context.Context, request *v1pb.UpdateAttachmentRequest) (*v1pb.Attachment, error) { + attachmentUID, err := ExtractAttachmentUIDFromName(request.Attachment.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid attachment id: %v", err) + } + if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 { + return nil, status.Errorf(codes.InvalidArgument, "update mask is required") + } + attachment, err := s.Store.GetAttachment(ctx, &store.FindAttachment{UID: &attachmentUID}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get attachment: %v", err) + } + + currentTs := time.Now().Unix() + update := &store.UpdateAttachment{ + ID: attachment.ID, + UpdatedTs: ¤tTs, + } + for _, field := range request.UpdateMask.Paths { + if field == "filename" { + update.Filename = &request.Attachment.Filename + } + } + + if err := s.Store.UpdateAttachment(ctx, update); err != nil { + return nil, status.Errorf(codes.Internal, "failed to update attachment: %v", err) + } + return s.GetAttachment(ctx, &v1pb.GetAttachmentRequest{ + Name: request.Attachment.Name, + }) +} + +func (s *APIV1Service) DeleteAttachment(ctx context.Context, request *v1pb.DeleteAttachmentRequest) (*emptypb.Empty, error) { + attachmentUID, err := ExtractAttachmentUIDFromName(request.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid attachment id: %v", err) + } + user, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) + } + attachment, err := s.Store.GetAttachment(ctx, &store.FindAttachment{ + UID: &attachmentUID, + CreatorID: &user.ID, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to find attachment: %v", err) + } + if attachment == nil { + return nil, status.Errorf(codes.NotFound, "attachment not found") + } + // Delete the attachment from the database. + if err := s.Store.DeleteAttachment(ctx, &store.DeleteAttachment{ + ID: attachment.ID, + }); err != nil { + return nil, status.Errorf(codes.Internal, "failed to delete attachment: %v", err) + } + return &emptypb.Empty{}, nil +} + +func (s *APIV1Service) convertAttachmentFromStore(ctx context.Context, attachment *store.Attachment) *v1pb.Attachment { + attachmentMessage := &v1pb.Attachment{ + Name: fmt.Sprintf("%s%s", AttachmentNamePrefix, attachment.UID), + CreateTime: timestamppb.New(time.Unix(attachment.CreatedTs, 0)), + Filename: attachment.Filename, + Type: attachment.Type, + Size: attachment.Size, + } + if attachment.StorageType == storepb.AttachmentStorageType_EXTERNAL || attachment.StorageType == storepb.AttachmentStorageType_S3 { + attachmentMessage.ExternalLink = attachment.Reference + } + if attachment.MemoID != nil { + memo, _ := s.Store.GetMemo(ctx, &store.FindMemo{ + ID: attachment.MemoID, + }) + if memo != nil { + memoName := fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID) + attachmentMessage.Memo = &memoName + } + } + + return attachmentMessage +} + +// SaveAttachmentBlob save the blob of attachment based on the storage config. +func SaveAttachmentBlob(ctx context.Context, profile *profile.Profile, stores *store.Store, create *store.Attachment) error { + workspaceStorageSetting, err := stores.GetWorkspaceStorageSetting(ctx) + if err != nil { + return errors.Wrap(err, "Failed to find workspace storage setting") + } + + if workspaceStorageSetting.StorageType == storepb.WorkspaceStorageSetting_LOCAL { + filepathTemplate := "assets/{timestamp}_{filename}" + if workspaceStorageSetting.FilepathTemplate != "" { + filepathTemplate = workspaceStorageSetting.FilepathTemplate + } + + internalPath := filepathTemplate + if !strings.Contains(internalPath, "{filename}") { + internalPath = filepath.Join(internalPath, "{filename}") + } + internalPath = replaceFilenameWithPathTemplate(internalPath, create.Filename) + internalPath = filepath.ToSlash(internalPath) + + // Ensure the directory exists. + osPath := filepath.FromSlash(internalPath) + if !filepath.IsAbs(osPath) { + osPath = filepath.Join(profile.Data, osPath) + } + dir := filepath.Dir(osPath) + if err = os.MkdirAll(dir, os.ModePerm); err != nil { + return errors.Wrap(err, "Failed to create directory") + } + dst, err := os.Create(osPath) + if err != nil { + return errors.Wrap(err, "Failed to create file") + } + defer dst.Close() + + // Write the blob to the file. + if err := os.WriteFile(osPath, create.Blob, 0644); err != nil { + return errors.Wrap(err, "Failed to write file") + } + create.Reference = internalPath + create.Blob = nil + create.StorageType = storepb.AttachmentStorageType_LOCAL + } else if workspaceStorageSetting.StorageType == storepb.WorkspaceStorageSetting_S3 { + s3Config := workspaceStorageSetting.S3Config + if s3Config == nil { + return errors.Errorf("No actived external storage found") + } + s3Client, err := s3.NewClient(ctx, s3Config) + if err != nil { + return errors.Wrap(err, "Failed to create s3 client") + } + + filepathTemplate := workspaceStorageSetting.FilepathTemplate + if !strings.Contains(filepathTemplate, "{filename}") { + filepathTemplate = filepath.Join(filepathTemplate, "{filename}") + } + filepathTemplate = replaceFilenameWithPathTemplate(filepathTemplate, create.Filename) + key, err := s3Client.UploadObject(ctx, filepathTemplate, create.Type, bytes.NewReader(create.Blob)) + if err != nil { + return errors.Wrap(err, "Failed to upload via s3 client") + } + presignURL, err := s3Client.PresignGetObject(ctx, key) + if err != nil { + return errors.Wrap(err, "Failed to presign via s3 client") + } + + create.Reference = presignURL + create.Blob = nil + create.StorageType = storepb.AttachmentStorageType_S3 + create.Payload = &storepb.AttachmentPayload{ + Payload: &storepb.AttachmentPayload_S3Object_{ + S3Object: &storepb.AttachmentPayload_S3Object{ + S3Config: s3Config, + Key: key, + LastPresignedTime: timestamppb.New(time.Now()), + }, + }, + } + } + + return nil +} + +func (s *APIV1Service) GetAttachmentBlob(attachment *store.Attachment) ([]byte, error) { + // For local storage, read the file from the local disk. + if attachment.StorageType == storepb.AttachmentStorageType_LOCAL { + attachmentPath := filepath.FromSlash(attachment.Reference) + if !filepath.IsAbs(attachmentPath) { + attachmentPath = filepath.Join(s.Profile.Data, attachmentPath) + } + + file, err := os.Open(attachmentPath) + if err != nil { + if os.IsNotExist(err) { + return nil, errors.Wrap(err, "file not found") + } + return nil, errors.Wrap(err, "failed to open the file") + } + defer file.Close() + blob, err := io.ReadAll(file) + if err != nil { + return nil, errors.Wrap(err, "failed to read the file") + } + return blob, nil + } + // For database storage, return the blob from the database. + return attachment.Blob, nil +} + +const ( + // thumbnailRatio is the ratio of the thumbnail image. + thumbnailRatio = 0.8 +) + +// getOrGenerateThumbnail returns the thumbnail image of the attachment. +func (s *APIV1Service) getOrGenerateThumbnail(attachment *store.Attachment) ([]byte, error) { + thumbnailCacheFolder := filepath.Join(s.Profile.Data, ThumbnailCacheFolder) + if err := os.MkdirAll(thumbnailCacheFolder, os.ModePerm); err != nil { + return nil, errors.Wrap(err, "failed to create thumbnail cache folder") + } + filePath := filepath.Join(thumbnailCacheFolder, fmt.Sprintf("%d%s", attachment.ID, filepath.Ext(attachment.Filename))) + if _, err := os.Stat(filePath); err != nil { + if !os.IsNotExist(err) { + return nil, errors.Wrap(err, "failed to check thumbnail image stat") + } + + // If thumbnail image does not exist, generate and save the thumbnail image. + blob, err := s.GetAttachmentBlob(attachment) + if err != nil { + return nil, errors.Wrap(err, "failed to get attachment blob") + } + img, err := imaging.Decode(bytes.NewReader(blob), imaging.AutoOrientation(true)) + if err != nil { + return nil, errors.Wrap(err, "failed to decode thumbnail image") + } + + thumbnailWidth := int(float64(img.Bounds().Dx()) * thumbnailRatio) + // Resize the image to the thumbnailWidth. + thumbnailImage := imaging.Resize(img, thumbnailWidth, 0, imaging.Lanczos) + if err := imaging.Save(thumbnailImage, filePath); err != nil { + return nil, errors.Wrap(err, "failed to save thumbnail file") + } + } + + thumbnailFile, err := os.Open(filePath) + if err != nil { + return nil, errors.Wrap(err, "failed to open thumbnail file") + } + defer thumbnailFile.Close() + blob, err := io.ReadAll(thumbnailFile) + if err != nil { + return nil, errors.Wrap(err, "failed to read thumbnail file") + } + return blob, nil +} + +var fileKeyPattern = regexp.MustCompile(`\{[a-z]{1,9}\}`) + +func replaceFilenameWithPathTemplate(path, filename string) string { + t := time.Now() + path = fileKeyPattern.ReplaceAllStringFunc(path, func(s string) string { + switch s { + case "{filename}": + return filename + case "{timestamp}": + return fmt.Sprintf("%d", t.Unix()) + case "{year}": + return fmt.Sprintf("%d", t.Year()) + case "{month}": + return fmt.Sprintf("%02d", t.Month()) + case "{day}": + return fmt.Sprintf("%02d", t.Day()) + case "{hour}": + return fmt.Sprintf("%02d", t.Hour()) + case "{minute}": + return fmt.Sprintf("%02d", t.Minute()) + case "{second}": + return fmt.Sprintf("%02d", t.Second()) + case "{uuid}": + return util.GenUUID() + } + return s + }) + return path +} + +// handleRangeRequest handles HTTP range requests for video/audio streaming (iOS Safari requirement). +func (*APIV1Service) handleRangeRequest(ctx context.Context, data []byte, rangeHeader, contentType string) (*httpbody.HttpBody, error) { + // Parse "bytes=start-end" + if !strings.HasPrefix(rangeHeader, "bytes=") { + return nil, status.Errorf(codes.InvalidArgument, "invalid range header format") + } + + rangeSpec := strings.TrimPrefix(rangeHeader, "bytes=") + parts := strings.Split(rangeSpec, "-") + if len(parts) != 2 { + return nil, status.Errorf(codes.InvalidArgument, "invalid range specification") + } + + fileSize := int64(len(data)) + start, end := int64(0), fileSize-1 + + // Parse start position + if parts[0] != "" { + if s, err := strconv.ParseInt(parts[0], 10, 64); err == nil { + start = s + } else { + return nil, status.Errorf(codes.InvalidArgument, "invalid range start: %s", parts[0]) + } + } + + // Parse end position + if parts[1] != "" { + if e, err := strconv.ParseInt(parts[1], 10, 64); err == nil { + end = e + } else { + return nil, status.Errorf(codes.InvalidArgument, "invalid range end: %s", parts[1]) + } + } + + // Validate range + if start < 0 || end >= fileSize || start > end { + // Set Content-Range header for 416 response + if err := setResponseHeaders(ctx, map[string]string{ + "content-range": fmt.Sprintf("bytes */%d", fileSize), + }); err != nil { + slog.Warn("failed to set content-range header", slog.Any("error", err)) + } + return nil, status.Errorf(codes.OutOfRange, "requested range not satisfiable") + } + + // Set partial content headers (HTTP 206) + if err := setResponseHeaders(ctx, map[string]string{ + "accept-ranges": "bytes", + "content-range": fmt.Sprintf("bytes %d-%d/%d", start, end, fileSize), + "content-length": fmt.Sprintf("%d", end-start+1), + "cache-control": "public, max-age=3600", + }); err != nil { + slog.Warn("failed to set partial content headers", slog.Any("error", err)) + } + + // Extract the requested range + rangeData := data[start : end+1] + + slog.Debug("serving partial content", + slog.Int64("start", start), + slog.Int64("end", end), + slog.Int64("total", fileSize), + slog.Int("chunk_size", len(rangeData))) + + return &httpbody.HttpBody{ + ContentType: contentType, + Data: rangeData, + }, nil +} + +// setResponseHeaders is a helper function to set gRPC response headers. +func setResponseHeaders(ctx context.Context, headers map[string]string) error { + pairs := make([]string, 0, len(headers)*2) + for key, value := range headers { + pairs = append(pairs, key, value) + } + return grpc.SetHeader(ctx, metadata.Pairs(pairs...)) +} diff --git a/server/router/api/v1/auth.go b/server/router/api/v1/auth.go new file mode 100644 index 0000000..d3d5fbb --- /dev/null +++ b/server/router/api/v1/auth.go @@ -0,0 +1,91 @@ +package v1 + +import ( + "fmt" + "strings" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/pkg/errors" + + "github.com/usememos/memos/internal/util" +) + +const ( + // issuer is the issuer of the jwt token. + Issuer = "memos" + // Signing key section. For now, this is only used for signing, not for verifying since we only + // have 1 version. But it will be used to maintain backward compatibility if we change the signing mechanism. + KeyID = "v1" + // AccessTokenAudienceName is the audience name of the access token. + AccessTokenAudienceName = "user.access-token" + // SessionSlidingDuration is the sliding expiration duration for user sessions (2 weeks). + // Sessions are considered valid if last_accessed_time + SessionSlidingDuration > current_time. + SessionSlidingDuration = 14 * 24 * time.Hour + + // SessionCookieName is the cookie name of user session ID. + SessionCookieName = "user_session" +) + +type ClaimsMessage struct { + Name string `json:"name"` + jwt.RegisteredClaims +} + +// GenerateAccessToken generates an access token. +func GenerateAccessToken(username string, userID int32, expirationTime time.Time, secret []byte) (string, error) { + return generateToken(username, userID, AccessTokenAudienceName, expirationTime, secret) +} + +// generateToken generates a jwt token. +func generateToken(username string, userID int32, audience string, expirationTime time.Time, secret []byte) (string, error) { + registeredClaims := jwt.RegisteredClaims{ + Issuer: Issuer, + Audience: jwt.ClaimStrings{audience}, + IssuedAt: jwt.NewNumericDate(time.Now()), + Subject: fmt.Sprint(userID), + } + if !expirationTime.IsZero() { + registeredClaims.ExpiresAt = jwt.NewNumericDate(expirationTime) + } + + // Declare the token with the HS256 algorithm used for signing, and the claims. + token := jwt.NewWithClaims(jwt.SigningMethodHS256, &ClaimsMessage{ + Name: username, + RegisteredClaims: registeredClaims, + }) + token.Header["kid"] = KeyID + + // Create the JWT string. + tokenString, err := token.SignedString(secret) + if err != nil { + return "", err + } + + return tokenString, nil +} + +// GenerateSessionID generates a unique session ID using UUIDv4. +func GenerateSessionID() (string, error) { + return util.GenUUID(), nil +} + +// BuildSessionCookieValue builds the session cookie value in format {userID}-{sessionID}. +func BuildSessionCookieValue(userID int32, sessionID string) string { + return fmt.Sprintf("%d-%s", userID, sessionID) +} + +// ParseSessionCookieValue parses the session cookie value to extract userID and sessionID. +func ParseSessionCookieValue(cookieValue string) (int32, string, error) { + parts := strings.SplitN(cookieValue, "-", 2) + if len(parts) != 2 { + return 0, "", errors.New("invalid session cookie format") + } + + userID, err := util.ConvertStringToInt32(parts[0]) + if err != nil { + return 0, "", errors.Errorf("invalid user ID in session cookie: %v", err) + } + + return userID, parts[1], nil +} diff --git a/server/router/api/v1/auth_service.go b/server/router/api/v1/auth_service.go new file mode 100644 index 0000000..be34a26 --- /dev/null +++ b/server/router/api/v1/auth_service.go @@ -0,0 +1,502 @@ +package v1 + +import ( + "context" + "fmt" + "log/slog" + "regexp" + "strings" + "time" + + "github.com/pkg/errors" + "golang.org/x/crypto/bcrypt" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/usememos/memos/internal/util" + "github.com/usememos/memos/plugin/idp" + "github.com/usememos/memos/plugin/idp/oauth2" + v1pb "github.com/usememos/memos/proto/gen/api/v1" + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +const ( + unmatchedUsernameAndPasswordError = "unmatched username and password" +) + +func (s *APIV1Service) GetCurrentSession(ctx context.Context, _ *v1pb.GetCurrentSessionRequest) (*v1pb.GetCurrentSessionResponse, error) { + user, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err) + } + if user == nil { + // Clear auth cookies + if err := s.clearAuthCookies(ctx); err != nil { + return nil, status.Errorf(codes.Internal, "failed to clear auth cookies: %v", err) + } + return nil, status.Errorf(codes.Unauthenticated, "user not found") + } + + var lastAccessedAt *timestamppb.Timestamp + // Update session last accessed time if we have a session ID and get the current session info + if sessionID, ok := ctx.Value(sessionIDContextKey).(string); ok && sessionID != "" { + now := timestamppb.Now() + if err := s.Store.UpdateUserSessionLastAccessed(ctx, user.ID, sessionID, now); err != nil { + // Log error but don't fail the request + slog.Error("failed to update session last accessed time", "error", err) + } + lastAccessedAt = now + } + + return &v1pb.GetCurrentSessionResponse{ + User: convertUserFromStore(user), + LastAccessedAt: lastAccessedAt, + }, nil +} + +func (s *APIV1Service) CreateSession(ctx context.Context, request *v1pb.CreateSessionRequest) (*v1pb.CreateSessionResponse, error) { + var existingUser *store.User + if passwordCredentials := request.GetPasswordCredentials(); passwordCredentials != nil { + user, err := s.Store.GetUser(ctx, &store.FindUser{ + Username: &passwordCredentials.Username, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user, error: %v", err) + } + if user == nil { + return nil, status.Errorf(codes.InvalidArgument, unmatchedUsernameAndPasswordError) + } + // Compare the stored hashed password, with the hashed version of the password that was received. + if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(passwordCredentials.Password)); err != nil { + return nil, status.Errorf(codes.InvalidArgument, unmatchedUsernameAndPasswordError) + } + workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get workspace general setting, error: %v", err) + } + // Check if the password auth in is allowed. + if workspaceGeneralSetting.DisallowPasswordAuth && user.Role == store.RoleUser { + return nil, status.Errorf(codes.PermissionDenied, "password signin is not allowed") + } + existingUser = user + } else if ssoCredentials := request.GetSsoCredentials(); ssoCredentials != nil { + identityProvider, err := s.Store.GetIdentityProvider(ctx, &store.FindIdentityProvider{ + ID: &ssoCredentials.IdpId, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get identity provider, error: %v", err) + } + if identityProvider == nil { + return nil, status.Errorf(codes.InvalidArgument, "identity provider not found") + } + + var userInfo *idp.IdentityProviderUserInfo + if identityProvider.Type == storepb.IdentityProvider_OAUTH2 { + oauth2IdentityProvider, err := oauth2.NewIdentityProvider(identityProvider.Config.GetOauth2Config()) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to create oauth2 identity provider, error: %v", err) + } + token, err := oauth2IdentityProvider.ExchangeToken(ctx, ssoCredentials.RedirectUri, ssoCredentials.Code) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to exchange token, error: %v", err) + } + userInfo, err = oauth2IdentityProvider.UserInfo(token) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user info, error: %v", err) + } + } + + identifierFilter := identityProvider.IdentifierFilter + if identifierFilter != "" { + identifierFilterRegex, err := regexp.Compile(identifierFilter) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to compile identifier filter regex, error: %v", err) + } + if !identifierFilterRegex.MatchString(userInfo.Identifier) { + return nil, status.Errorf(codes.PermissionDenied, "identifier %s is not allowed", userInfo.Identifier) + } + } + + user, err := s.Store.GetUser(ctx, &store.FindUser{ + Username: &userInfo.Identifier, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user, error: %v", err) + } + if user == nil { + // Check if the user is allowed to sign up. + workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get workspace general setting, error: %v", err) + } + if workspaceGeneralSetting.DisallowUserRegistration { + return nil, status.Errorf(codes.PermissionDenied, "user registration is not allowed") + } + + // Create a new user with the user info from the identity provider. + userCreate := &store.User{ + Username: userInfo.Identifier, + // The new signup user should be normal user by default. + Role: store.RoleUser, + Nickname: userInfo.DisplayName, + Email: userInfo.Email, + AvatarURL: userInfo.AvatarURL, + } + password, err := util.RandomString(20) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to generate random password, error: %v", err) + } + passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to generate password hash, error: %v", err) + } + userCreate.PasswordHash = string(passwordHash) + user, err = s.Store.CreateUser(ctx, userCreate) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to create user, error: %v", err) + } + } + existingUser = user + } + + if existingUser == nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid credentials") + } + if existingUser.RowStatus == store.Archived { + return nil, status.Errorf(codes.PermissionDenied, "user has been archived with username %s", existingUser.Username) + } + + // Default session expiration time is 100 year + expireTime := time.Now().Add(100 * 365 * 24 * time.Hour) + if err := s.doSignIn(ctx, existingUser, expireTime); err != nil { + return nil, status.Errorf(codes.Internal, "failed to sign in, error: %v", err) + } + + return &v1pb.CreateSessionResponse{ + User: convertUserFromStore(existingUser), + LastAccessedAt: timestamppb.Now(), + }, nil +} + +func (s *APIV1Service) doSignIn(ctx context.Context, user *store.User, expireTime time.Time) error { + // Generate unique session ID for web use + sessionID, err := GenerateSessionID() + if err != nil { + return status.Errorf(codes.Internal, "failed to generate session ID, error: %v", err) + } + + // Track session in user settings + if err := s.trackUserSession(ctx, user.ID, sessionID); err != nil { + // Log the error but don't fail the login if session tracking fails + // This ensures backward compatibility + slog.Error("failed to track user session", "error", err) + } + + // Set session cookie for web use (format: userID-sessionID) + sessionCookieValue := BuildSessionCookieValue(user.ID, sessionID) + sessionCookie, err := s.buildSessionCookie(ctx, sessionCookieValue, expireTime) + if err != nil { + return status.Errorf(codes.Internal, "failed to build session cookie, error: %v", err) + } + if err := grpc.SetHeader(ctx, metadata.New(map[string]string{ + "Set-Cookie": sessionCookie, + })); err != nil { + return status.Errorf(codes.Internal, "failed to set grpc header, error: %v", err) + } + + return nil +} + +func (s *APIV1Service) DeleteSession(ctx context.Context, _ *v1pb.DeleteSessionRequest) (*emptypb.Empty, error) { + user, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Unauthenticated, "failed to get current user: %v", err) + } + if user == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not found") + } + + // Check if we have a session ID (from cookie-based auth) + if sessionID, ok := ctx.Value(sessionIDContextKey).(string); ok && sessionID != "" { + // Remove session from user settings + if err := s.Store.RemoveUserSession(ctx, user.ID, sessionID); err != nil { + slog.Error("failed to remove user session", "error", err) + } + } + + if err := s.clearAuthCookies(ctx); err != nil { + return nil, status.Errorf(codes.Internal, "failed to clear auth cookies, error: %v", err) + } + return &emptypb.Empty{}, nil +} + +func (s *APIV1Service) clearAuthCookies(ctx context.Context) error { + // Clear session cookie + sessionCookie, err := s.buildSessionCookie(ctx, "", time.Time{}) + if err != nil { + return errors.Wrap(err, "failed to build session cookie") + } + + // Set both cookies in the response + if err := grpc.SetHeader(ctx, metadata.New(map[string]string{ + "Set-Cookie": sessionCookie, + })); err != nil { + return errors.Wrap(err, "failed to set grpc header") + } + return nil +} + +func (*APIV1Service) buildSessionCookie(ctx context.Context, sessionCookieValue string, expireTime time.Time) (string, error) { + attrs := []string{ + fmt.Sprintf("%s=%s", SessionCookieName, sessionCookieValue), + "Path=/", + "HttpOnly", + } + if expireTime.IsZero() { + attrs = append(attrs, "Expires=Thu, 01 Jan 1970 00:00:00 GMT") + } else { + attrs = append(attrs, "Expires="+expireTime.Format(time.RFC1123)) + } + + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return "", errors.New("failed to get metadata from context") + } + var origin string + for _, v := range md.Get("origin") { + origin = v + } + isHTTPS := strings.HasPrefix(origin, "https://") + if isHTTPS { + attrs = append(attrs, "SameSite=None") + attrs = append(attrs, "Secure") + } else { + attrs = append(attrs, "SameSite=Strict") + } + return strings.Join(attrs, "; "), nil +} + +func (s *APIV1Service) GetCurrentUser(ctx context.Context) (*store.User, error) { + userID, ok := ctx.Value(userIDContextKey).(int32) + if !ok { + return nil, nil + } + user, err := s.Store.GetUser(ctx, &store.FindUser{ + ID: &userID, + }) + if err != nil { + return nil, err + } + if user == nil { + return nil, errors.Errorf("user %d not found", userID) + } + return user, nil +} + +// Helper function to track user session for session management. +func (s *APIV1Service) trackUserSession(ctx context.Context, userID int32, sessionID string) error { + // Extract client information from the context + clientInfo := s.extractClientInfo(ctx) + + session := &storepb.SessionsUserSetting_Session{ + SessionId: sessionID, + CreateTime: timestamppb.Now(), + LastAccessedTime: timestamppb.Now(), + ClientInfo: clientInfo, + } + + return s.Store.AddUserSession(ctx, userID, session) +} + +// Helper function to extract client information from the gRPC context. +// extractClientInfo extracts comprehensive client information from the request context. +// This includes user agent parsing to determine device type, operating system, browser, +// and IP address extraction. This information is used to provide detailed session +// tracking and management capabilities in the web UI. +// +// Fields populated: +// - UserAgent: Raw user agent string +// - IpAddress: Client IP (from X-Forwarded-For or X-Real-IP headers) +// - DeviceType: "mobile", "tablet", or "desktop" +// - Os: Operating system name and version (e.g., "iOS 17.1", "Windows 10/11") +// - Browser: Browser name and version (e.g., "Chrome 120.0.0.0") +// - Country: Geographic location (TODO: implement with GeoIP service). +func (s *APIV1Service) extractClientInfo(ctx context.Context) *storepb.SessionsUserSetting_ClientInfo { + clientInfo := &storepb.SessionsUserSetting_ClientInfo{} + + // Extract user agent from metadata if available + if md, ok := metadata.FromIncomingContext(ctx); ok { + if userAgents := md.Get("user-agent"); len(userAgents) > 0 { + userAgent := userAgents[0] + clientInfo.UserAgent = userAgent + + // Parse user agent to extract device type, OS, browser info + s.parseUserAgent(userAgent, clientInfo) + } + if forwardedFor := md.Get("x-forwarded-for"); len(forwardedFor) > 0 { + ipAddress := strings.Split(forwardedFor[0], ",")[0] // Get the first IP in case of multiple + ipAddress = strings.TrimSpace(ipAddress) + clientInfo.IpAddress = ipAddress + } else if realIP := md.Get("x-real-ip"); len(realIP) > 0 { + clientInfo.IpAddress = realIP[0] + } + } + + return clientInfo +} + +// parseUserAgent extracts device type, OS, and browser information from user agent string. +func (*APIV1Service) parseUserAgent(userAgent string, clientInfo *storepb.SessionsUserSetting_ClientInfo) { + if userAgent == "" { + return + } + + userAgent = strings.ToLower(userAgent) + + // Detect device type + if strings.Contains(userAgent, "ipad") { + clientInfo.DeviceType = "tablet" + } else if strings.Contains(userAgent, "mobile") || strings.Contains(userAgent, "android") || + strings.Contains(userAgent, "iphone") || strings.Contains(userAgent, "ipod") || + strings.Contains(userAgent, "windows phone") || strings.Contains(userAgent, "blackberry") { + clientInfo.DeviceType = "mobile" + } else if strings.Contains(userAgent, "tablet") { + clientInfo.DeviceType = "tablet" + } else { + clientInfo.DeviceType = "desktop" + } + + // Detect operating system + if strings.Contains(userAgent, "iphone os") || strings.Contains(userAgent, "cpu os") { + // Extract iOS version + if idx := strings.Index(userAgent, "cpu os "); idx != -1 { + versionStart := idx + 7 + versionEnd := strings.Index(userAgent[versionStart:], " ") + if versionEnd != -1 { + version := strings.ReplaceAll(userAgent[versionStart:versionStart+versionEnd], "_", ".") + clientInfo.Os = "iOS " + version + } else { + clientInfo.Os = "iOS" + } + } else if idx := strings.Index(userAgent, "iphone os "); idx != -1 { + versionStart := idx + 10 + versionEnd := strings.Index(userAgent[versionStart:], " ") + if versionEnd != -1 { + version := strings.ReplaceAll(userAgent[versionStart:versionStart+versionEnd], "_", ".") + clientInfo.Os = "iOS " + version + } else { + clientInfo.Os = "iOS" + } + } else { + clientInfo.Os = "iOS" + } + } else if strings.Contains(userAgent, "android") { + // Extract Android version + if idx := strings.Index(userAgent, "android "); idx != -1 { + versionStart := idx + 8 + versionEnd := strings.Index(userAgent[versionStart:], ";") + if versionEnd == -1 { + versionEnd = strings.Index(userAgent[versionStart:], ")") + } + if versionEnd != -1 { + version := userAgent[versionStart : versionStart+versionEnd] + clientInfo.Os = "Android " + version + } else { + clientInfo.Os = "Android" + } + } else { + clientInfo.Os = "Android" + } + } else if strings.Contains(userAgent, "windows nt 10.0") { + clientInfo.Os = "Windows 10/11" + } else if strings.Contains(userAgent, "windows nt 6.3") { + clientInfo.Os = "Windows 8.1" + } else if strings.Contains(userAgent, "windows nt 6.1") { + clientInfo.Os = "Windows 7" + } else if strings.Contains(userAgent, "windows") { + clientInfo.Os = "Windows" + } else if strings.Contains(userAgent, "mac os x") { + // Extract macOS version + if idx := strings.Index(userAgent, "mac os x "); idx != -1 { + versionStart := idx + 9 + versionEnd := strings.Index(userAgent[versionStart:], ";") + if versionEnd == -1 { + versionEnd = strings.Index(userAgent[versionStart:], ")") + } + if versionEnd != -1 { + version := strings.ReplaceAll(userAgent[versionStart:versionStart+versionEnd], "_", ".") + clientInfo.Os = "macOS " + version + } else { + clientInfo.Os = "macOS" + } + } else { + clientInfo.Os = "macOS" + } + } else if strings.Contains(userAgent, "linux") { + clientInfo.Os = "Linux" + } else if strings.Contains(userAgent, "cros") { + clientInfo.Os = "Chrome OS" + } + + // Detect browser + if strings.Contains(userAgent, "edg/") { + // Extract Edge version + if idx := strings.Index(userAgent, "edg/"); idx != -1 { + versionStart := idx + 4 + versionEnd := strings.Index(userAgent[versionStart:], " ") + if versionEnd == -1 { + versionEnd = len(userAgent) - versionStart + } + version := userAgent[versionStart : versionStart+versionEnd] + clientInfo.Browser = "Edge " + version + } else { + clientInfo.Browser = "Edge" + } + } else if strings.Contains(userAgent, "chrome/") && !strings.Contains(userAgent, "edg") { + // Extract Chrome version + if idx := strings.Index(userAgent, "chrome/"); idx != -1 { + versionStart := idx + 7 + versionEnd := strings.Index(userAgent[versionStart:], " ") + if versionEnd == -1 { + versionEnd = len(userAgent) - versionStart + } + version := userAgent[versionStart : versionStart+versionEnd] + clientInfo.Browser = "Chrome " + version + } else { + clientInfo.Browser = "Chrome" + } + } else if strings.Contains(userAgent, "firefox/") { + // Extract Firefox version + if idx := strings.Index(userAgent, "firefox/"); idx != -1 { + versionStart := idx + 8 + versionEnd := strings.Index(userAgent[versionStart:], " ") + if versionEnd == -1 { + versionEnd = len(userAgent) - versionStart + } + version := userAgent[versionStart : versionStart+versionEnd] + clientInfo.Browser = "Firefox " + version + } else { + clientInfo.Browser = "Firefox" + } + } else if strings.Contains(userAgent, "safari/") && !strings.Contains(userAgent, "chrome") && !strings.Contains(userAgent, "edg") { + // Extract Safari version + if idx := strings.Index(userAgent, "version/"); idx != -1 { + versionStart := idx + 8 + versionEnd := strings.Index(userAgent[versionStart:], " ") + if versionEnd == -1 { + versionEnd = len(userAgent) - versionStart + } + version := userAgent[versionStart : versionStart+versionEnd] + clientInfo.Browser = "Safari " + version + } else { + clientInfo.Browser = "Safari" + } + } else if strings.Contains(userAgent, "opera/") || strings.Contains(userAgent, "opr/") { + clientInfo.Browser = "Opera" + } +} diff --git a/server/router/api/v1/auth_service_client_info_test.go b/server/router/api/v1/auth_service_client_info_test.go new file mode 100644 index 0000000..82429e7 --- /dev/null +++ b/server/router/api/v1/auth_service_client_info_test.go @@ -0,0 +1,179 @@ +package v1 + +import ( + "context" + "testing" + + "google.golang.org/grpc/metadata" + + storepb "github.com/usememos/memos/proto/gen/store" +) + +func TestParseUserAgent(t *testing.T) { + service := &APIV1Service{} + + tests := []struct { + name string + userAgent string + expectedDevice string + expectedOS string + expectedBrowser string + }{ + { + name: "Chrome on Windows", + userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36", + expectedDevice: "desktop", + expectedOS: "Windows 10/11", + expectedBrowser: "Chrome 119.0.0.0", + }, + { + name: "Safari on macOS", + userAgent: "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15", + expectedDevice: "desktop", + expectedOS: "macOS 10.15.7", + expectedBrowser: "Safari 17.0", + }, + { + name: "Chrome on Android Mobile", + userAgent: "Mozilla/5.0 (Linux; Android 13; SM-G998B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Mobile Safari/537.36", + expectedDevice: "mobile", + expectedOS: "Android 13", + expectedBrowser: "Chrome 119.0.0.0", + }, + { + name: "Safari on iPhone", + userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1", + expectedDevice: "mobile", + expectedOS: "iOS 17.0", + expectedBrowser: "Safari 17.0", + }, + { + name: "Firefox on Windows", + userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0", + expectedDevice: "desktop", + expectedOS: "Windows 10/11", + expectedBrowser: "Firefox 119.0", + }, + { + name: "Edge on Windows", + userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36 Edg/119.0.0.0", + expectedDevice: "desktop", + expectedOS: "Windows 10/11", + expectedBrowser: "Edge 119.0.0.0", + }, + { + name: "iPad Safari", + userAgent: "Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1", + expectedDevice: "tablet", + expectedOS: "iOS 17.0", + expectedBrowser: "Safari 17.0", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + clientInfo := &storepb.SessionsUserSetting_ClientInfo{} + service.parseUserAgent(tt.userAgent, clientInfo) + + if clientInfo.DeviceType != tt.expectedDevice { + t.Errorf("Expected device type %s, got %s", tt.expectedDevice, clientInfo.DeviceType) + } + if clientInfo.Os != tt.expectedOS { + t.Errorf("Expected OS %s, got %s", tt.expectedOS, clientInfo.Os) + } + if clientInfo.Browser != tt.expectedBrowser { + t.Errorf("Expected browser %s, got %s", tt.expectedBrowser, clientInfo.Browser) + } + }) + } +} + +func TestExtractClientInfo(t *testing.T) { + service := &APIV1Service{} + + // Test with metadata containing user agent and IP + md := metadata.New(map[string]string{ + "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36", + "x-forwarded-for": "203.0.113.1, 198.51.100.1", + "x-real-ip": "203.0.113.1", + }) + + ctx := metadata.NewIncomingContext(context.Background(), md) + + clientInfo := service.extractClientInfo(ctx) + + if clientInfo.UserAgent == "" { + t.Error("Expected user agent to be set") + } + if clientInfo.IpAddress != "203.0.113.1" { + t.Errorf("Expected IP address to be 203.0.113.1, got %s", clientInfo.IpAddress) + } + if clientInfo.DeviceType != "desktop" { + t.Errorf("Expected device type to be desktop, got %s", clientInfo.DeviceType) + } + if clientInfo.Os != "Windows 10/11" { + t.Errorf("Expected OS to be Windows 10/11, got %s", clientInfo.Os) + } + if clientInfo.Browser != "Chrome 119.0.0.0" { + t.Errorf("Expected browser to be Chrome 119.0.0.0, got %s", clientInfo.Browser) + } +} + +// TestClientInfoExamples demonstrates the enhanced client info extraction with various user agents. +func TestClientInfoExamples(t *testing.T) { + service := &APIV1Service{} + + examples := []struct { + description string + userAgent string + }{ + { + description: "Modern Chrome on Windows 11", + userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + }, + { + description: "Safari on iPhone 15 Pro", + userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1", + }, + { + description: "Chrome on Samsung Galaxy", + userAgent: "Mozilla/5.0 (Linux; Android 14; SM-S918B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36", + }, + { + description: "Firefox on Ubuntu", + userAgent: "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/120.0", + }, + { + description: "Edge on Windows 10", + userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36 Edg/120.0.0.0", + }, + { + description: "Safari on iPad Air", + userAgent: "Mozilla/5.0 (iPad; CPU OS 17_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.1 Mobile/15E148 Safari/604.1", + }, + } + + for _, example := range examples { + t.Run(example.description, func(t *testing.T) { + clientInfo := &storepb.SessionsUserSetting_ClientInfo{} + service.parseUserAgent(example.userAgent, clientInfo) + + t.Logf("User Agent: %s", example.userAgent) + t.Logf("Device Type: %s", clientInfo.DeviceType) + t.Logf("Operating System: %s", clientInfo.Os) + t.Logf("Browser: %s", clientInfo.Browser) + t.Logf("---") + + // Ensure all fields are populated + if clientInfo.DeviceType == "" { + t.Error("Device type should not be empty") + } + if clientInfo.Os == "" { + t.Error("OS should not be empty") + } + if clientInfo.Browser == "" { + t.Error("Browser should not be empty") + } + }) + } +} diff --git a/server/router/api/v1/common.go b/server/router/api/v1/common.go new file mode 100644 index 0000000..1e19993 --- /dev/null +++ b/server/router/api/v1/common.go @@ -0,0 +1,70 @@ +package v1 + +import ( + "encoding/base64" + + "github.com/pkg/errors" + "google.golang.org/protobuf/proto" + + v1pb "github.com/usememos/memos/proto/gen/api/v1" + "github.com/usememos/memos/store" +) + +const ( + // DefaultPageSize is the default page size for requests. + DefaultPageSize = 10 + // MaxPageSize is the maximum page size for requests. + MaxPageSize = 1000 +) + +func convertStateFromStore(rowStatus store.RowStatus) v1pb.State { + switch rowStatus { + case store.Normal: + return v1pb.State_NORMAL + case store.Archived: + return v1pb.State_ARCHIVED + default: + return v1pb.State_STATE_UNSPECIFIED + } +} + +func convertStateToStore(state v1pb.State) store.RowStatus { + switch state { + case v1pb.State_NORMAL: + return store.Normal + case v1pb.State_ARCHIVED: + return store.Archived + default: + return store.Normal + } +} + +func getPageToken(limit int, offset int) (string, error) { + return marshalPageToken(&v1pb.PageToken{ + Limit: int32(limit), + Offset: int32(offset), + }) +} + +func marshalPageToken(pageToken *v1pb.PageToken) (string, error) { + b, err := proto.Marshal(pageToken) + if err != nil { + return "", errors.Wrapf(err, "failed to marshal page token") + } + return base64.StdEncoding.EncodeToString(b), nil +} + +func unmarshalPageToken(s string, pageToken *v1pb.PageToken) error { + b, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return errors.Wrapf(err, "failed to decode page token") + } + if err := proto.Unmarshal(b, pageToken); err != nil { + return errors.Wrapf(err, "failed to unmarshal page token") + } + return nil +} + +func isSuperUser(user *store.User) bool { + return user.Role == store.RoleAdmin || user.Role == store.RoleHost +} diff --git a/server/router/api/v1/health_service.go b/server/router/api/v1/health_service.go new file mode 100644 index 0000000..47a00c8 --- /dev/null +++ b/server/router/api/v1/health_service.go @@ -0,0 +1,21 @@ +package v1 + +import ( + "context" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/status" + + "github.com/usememos/memos/store" +) + +func (s *APIV1Service) Check(ctx context.Context, + _ *grpc_health_v1.HealthCheckRequest) (*grpc_health_v1.HealthCheckResponse, error) { + history, err := s.Store.GetDriver().FindMigrationHistoryList(ctx, &store.FindMigrationHistory{}) + if err != nil || len(history) == 0 { + return nil, status.Errorf(codes.Unavailable, "not available") + } + + return &grpc_health_v1.HealthCheckResponse{Status: grpc_health_v1.HealthCheckResponse_SERVING}, nil +} diff --git a/server/router/api/v1/idp_service.go b/server/router/api/v1/idp_service.go new file mode 100644 index 0000000..2c54ca6 --- /dev/null +++ b/server/router/api/v1/idp_service.go @@ -0,0 +1,183 @@ +package v1 + +import ( + "context" + "fmt" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + + v1pb "github.com/usememos/memos/proto/gen/api/v1" + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func (s *APIV1Service) CreateIdentityProvider(ctx context.Context, request *v1pb.CreateIdentityProviderRequest) (*v1pb.IdentityProvider, error) { + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) + } + if currentUser == nil || currentUser.Role != store.RoleHost { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + identityProvider, err := s.Store.CreateIdentityProvider(ctx, convertIdentityProviderToStore(request.IdentityProvider)) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to create identity provider, error: %+v", err) + } + return convertIdentityProviderFromStore(identityProvider), nil +} + +func (s *APIV1Service) ListIdentityProviders(ctx context.Context, _ *v1pb.ListIdentityProvidersRequest) (*v1pb.ListIdentityProvidersResponse, error) { + identityProviders, err := s.Store.ListIdentityProviders(ctx, &store.FindIdentityProvider{}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list identity providers, error: %+v", err) + } + + response := &v1pb.ListIdentityProvidersResponse{ + IdentityProviders: []*v1pb.IdentityProvider{}, + } + for _, identityProvider := range identityProviders { + response.IdentityProviders = append(response.IdentityProviders, convertIdentityProviderFromStore(identityProvider)) + } + return response, nil +} + +func (s *APIV1Service) GetIdentityProvider(ctx context.Context, request *v1pb.GetIdentityProviderRequest) (*v1pb.IdentityProvider, error) { + id, err := ExtractIdentityProviderIDFromName(request.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid identity provider name: %v", err) + } + identityProvider, err := s.Store.GetIdentityProvider(ctx, &store.FindIdentityProvider{ + ID: &id, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get identity provider, error: %+v", err) + } + if identityProvider == nil { + return nil, status.Errorf(codes.NotFound, "identity provider not found") + } + return convertIdentityProviderFromStore(identityProvider), nil +} + +func (s *APIV1Service) UpdateIdentityProvider(ctx context.Context, request *v1pb.UpdateIdentityProviderRequest) (*v1pb.IdentityProvider, error) { + if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 { + return nil, status.Errorf(codes.InvalidArgument, "update_mask is required") + } + + id, err := ExtractIdentityProviderIDFromName(request.IdentityProvider.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid identity provider name: %v", err) + } + update := &store.UpdateIdentityProviderV1{ + ID: id, + Type: storepb.IdentityProvider_Type(storepb.IdentityProvider_Type_value[request.IdentityProvider.Type.String()]), + } + for _, field := range request.UpdateMask.Paths { + switch field { + case "title": + update.Name = &request.IdentityProvider.Title + case "identifier_filter": + update.IdentifierFilter = &request.IdentityProvider.IdentifierFilter + case "config": + update.Config = convertIdentityProviderConfigToStore(request.IdentityProvider.Type, request.IdentityProvider.Config) + } + } + + identityProvider, err := s.Store.UpdateIdentityProvider(ctx, update) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to update identity provider, error: %+v", err) + } + return convertIdentityProviderFromStore(identityProvider), nil +} + +func (s *APIV1Service) DeleteIdentityProvider(ctx context.Context, request *v1pb.DeleteIdentityProviderRequest) (*emptypb.Empty, error) { + id, err := ExtractIdentityProviderIDFromName(request.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid identity provider name: %v", err) + } + + // Check if the identity provider exists before trying to delete it + identityProvider, err := s.Store.GetIdentityProvider(ctx, &store.FindIdentityProvider{ID: &id}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to check identity provider existence: %v", err) + } + if identityProvider == nil { + return nil, status.Errorf(codes.NotFound, "identity provider not found") + } + + if err := s.Store.DeleteIdentityProvider(ctx, &store.DeleteIdentityProvider{ID: id}); err != nil { + return nil, status.Errorf(codes.Internal, "failed to delete identity provider, error: %+v", err) + } + return &emptypb.Empty{}, nil +} + +func convertIdentityProviderFromStore(identityProvider *storepb.IdentityProvider) *v1pb.IdentityProvider { + temp := &v1pb.IdentityProvider{ + Name: fmt.Sprintf("%s%d", IdentityProviderNamePrefix, identityProvider.Id), + Title: identityProvider.Name, + IdentifierFilter: identityProvider.IdentifierFilter, + Type: v1pb.IdentityProvider_Type(v1pb.IdentityProvider_Type_value[identityProvider.Type.String()]), + } + if identityProvider.Type == storepb.IdentityProvider_OAUTH2 { + oauth2Config := identityProvider.Config.GetOauth2Config() + temp.Config = &v1pb.IdentityProviderConfig{ + Config: &v1pb.IdentityProviderConfig_Oauth2Config{ + Oauth2Config: &v1pb.OAuth2Config{ + ClientId: oauth2Config.ClientId, + ClientSecret: oauth2Config.ClientSecret, + AuthUrl: oauth2Config.AuthUrl, + TokenUrl: oauth2Config.TokenUrl, + UserInfoUrl: oauth2Config.UserInfoUrl, + Scopes: oauth2Config.Scopes, + FieldMapping: &v1pb.FieldMapping{ + Identifier: oauth2Config.FieldMapping.Identifier, + DisplayName: oauth2Config.FieldMapping.DisplayName, + Email: oauth2Config.FieldMapping.Email, + AvatarUrl: oauth2Config.FieldMapping.AvatarUrl, + }, + }, + }, + } + } + return temp +} + +func convertIdentityProviderToStore(identityProvider *v1pb.IdentityProvider) *storepb.IdentityProvider { + id, _ := ExtractIdentityProviderIDFromName(identityProvider.Name) + + temp := &storepb.IdentityProvider{ + Id: id, + Name: identityProvider.Title, + IdentifierFilter: identityProvider.IdentifierFilter, + Type: storepb.IdentityProvider_Type(storepb.IdentityProvider_Type_value[identityProvider.Type.String()]), + Config: convertIdentityProviderConfigToStore(identityProvider.Type, identityProvider.Config), + } + return temp +} + +func convertIdentityProviderConfigToStore(identityProviderType v1pb.IdentityProvider_Type, config *v1pb.IdentityProviderConfig) *storepb.IdentityProviderConfig { + if identityProviderType == v1pb.IdentityProvider_OAUTH2 { + oauth2Config := config.GetOauth2Config() + return &storepb.IdentityProviderConfig{ + Config: &storepb.IdentityProviderConfig_Oauth2Config{ + Oauth2Config: &storepb.OAuth2Config{ + ClientId: oauth2Config.ClientId, + ClientSecret: oauth2Config.ClientSecret, + AuthUrl: oauth2Config.AuthUrl, + TokenUrl: oauth2Config.TokenUrl, + UserInfoUrl: oauth2Config.UserInfoUrl, + Scopes: oauth2Config.Scopes, + FieldMapping: &storepb.FieldMapping{ + Identifier: oauth2Config.FieldMapping.Identifier, + DisplayName: oauth2Config.FieldMapping.DisplayName, + Email: oauth2Config.FieldMapping.Email, + AvatarUrl: oauth2Config.FieldMapping.AvatarUrl, + }, + }, + }, + } + } + return nil +} diff --git a/server/router/api/v1/inbox_service.go b/server/router/api/v1/inbox_service.go new file mode 100644 index 0000000..a14fb06 --- /dev/null +++ b/server/router/api/v1/inbox_service.go @@ -0,0 +1,226 @@ +package v1 + +import ( + "context" + "fmt" + "time" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/timestamppb" + + v1pb "github.com/usememos/memos/proto/gen/api/v1" + "github.com/usememos/memos/store" +) + +func (s *APIV1Service) ListInboxes(ctx context.Context, request *v1pb.ListInboxesRequest) (*v1pb.ListInboxesResponse, error) { + // Extract user ID from parent resource name + userID, err := ExtractUserIDFromName(request.Parent) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid parent name %q: %v", request.Parent, err) + } + + // Get current user for authorization + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user") + } + if currentUser == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } + + // Check if current user can access the requested user's inboxes + if currentUser.ID != userID { + // Only allow hosts and admins to access other users' inboxes + if currentUser.Role != store.RoleHost && currentUser.Role != store.RoleAdmin { + return nil, status.Errorf(codes.PermissionDenied, "cannot access inboxes for user %q", request.Parent) + } + } + + var limit, offset int + if request.PageToken != "" { + var pageToken v1pb.PageToken + if err := unmarshalPageToken(request.PageToken, &pageToken); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid page token: %v", err) + } + limit = int(pageToken.Limit) + offset = int(pageToken.Offset) + } else { + limit = int(request.PageSize) + } + if limit <= 0 { + limit = DefaultPageSize + } + if limit > MaxPageSize { + limit = MaxPageSize + } + limitPlusOne := limit + 1 + + findInbox := &store.FindInbox{ + ReceiverID: &userID, + Limit: &limitPlusOne, + Offset: &offset, + } + + inboxes, err := s.Store.ListInboxes(ctx, findInbox) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list inboxes: %v", err) + } + + inboxMessages := []*v1pb.Inbox{} + nextPageToken := "" + if len(inboxes) == limitPlusOne { + inboxes = inboxes[:limit] + nextPageToken, err = getPageToken(limit, offset+limit) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get next page token: %v", err) + } + } + for _, inbox := range inboxes { + inboxMessage := convertInboxFromStore(inbox) + if inboxMessage.Type == v1pb.Inbox_TYPE_UNSPECIFIED { + continue + } + inboxMessages = append(inboxMessages, inboxMessage) + } + + response := &v1pb.ListInboxesResponse{ + Inboxes: inboxMessages, + NextPageToken: nextPageToken, + TotalSize: int32(len(inboxMessages)), // For now, use actual returned count + } + return response, nil +} + +func (s *APIV1Service) UpdateInbox(ctx context.Context, request *v1pb.UpdateInboxRequest) (*v1pb.Inbox, error) { + if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 { + return nil, status.Errorf(codes.InvalidArgument, "update mask is required") + } + + inboxID, err := ExtractInboxIDFromName(request.Inbox.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid inbox name %q: %v", request.Inbox.Name, err) + } + + // Get current user for authorization + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user") + } + if currentUser == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } + + // Get the existing inbox to verify ownership + inboxes, err := s.Store.ListInboxes(ctx, &store.FindInbox{ + ID: &inboxID, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get inbox: %v", err) + } + if len(inboxes) == 0 { + return nil, status.Errorf(codes.NotFound, "inbox %q not found", request.Inbox.Name) + } + existingInbox := inboxes[0] + + // Check if current user can update this inbox (must be the receiver) + if currentUser.ID != existingInbox.ReceiverID { + return nil, status.Errorf(codes.PermissionDenied, "cannot update inbox for another user") + } + + update := &store.UpdateInbox{ + ID: inboxID, + } + for _, field := range request.UpdateMask.Paths { + if field == "status" { + if request.Inbox.Status == v1pb.Inbox_STATUS_UNSPECIFIED { + return nil, status.Errorf(codes.InvalidArgument, "status cannot be unspecified") + } + update.Status = convertInboxStatusToStore(request.Inbox.Status) + } else { + return nil, status.Errorf(codes.InvalidArgument, "unsupported field in update mask: %q", field) + } + } + + inbox, err := s.Store.UpdateInbox(ctx, update) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to update inbox: %v", err) + } + + return convertInboxFromStore(inbox), nil +} + +func (s *APIV1Service) DeleteInbox(ctx context.Context, request *v1pb.DeleteInboxRequest) (*emptypb.Empty, error) { + inboxID, err := ExtractInboxIDFromName(request.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid inbox name %q: %v", request.Name, err) + } + + // Get current user for authorization + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user") + } + if currentUser == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } + + // Get the existing inbox to verify ownership + inboxes, err := s.Store.ListInboxes(ctx, &store.FindInbox{ + ID: &inboxID, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get inbox: %v", err) + } + if len(inboxes) == 0 { + return nil, status.Errorf(codes.NotFound, "inbox %q not found", request.Name) + } + existingInbox := inboxes[0] + + // Check if current user can delete this inbox (must be the receiver) + if currentUser.ID != existingInbox.ReceiverID { + return nil, status.Errorf(codes.PermissionDenied, "cannot delete inbox for another user") + } + + if err := s.Store.DeleteInbox(ctx, &store.DeleteInbox{ + ID: inboxID, + }); err != nil { + return nil, status.Errorf(codes.Internal, "failed to delete inbox: %v", err) + } + return &emptypb.Empty{}, nil +} + +func convertInboxFromStore(inbox *store.Inbox) *v1pb.Inbox { + return &v1pb.Inbox{ + Name: fmt.Sprintf("%s%d", InboxNamePrefix, inbox.ID), + Sender: fmt.Sprintf("%s%d", UserNamePrefix, inbox.SenderID), + Receiver: fmt.Sprintf("%s%d", UserNamePrefix, inbox.ReceiverID), + Status: convertInboxStatusFromStore(inbox.Status), + CreateTime: timestamppb.New(time.Unix(inbox.CreatedTs, 0)), + Type: v1pb.Inbox_Type(inbox.Message.Type), + ActivityId: inbox.Message.ActivityId, + } +} + +func convertInboxStatusFromStore(status store.InboxStatus) v1pb.Inbox_Status { + switch status { + case store.UNREAD: + return v1pb.Inbox_UNREAD + case store.ARCHIVED: + return v1pb.Inbox_ARCHIVED + default: + return v1pb.Inbox_STATUS_UNSPECIFIED + } +} + +func convertInboxStatusToStore(status v1pb.Inbox_Status) store.InboxStatus { + switch status { + case v1pb.Inbox_UNREAD: + return store.UNREAD + case v1pb.Inbox_ARCHIVED: + return store.ARCHIVED + default: + return store.UNREAD + } +} diff --git a/server/router/api/v1/logger_interceptor.go b/server/router/api/v1/logger_interceptor.go new file mode 100644 index 0000000..b5dd2ea --- /dev/null +++ b/server/router/api/v1/logger_interceptor.go @@ -0,0 +1,48 @@ +package v1 + +import ( + "context" + "log/slog" + + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type LoggerInterceptor struct { +} + +func NewLoggerInterceptor() *LoggerInterceptor { + return &LoggerInterceptor{} +} + +func (in *LoggerInterceptor) LoggerInterceptor(ctx context.Context, request any, serverInfo *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + resp, err := handler(ctx, request) + in.loggerInterceptorDo(ctx, serverInfo.FullMethod, err) + return resp, err +} + +func (*LoggerInterceptor) loggerInterceptorDo(ctx context.Context, fullMethod string, err error) { + st := status.Convert(err) + var logLevel slog.Level + var logMsg string + switch st.Code() { + case codes.OK: + logLevel = slog.LevelInfo + logMsg = "OK" + case codes.Unauthenticated, codes.OutOfRange, codes.PermissionDenied, codes.NotFound: + logLevel = slog.LevelInfo + logMsg = "client error" + case codes.Internal, codes.Unknown, codes.DataLoss, codes.Unavailable, codes.DeadlineExceeded: + logLevel = slog.LevelError + logMsg = "server error" + default: + logLevel = slog.LevelError + logMsg = "unknown error" + } + logAttrs := []slog.Attr{slog.String("method", fullMethod)} + if err != nil { + logAttrs = append(logAttrs, slog.String("error", err.Error())) + } + slog.LogAttrs(ctx, logLevel, logMsg, logAttrs...) +} diff --git a/server/router/api/v1/markdown_service.go b/server/router/api/v1/markdown_service.go new file mode 100644 index 0000000..96eb793 --- /dev/null +++ b/server/router/api/v1/markdown_service.go @@ -0,0 +1,279 @@ +package v1 + +import ( + "context" + + "github.com/pkg/errors" + "github.com/usememos/gomark/ast" + "github.com/usememos/gomark/parser" + "github.com/usememos/gomark/parser/tokenizer" + "github.com/usememos/gomark/renderer" + "github.com/usememos/gomark/restore" + + "github.com/usememos/memos/plugin/httpgetter" + v1pb "github.com/usememos/memos/proto/gen/api/v1" +) + +func (*APIV1Service) ParseMarkdown(_ context.Context, request *v1pb.ParseMarkdownRequest) (*v1pb.ParseMarkdownResponse, error) { + rawNodes, err := parser.Parse(tokenizer.Tokenize(request.Markdown)) + if err != nil { + return nil, errors.Wrap(err, "failed to parse memo content") + } + + nodes := convertFromASTNodes(rawNodes) + return &v1pb.ParseMarkdownResponse{ + Nodes: nodes, + }, nil +} + +func (*APIV1Service) RestoreMarkdownNodes(_ context.Context, request *v1pb.RestoreMarkdownNodesRequest) (*v1pb.RestoreMarkdownNodesResponse, error) { + markdown := restore.Restore(convertToASTNodes(request.Nodes)) + return &v1pb.RestoreMarkdownNodesResponse{ + Markdown: markdown, + }, nil +} + +func (*APIV1Service) StringifyMarkdownNodes(_ context.Context, request *v1pb.StringifyMarkdownNodesRequest) (*v1pb.StringifyMarkdownNodesResponse, error) { + stringRenderer := renderer.NewStringRenderer() + plainText := stringRenderer.Render(convertToASTNodes(request.Nodes)) + return &v1pb.StringifyMarkdownNodesResponse{ + PlainText: plainText, + }, nil +} + +func (*APIV1Service) GetLinkMetadata(_ context.Context, request *v1pb.GetLinkMetadataRequest) (*v1pb.LinkMetadata, error) { + htmlMeta, err := httpgetter.GetHTMLMeta(request.Link) + if err != nil { + return nil, err + } + + return &v1pb.LinkMetadata{ + Title: htmlMeta.Title, + Description: htmlMeta.Description, + Image: htmlMeta.Image, + }, nil +} + +func convertFromASTNode(rawNode ast.Node) *v1pb.Node { + node := &v1pb.Node{ + Type: v1pb.NodeType(v1pb.NodeType_value[string(rawNode.Type())]), + } + + switch n := rawNode.(type) { + case *ast.LineBreak: + node.Node = &v1pb.Node_LineBreakNode{} + case *ast.Paragraph: + children := convertFromASTNodes(n.Children) + node.Node = &v1pb.Node_ParagraphNode{ParagraphNode: &v1pb.ParagraphNode{Children: children}} + case *ast.CodeBlock: + node.Node = &v1pb.Node_CodeBlockNode{CodeBlockNode: &v1pb.CodeBlockNode{Language: n.Language, Content: n.Content}} + case *ast.Heading: + children := convertFromASTNodes(n.Children) + node.Node = &v1pb.Node_HeadingNode{HeadingNode: &v1pb.HeadingNode{Level: int32(n.Level), Children: children}} + case *ast.HorizontalRule: + node.Node = &v1pb.Node_HorizontalRuleNode{HorizontalRuleNode: &v1pb.HorizontalRuleNode{Symbol: n.Symbol}} + case *ast.Blockquote: + children := convertFromASTNodes(n.Children) + node.Node = &v1pb.Node_BlockquoteNode{BlockquoteNode: &v1pb.BlockquoteNode{Children: children}} + case *ast.List: + children := convertFromASTNodes(n.Children) + node.Node = &v1pb.Node_ListNode{ListNode: &v1pb.ListNode{Kind: convertListKindFromASTNode(n.Kind), Indent: int32(n.Indent), Children: children}} + case *ast.OrderedListItem: + children := convertFromASTNodes(n.Children) + node.Node = &v1pb.Node_OrderedListItemNode{OrderedListItemNode: &v1pb.OrderedListItemNode{Number: n.Number, Indent: int32(n.Indent), Children: children}} + case *ast.UnorderedListItem: + children := convertFromASTNodes(n.Children) + node.Node = &v1pb.Node_UnorderedListItemNode{UnorderedListItemNode: &v1pb.UnorderedListItemNode{Symbol: n.Symbol, Indent: int32(n.Indent), Children: children}} + case *ast.TaskListItem: + children := convertFromASTNodes(n.Children) + node.Node = &v1pb.Node_TaskListItemNode{TaskListItemNode: &v1pb.TaskListItemNode{Symbol: n.Symbol, Indent: int32(n.Indent), Complete: n.Complete, Children: children}} + case *ast.MathBlock: + node.Node = &v1pb.Node_MathBlockNode{MathBlockNode: &v1pb.MathBlockNode{Content: n.Content}} + case *ast.Table: + node.Node = &v1pb.Node_TableNode{TableNode: convertTableFromASTNode(n)} + case *ast.EmbeddedContent: + node.Node = &v1pb.Node_EmbeddedContentNode{EmbeddedContentNode: &v1pb.EmbeddedContentNode{ResourceName: n.ResourceName, Params: n.Params}} + case *ast.Text: + node.Node = &v1pb.Node_TextNode{TextNode: &v1pb.TextNode{Content: n.Content}} + case *ast.Bold: + node.Node = &v1pb.Node_BoldNode{BoldNode: &v1pb.BoldNode{Symbol: n.Symbol, Children: convertFromASTNodes(n.Children)}} + case *ast.Italic: + node.Node = &v1pb.Node_ItalicNode{ItalicNode: &v1pb.ItalicNode{Symbol: n.Symbol, Children: convertFromASTNodes(n.Children)}} + case *ast.BoldItalic: + node.Node = &v1pb.Node_BoldItalicNode{BoldItalicNode: &v1pb.BoldItalicNode{Symbol: n.Symbol, Content: n.Content}} + case *ast.Code: + node.Node = &v1pb.Node_CodeNode{CodeNode: &v1pb.CodeNode{Content: n.Content}} + case *ast.Image: + node.Node = &v1pb.Node_ImageNode{ImageNode: &v1pb.ImageNode{AltText: n.AltText, Url: n.URL}} + case *ast.Link: + node.Node = &v1pb.Node_LinkNode{LinkNode: &v1pb.LinkNode{Content: convertFromASTNodes(n.Content), Url: n.URL}} + case *ast.AutoLink: + node.Node = &v1pb.Node_AutoLinkNode{AutoLinkNode: &v1pb.AutoLinkNode{Url: n.URL, IsRawText: n.IsRawText}} + case *ast.Tag: + node.Node = &v1pb.Node_TagNode{TagNode: &v1pb.TagNode{Content: n.Content}} + case *ast.Strikethrough: + node.Node = &v1pb.Node_StrikethroughNode{StrikethroughNode: &v1pb.StrikethroughNode{Content: n.Content}} + case *ast.EscapingCharacter: + node.Node = &v1pb.Node_EscapingCharacterNode{EscapingCharacterNode: &v1pb.EscapingCharacterNode{Symbol: n.Symbol}} + case *ast.Math: + node.Node = &v1pb.Node_MathNode{MathNode: &v1pb.MathNode{Content: n.Content}} + case *ast.Highlight: + node.Node = &v1pb.Node_HighlightNode{HighlightNode: &v1pb.HighlightNode{Content: n.Content}} + case *ast.Subscript: + node.Node = &v1pb.Node_SubscriptNode{SubscriptNode: &v1pb.SubscriptNode{Content: n.Content}} + case *ast.Superscript: + node.Node = &v1pb.Node_SuperscriptNode{SuperscriptNode: &v1pb.SuperscriptNode{Content: n.Content}} + case *ast.ReferencedContent: + node.Node = &v1pb.Node_ReferencedContentNode{ReferencedContentNode: &v1pb.ReferencedContentNode{ResourceName: n.ResourceName, Params: n.Params}} + case *ast.Spoiler: + node.Node = &v1pb.Node_SpoilerNode{SpoilerNode: &v1pb.SpoilerNode{Content: n.Content}} + case *ast.HTMLElement: + node.Node = &v1pb.Node_HtmlElementNode{HtmlElementNode: &v1pb.HTMLElementNode{TagName: n.TagName, Attributes: n.Attributes}} + default: + node.Node = &v1pb.Node_TextNode{TextNode: &v1pb.TextNode{}} + } + return node +} + +func convertFromASTNodes(rawNodes []ast.Node) []*v1pb.Node { + nodes := []*v1pb.Node{} + for _, rawNode := range rawNodes { + node := convertFromASTNode(rawNode) + nodes = append(nodes, node) + } + return nodes +} + +func convertTableFromASTNode(node *ast.Table) *v1pb.TableNode { + table := &v1pb.TableNode{ + Header: convertFromASTNodes(node.Header), + Delimiter: node.Delimiter, + } + for _, row := range node.Rows { + table.Rows = append(table.Rows, &v1pb.TableNode_Row{Cells: convertFromASTNodes(row)}) + } + return table +} + +func convertListKindFromASTNode(node ast.ListKind) v1pb.ListNode_Kind { + switch node { + case ast.OrderedList: + return v1pb.ListNode_ORDERED + case ast.UnorderedList: + return v1pb.ListNode_UNORDERED + case ast.DescrpitionList: + return v1pb.ListNode_DESCRIPTION + default: + return v1pb.ListNode_KIND_UNSPECIFIED + } +} + +func convertToASTNode(node *v1pb.Node) ast.Node { + switch n := node.Node.(type) { + case *v1pb.Node_LineBreakNode: + return &ast.LineBreak{} + case *v1pb.Node_ParagraphNode: + children := convertToASTNodes(n.ParagraphNode.Children) + return &ast.Paragraph{Children: children} + case *v1pb.Node_CodeBlockNode: + return &ast.CodeBlock{Language: n.CodeBlockNode.Language, Content: n.CodeBlockNode.Content} + case *v1pb.Node_HeadingNode: + children := convertToASTNodes(n.HeadingNode.Children) + return &ast.Heading{Level: int(n.HeadingNode.Level), Children: children} + case *v1pb.Node_HorizontalRuleNode: + return &ast.HorizontalRule{Symbol: n.HorizontalRuleNode.Symbol} + case *v1pb.Node_BlockquoteNode: + children := convertToASTNodes(n.BlockquoteNode.Children) + return &ast.Blockquote{Children: children} + case *v1pb.Node_ListNode: + children := convertToASTNodes(n.ListNode.Children) + return &ast.List{Kind: convertListKindToASTNode(n.ListNode.Kind), Indent: int(n.ListNode.Indent), Children: children} + case *v1pb.Node_OrderedListItemNode: + children := convertToASTNodes(n.OrderedListItemNode.Children) + return &ast.OrderedListItem{Number: n.OrderedListItemNode.Number, Indent: int(n.OrderedListItemNode.Indent), Children: children} + case *v1pb.Node_UnorderedListItemNode: + children := convertToASTNodes(n.UnorderedListItemNode.Children) + return &ast.UnorderedListItem{Symbol: n.UnorderedListItemNode.Symbol, Indent: int(n.UnorderedListItemNode.Indent), Children: children} + case *v1pb.Node_TaskListItemNode: + children := convertToASTNodes(n.TaskListItemNode.Children) + return &ast.TaskListItem{Symbol: n.TaskListItemNode.Symbol, Indent: int(n.TaskListItemNode.Indent), Complete: n.TaskListItemNode.Complete, Children: children} + case *v1pb.Node_MathBlockNode: + return &ast.MathBlock{Content: n.MathBlockNode.Content} + case *v1pb.Node_TableNode: + return convertTableToASTNode(n.TableNode) + case *v1pb.Node_EmbeddedContentNode: + return &ast.EmbeddedContent{ResourceName: n.EmbeddedContentNode.ResourceName, Params: n.EmbeddedContentNode.Params} + case *v1pb.Node_TextNode: + return &ast.Text{Content: n.TextNode.Content} + case *v1pb.Node_BoldNode: + return &ast.Bold{Symbol: n.BoldNode.Symbol, Children: convertToASTNodes(n.BoldNode.Children)} + case *v1pb.Node_ItalicNode: + return &ast.Italic{Symbol: n.ItalicNode.Symbol, Children: convertToASTNodes(n.ItalicNode.Children)} + case *v1pb.Node_BoldItalicNode: + return &ast.BoldItalic{Symbol: n.BoldItalicNode.Symbol, Content: n.BoldItalicNode.Content} + case *v1pb.Node_CodeNode: + return &ast.Code{Content: n.CodeNode.Content} + case *v1pb.Node_ImageNode: + return &ast.Image{AltText: n.ImageNode.AltText, URL: n.ImageNode.Url} + case *v1pb.Node_LinkNode: + return &ast.Link{Content: convertToASTNodes(n.LinkNode.Content), URL: n.LinkNode.Url} + case *v1pb.Node_AutoLinkNode: + return &ast.AutoLink{URL: n.AutoLinkNode.Url, IsRawText: n.AutoLinkNode.IsRawText} + case *v1pb.Node_TagNode: + return &ast.Tag{Content: n.TagNode.Content} + case *v1pb.Node_StrikethroughNode: + return &ast.Strikethrough{Content: n.StrikethroughNode.Content} + case *v1pb.Node_EscapingCharacterNode: + return &ast.EscapingCharacter{Symbol: n.EscapingCharacterNode.Symbol} + case *v1pb.Node_MathNode: + return &ast.Math{Content: n.MathNode.Content} + case *v1pb.Node_HighlightNode: + return &ast.Highlight{Content: n.HighlightNode.Content} + case *v1pb.Node_SubscriptNode: + return &ast.Subscript{Content: n.SubscriptNode.Content} + case *v1pb.Node_SuperscriptNode: + return &ast.Superscript{Content: n.SuperscriptNode.Content} + case *v1pb.Node_ReferencedContentNode: + return &ast.ReferencedContent{ResourceName: n.ReferencedContentNode.ResourceName, Params: n.ReferencedContentNode.Params} + case *v1pb.Node_SpoilerNode: + return &ast.Spoiler{Content: n.SpoilerNode.Content} + case *v1pb.Node_HtmlElementNode: + return &ast.HTMLElement{TagName: n.HtmlElementNode.TagName, Attributes: n.HtmlElementNode.Attributes} + default: + return &ast.Text{} + } +} + +func convertToASTNodes(nodes []*v1pb.Node) []ast.Node { + rawNodes := []ast.Node{} + for _, node := range nodes { + rawNode := convertToASTNode(node) + rawNodes = append(rawNodes, rawNode) + } + return rawNodes +} + +func convertTableToASTNode(node *v1pb.TableNode) *ast.Table { + table := &ast.Table{ + Header: convertToASTNodes(node.Header), + Delimiter: node.Delimiter, + } + for _, row := range node.Rows { + table.Rows = append(table.Rows, convertToASTNodes(row.Cells)) + } + return table +} + +func convertListKindToASTNode(kind v1pb.ListNode_Kind) ast.ListKind { + switch kind { + case v1pb.ListNode_ORDERED: + return ast.OrderedList + case v1pb.ListNode_UNORDERED: + return ast.UnorderedList + case v1pb.ListNode_DESCRIPTION: + return ast.DescrpitionList + default: + // Default to description list. + return ast.DescrpitionList + } +} diff --git a/server/router/api/v1/memo_attachment_service.go b/server/router/api/v1/memo_attachment_service.go new file mode 100644 index 0000000..95e0d48 --- /dev/null +++ b/server/router/api/v1/memo_attachment_service.go @@ -0,0 +1,102 @@ +package v1 + +import ( + "context" + "slices" + "time" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + + v1pb "github.com/usememos/memos/proto/gen/api/v1" + "github.com/usememos/memos/store" +) + +func (s *APIV1Service) SetMemoAttachments(ctx context.Context, request *v1pb.SetMemoAttachmentsRequest) (*emptypb.Empty, error) { + memoUID, err := ExtractMemoUIDFromName(request.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err) + } + memo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get memo") + } + attachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{ + MemoID: &memo.ID, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list attachments") + } + + // Delete attachments that are not in the request. + for _, attachment := range attachments { + found := false + for _, requestAttachment := range request.Attachments { + requestAttachmentUID, err := ExtractAttachmentUIDFromName(requestAttachment.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid attachment name: %v", err) + } + if attachment.UID == requestAttachmentUID { + found = true + break + } + } + if !found { + if err = s.Store.DeleteAttachment(ctx, &store.DeleteAttachment{ + ID: int32(attachment.ID), + MemoID: &memo.ID, + }); err != nil { + return nil, status.Errorf(codes.Internal, "failed to delete attachment") + } + } + } + + slices.Reverse(request.Attachments) + // Update attachments' memo_id in the request. + for index, attachment := range request.Attachments { + attachmentUID, err := ExtractAttachmentUIDFromName(attachment.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid attachment name: %v", err) + } + tempAttachment, err := s.Store.GetAttachment(ctx, &store.FindAttachment{UID: &attachmentUID}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get attachment: %v", err) + } + updatedTs := time.Now().Unix() + int64(index) + if err := s.Store.UpdateAttachment(ctx, &store.UpdateAttachment{ + ID: tempAttachment.ID, + MemoID: &memo.ID, + UpdatedTs: &updatedTs, + }); err != nil { + return nil, status.Errorf(codes.Internal, "failed to update attachment: %v", err) + } + } + + return &emptypb.Empty{}, nil +} + +func (s *APIV1Service) ListMemoAttachments(ctx context.Context, request *v1pb.ListMemoAttachmentsRequest) (*v1pb.ListMemoAttachmentsResponse, error) { + memoUID, err := ExtractMemoUIDFromName(request.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err) + } + memo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get memo: %v", err) + } + attachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{ + MemoID: &memo.ID, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list attachments: %v", err) + } + + response := &v1pb.ListMemoAttachmentsResponse{ + Attachments: []*v1pb.Attachment{}, + } + for _, attachment := range attachments { + response.Attachments = append(response.Attachments, s.convertAttachmentFromStore(ctx, attachment)) + } + return response, nil +} diff --git a/server/router/api/v1/memo_export_import.go b/server/router/api/v1/memo_export_import.go new file mode 100644 index 0000000..25f033c --- /dev/null +++ b/server/router/api/v1/memo_export_import.go @@ -0,0 +1,427 @@ +package v1 + +import ( + "context" + "encoding/json" + "fmt" + "log/slog" + "time" + + "github.com/pkg/errors" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + v1pb "github.com/usememos/memos/proto/gen/api/v1" + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/server/runner/memopayload" + "github.com/usememos/memos/store" +) + +// ExportFormat represents the format for export/import operations +type ExportFormat string + +const ( + FormatJSON ExportFormat = "json" +) + +// ExportData represents the structure of exported data +type ExportData struct { + Version string `json:"version"` + ExportedAt time.Time `json:"exported_at"` + Memos []ExportMemo `json:"memos"` +} + +// ExportMemo represents a memo in the export format +type ExportMemo struct { + UID string `json:"uid"` + Content string `json:"content"` + Visibility string `json:"visibility"` + Pinned bool `json:"pinned"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DisplayTime *time.Time `json:"display_time,omitempty"` + Tags []string `json:"tags,omitempty"` + Location *ExportLocation `json:"location,omitempty"` + Attachments []ExportAttachment `json:"attachments,omitempty"` + Relations []ExportMemoRelation `json:"relations,omitempty"` +} + +// ExportLocation represents location data in export format +type ExportLocation struct { + Placeholder string `json:"placeholder,omitempty"` + Latitude float64 `json:"latitude,omitempty"` + Longitude float64 `json:"longitude,omitempty"` +} + +// ExportAttachment represents attachment data in export format +type ExportAttachment struct { + UID string `json:"uid"` + Filename string `json:"filename"` + Type string `json:"type"` + Size int64 `json:"size"` +} + +// ExportMemoRelation represents memo relations in export format +type ExportMemoRelation struct { + RelatedMemoUID string `json:"related_memo_uid"` + Type string `json:"type"` +} + +// ExportMemos exports memos for the current user in JSON format +func (s *APIV1Service) ExportMemos(ctx context.Context, request *v1pb.ExportMemosRequest) (*v1pb.ExportMemosResponse, error) { + user, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user") + } + + // Validate format (default to JSON) + format := request.Format + if format == "" { + format = string(FormatJSON) + } + if format != string(FormatJSON) { + return nil, status.Errorf(codes.InvalidArgument, "unsupported export format: %s", format) + } + + // Get all memos for the user + memoFind := &store.FindMemo{ + CreatorID: &user.ID, + ExcludeComments: true, + } + + // Apply filters if specified + if request.Filter != "" { + // Use existing filter validation from shortcut service + memoFind.Filter = &request.Filter + } + + // Include archived memos if requested + if request.ExcludeArchived { + normalStatus := store.Normal + memoFind.RowStatus = &normalStatus + } + + memos, err := s.Store.ListMemos(ctx, memoFind) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list memos: %v", err) + } + + // Convert memos to export format + exportMemos := make([]ExportMemo, 0, len(memos)) + for _, memo := range memos { + exportMemo, err := s.convertMemoToExport(ctx, memo, request.IncludeAttachments, request.IncludeRelations) + if err != nil { + slog.Warn("Failed to convert memo to export format", slog.Any("memo_id", memo.ID), slog.Any("error", err)) + continue + } + exportMemos = append(exportMemos, *exportMemo) + } + + // Create export data structure + exportData := &ExportData{ + Version: "1.0", + ExportedAt: time.Now(), + Memos: exportMemos, + } + + // Serialize to JSON + jsonData, err := json.MarshalIndent(exportData, "", " ") + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to marshal export data: %v", err) + } + + return &v1pb.ExportMemosResponse{ + Data: jsonData, + Format: format, + Filename: fmt.Sprintf("memos_export_%s.json", time.Now().Format("20060102_150405")), + MemoCount: int32(len(exportMemos)), + SizeBytes: int64(len(jsonData)), + }, nil +} + +// ImportMemos imports memos from JSON data +func (s *APIV1Service) ImportMemos(ctx context.Context, request *v1pb.ImportMemosRequest) (*v1pb.ImportMemosResponse, error) { + startTime := time.Now() + + user, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user") + } + + // Validate format (default to JSON) + format := request.Format + if format == "" { + format = string(FormatJSON) + } + if format != string(FormatJSON) { + return nil, status.Errorf(codes.InvalidArgument, "unsupported import format: %s", format) + } + + // Parse the JSON data + var importData ExportData + if err := json.Unmarshal(request.Data, &importData); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "failed to parse import data: %v", err) + } + + // Validate import data version + if importData.Version != "1.0" { + return nil, status.Errorf(codes.InvalidArgument, "unsupported import data version: %s", importData.Version) + } + + var importedCount int32 + var skippedCount int32 + var createdCount int32 + var updatedCount int32 + var validationErrors int32 + var attachmentsImported int32 + var relationsImported int32 + var errors []string + var warnings []string + + // Import each memo + for _, exportMemo := range importData.Memos { + result, err := s.importSingleMemo(ctx, user.ID, &exportMemo, request) + if err != nil { + errorMsg := fmt.Sprintf("Failed to import memo %s: %v", exportMemo.UID, err) + errors = append(errors, errorMsg) + skippedCount++ + if request.ValidateOnly { + validationErrors++ + } + slog.Warn("Failed to import memo", slog.String("uid", exportMemo.UID), slog.Any("error", err)) + continue + } + + importedCount++ + if result.Created { + createdCount++ + } else { + updatedCount++ + } + attachmentsImported += result.AttachmentsImported + relationsImported += result.RelationsImported + + if len(result.Warnings) > 0 { + warnings = append(warnings, result.Warnings...) + } + } + + duration := time.Since(startTime) + + summary := &v1pb.ImportSummary{ + TotalMemos: int32(len(importData.Memos)), + CreatedCount: createdCount, + UpdatedCount: updatedCount, + AttachmentsImported: attachmentsImported, + RelationsImported: relationsImported, + DurationMs: duration.Milliseconds(), + } + + return &v1pb.ImportMemosResponse{ + ImportedCount: importedCount, + SkippedCount: skippedCount, + ValidationErrors: validationErrors, + Errors: errors, + Warnings: warnings, + Summary: summary, + }, nil +} + +// convertMemoToExport converts a store memo to export format +func (s *APIV1Service) convertMemoToExport(ctx context.Context, memo *store.Memo, includeAttachments, includeRelations bool) (*ExportMemo, error) { + exportMemo := &ExportMemo{ + UID: memo.UID, + Content: memo.Content, + Visibility: memo.Visibility.String(), + Pinned: memo.Pinned, + CreatedAt: time.Unix(memo.CreatedTs, 0), + UpdatedAt: time.Unix(memo.UpdatedTs, 0), + } + + // Extract tags from payload + if memo.Payload != nil && len(memo.Payload.Tags) > 0 { + exportMemo.Tags = memo.Payload.Tags + } + + // Add location if present + if memo.Payload != nil && memo.Payload.Location != nil { + exportMemo.Location = &ExportLocation{ + Placeholder: memo.Payload.Location.Placeholder, + Latitude: memo.Payload.Location.Latitude, + Longitude: memo.Payload.Location.Longitude, + } + } + + // Add attachments if requested + if includeAttachments { + attachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{MemoID: &memo.ID}) + if err != nil { + return nil, errors.Wrap(err, "failed to list attachments") + } + + for _, attachment := range attachments { + exportMemo.Attachments = append(exportMemo.Attachments, ExportAttachment{ + UID: attachment.UID, + Filename: attachment.Filename, + Type: attachment.Type, + Size: attachment.Size, + }) + } + } + + // Add relations if requested + if includeRelations { + relations, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{MemoID: &memo.ID}) + if err != nil { + return nil, errors.Wrap(err, "failed to list memo relations") + } + + for _, relation := range relations { + relatedMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{ID: &relation.RelatedMemoID}) + if err != nil || relatedMemo == nil { + continue // Skip if related memo not found + } + + exportMemo.Relations = append(exportMemo.Relations, ExportMemoRelation{ + RelatedMemoUID: relatedMemo.UID, + Type: string(relation.Type), + }) + } + } + + return exportMemo, nil +} + +// ImportResult represents the result of importing a single memo +type ImportResult struct { + Created bool + AttachmentsImported int32 + RelationsImported int32 + Warnings []string +} + +// importSingleMemo imports a single memo +func (s *APIV1Service) importSingleMemo(ctx context.Context, userID int32, exportMemo *ExportMemo, request *v1pb.ImportMemosRequest) (*ImportResult, error) { + result := &ImportResult{ + Warnings: []string{}, + } + + // Check if memo with this UID already exists + existingMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &exportMemo.UID}) + if err != nil { + return nil, errors.Wrap(err, "failed to check for existing memo") + } + + if existingMemo != nil && !request.OverwriteExisting { + return nil, fmt.Errorf("memo with UID %s already exists", exportMemo.UID) + } + + // Validate memo content length + contentLengthLimit, err := s.getContentLengthLimit(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to get content length limit") + } + if len(exportMemo.Content) > contentLengthLimit { + return nil, fmt.Errorf("content too long (max %d characters)", contentLengthLimit) + } + + // Parse visibility + visibility := store.Private + switch exportMemo.Visibility { + case "PUBLIC": + visibility = store.Public + case "PROTECTED": + visibility = store.Protected + case "PRIVATE": + visibility = store.Private + default: + result.Warnings = append(result.Warnings, fmt.Sprintf("Unknown visibility %s for memo %s, defaulting to PRIVATE", exportMemo.Visibility, exportMemo.UID)) + } + + // Create memo payload + payload := &storepb.MemoPayload{ + Tags: exportMemo.Tags, + } + + if exportMemo.Location != nil { + payload.Location = &storepb.MemoPayload_Location{ + Placeholder: exportMemo.Location.Placeholder, + Latitude: exportMemo.Location.Latitude, + Longitude: exportMemo.Location.Longitude, + } + } + + // Set timestamps + createdTs := exportMemo.CreatedAt.Unix() + updatedTs := exportMemo.UpdatedAt.Unix() + if !request.PreserveTimestamps { + now := time.Now().Unix() + createdTs = now + updatedTs = now + } + + if request.ValidateOnly { + // Just validate, don't actually create/update + return result, nil + } + + if existingMemo != nil { + // Update existing memo + update := &store.UpdateMemo{ + ID: existingMemo.ID, + Content: &exportMemo.Content, + Visibility: &visibility, + Pinned: &exportMemo.Pinned, + Payload: payload, + } + + if request.PreserveTimestamps { + update.CreatedTs = &createdTs + update.UpdatedTs = &updatedTs + } + + if err := s.Store.UpdateMemo(ctx, update); err != nil { + return nil, errors.Wrap(err, "failed to update existing memo") + } + result.Created = false + } else { + // Create new memo + create := &store.Memo{ + UID: exportMemo.UID, + CreatorID: userID, + CreatedTs: createdTs, + UpdatedTs: updatedTs, + Content: exportMemo.Content, + Visibility: visibility, + Pinned: exportMemo.Pinned, + Payload: payload, + } + + // Rebuild memo payload to extract tags and other metadata + if err := memopayload.RebuildMemoPayload(create); err != nil { + return nil, errors.Wrap(err, "failed to rebuild memo payload") + } + + _, err := s.Store.CreateMemo(ctx, create) + if err != nil { + return nil, errors.Wrap(err, "failed to create memo") + } + result.Created = true + } + + // Import attachments if not skipped + if !request.SkipAttachments && len(exportMemo.Attachments) > 0 { + result.Warnings = append(result.Warnings, fmt.Sprintf("Attachments for memo %s were skipped (attachment import not yet implemented)", exportMemo.UID)) + // TODO: Implement attachment import + // This would require handling file uploads and storage + } + + // Import relations if not skipped + if !request.SkipRelations && len(exportMemo.Relations) > 0 { + result.Warnings = append(result.Warnings, fmt.Sprintf("Relations for memo %s were skipped (relation import not yet implemented)", exportMemo.UID)) + // TODO: Implement relation import + // This would require resolving related memo UIDs and creating relations + } + + return result, nil +} diff --git a/server/router/api/v1/memo_relation_service.go b/server/router/api/v1/memo_relation_service.go new file mode 100644 index 0000000..3c59e3c --- /dev/null +++ b/server/router/api/v1/memo_relation_service.go @@ -0,0 +1,170 @@ +package v1 + +import ( + "context" + "fmt" + + "github.com/pkg/errors" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + + v1pb "github.com/usememos/memos/proto/gen/api/v1" + "github.com/usememos/memos/store" +) + +func (s *APIV1Service) SetMemoRelations(ctx context.Context, request *v1pb.SetMemoRelationsRequest) (*emptypb.Empty, error) { + memoUID, err := ExtractMemoUIDFromName(request.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err) + } + memo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get memo") + } + referenceType := store.MemoRelationReference + // Delete all reference relations first. + if err := s.Store.DeleteMemoRelation(ctx, &store.DeleteMemoRelation{ + MemoID: &memo.ID, + Type: &referenceType, + }); err != nil { + return nil, status.Errorf(codes.Internal, "failed to delete memo relation") + } + + for _, relation := range request.Relations { + // Ignore reflexive relations. + if request.Name == relation.RelatedMemo.Name { + continue + } + // Ignore comment relations as there's no need to update a comment's relation. + // Inserting/Deleting a comment is handled elsewhere. + if relation.Type == v1pb.MemoRelation_COMMENT { + continue + } + relatedMemoUID, err := ExtractMemoUIDFromName(relation.RelatedMemo.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid related memo name: %v", err) + } + relatedMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &relatedMemoUID}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get related memo") + } + if _, err := s.Store.UpsertMemoRelation(ctx, &store.MemoRelation{ + MemoID: memo.ID, + RelatedMemoID: relatedMemo.ID, + Type: convertMemoRelationTypeToStore(relation.Type), + }); err != nil { + return nil, status.Errorf(codes.Internal, "failed to upsert memo relation") + } + } + + return &emptypb.Empty{}, nil +} + +func (s *APIV1Service) ListMemoRelations(ctx context.Context, request *v1pb.ListMemoRelationsRequest) (*v1pb.ListMemoRelationsResponse, error) { + memoUID, err := ExtractMemoUIDFromName(request.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err) + } + memo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get memo") + } + + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user") + } + var memoFilter string + if currentUser == nil { + memoFilter = `visibility == "PUBLIC"` + } else { + memoFilter = fmt.Sprintf(`creator_id == %d || visibility in ["PUBLIC", "PROTECTED"]`, currentUser.ID) + } + relationList := []*v1pb.MemoRelation{} + tempList, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{ + MemoID: &memo.ID, + MemoFilter: &memoFilter, + }) + if err != nil { + return nil, err + } + for _, raw := range tempList { + relation, err := s.convertMemoRelationFromStore(ctx, raw) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to convert memo relation") + } + relationList = append(relationList, relation) + } + tempList, err = s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{ + RelatedMemoID: &memo.ID, + MemoFilter: &memoFilter, + }) + if err != nil { + return nil, err + } + for _, raw := range tempList { + relation, err := s.convertMemoRelationFromStore(ctx, raw) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to convert memo relation") + } + relationList = append(relationList, relation) + } + + response := &v1pb.ListMemoRelationsResponse{ + Relations: relationList, + } + return response, nil +} + +func (s *APIV1Service) convertMemoRelationFromStore(ctx context.Context, memoRelation *store.MemoRelation) (*v1pb.MemoRelation, error) { + memo, err := s.Store.GetMemo(ctx, &store.FindMemo{ID: &memoRelation.MemoID}) + if err != nil { + return nil, err + } + memoSnippet, err := getMemoContentSnippet(memo.Content) + if err != nil { + return nil, errors.Wrap(err, "failed to get memo content snippet") + } + relatedMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{ID: &memoRelation.RelatedMemoID}) + if err != nil { + return nil, err + } + relatedMemoSnippet, err := getMemoContentSnippet(relatedMemo.Content) + if err != nil { + return nil, errors.Wrap(err, "failed to get related memo content snippet") + } + return &v1pb.MemoRelation{ + Memo: &v1pb.MemoRelation_Memo{ + Name: fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID), + Snippet: memoSnippet, + }, + RelatedMemo: &v1pb.MemoRelation_Memo{ + Name: fmt.Sprintf("%s%s", MemoNamePrefix, relatedMemo.UID), + Snippet: relatedMemoSnippet, + }, + Type: convertMemoRelationTypeFromStore(memoRelation.Type), + }, nil +} + +func convertMemoRelationTypeFromStore(relationType store.MemoRelationType) v1pb.MemoRelation_Type { + switch relationType { + case store.MemoRelationReference: + return v1pb.MemoRelation_REFERENCE + case store.MemoRelationComment: + return v1pb.MemoRelation_COMMENT + default: + return v1pb.MemoRelation_TYPE_UNSPECIFIED + } +} + +func convertMemoRelationTypeToStore(relationType v1pb.MemoRelation_Type) store.MemoRelationType { + switch relationType { + case v1pb.MemoRelation_REFERENCE: + return store.MemoRelationReference + case v1pb.MemoRelation_COMMENT: + return store.MemoRelationComment + default: + return store.MemoRelationReference + } +} diff --git a/server/router/api/v1/memo_service.go b/server/router/api/v1/memo_service.go new file mode 100644 index 0000000..4d3d9fe --- /dev/null +++ b/server/router/api/v1/memo_service.go @@ -0,0 +1,785 @@ +package v1 + +import ( + "context" + "fmt" + "log/slog" + "strings" + "time" + "unicode/utf8" + + "github.com/lithammer/shortuuid/v4" + "github.com/pkg/errors" + "github.com/usememos/gomark/ast" + "github.com/usememos/gomark/parser" + "github.com/usememos/gomark/parser/tokenizer" + "github.com/usememos/gomark/renderer" + "github.com/usememos/gomark/restore" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + + "github.com/usememos/memos/plugin/webhook" + v1pb "github.com/usememos/memos/proto/gen/api/v1" + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/server/runner/memopayload" + "github.com/usememos/memos/store" +) + +func (s *APIV1Service) CreateMemo(ctx context.Context, request *v1pb.CreateMemoRequest) (*v1pb.Memo, error) { + user, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user") + } + + create := &store.Memo{ + UID: shortuuid.New(), + CreatorID: user.ID, + Content: request.Memo.Content, + Visibility: convertVisibilityToStore(request.Memo.Visibility), + } + workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get workspace memo related setting") + } + if workspaceMemoRelatedSetting.DisallowPublicVisibility && create.Visibility == store.Public { + return nil, status.Errorf(codes.PermissionDenied, "disable public memos system setting is enabled") + } + contentLengthLimit, err := s.getContentLengthLimit(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get content length limit") + } + if len(create.Content) > contentLengthLimit { + return nil, status.Errorf(codes.InvalidArgument, "content too long (max %d characters)", contentLengthLimit) + } + if err := memopayload.RebuildMemoPayload(create); err != nil { + return nil, status.Errorf(codes.Internal, "failed to rebuild memo payload: %v", err) + } + if request.Memo.Location != nil { + create.Payload.Location = convertLocationToStore(request.Memo.Location) + } + + memo, err := s.Store.CreateMemo(ctx, create) + if err != nil { + return nil, err + } + if len(request.Memo.Attachments) > 0 { + _, err := s.SetMemoAttachments(ctx, &v1pb.SetMemoAttachmentsRequest{ + Name: fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID), + Attachments: request.Memo.Attachments, + }) + if err != nil { + return nil, errors.Wrap(err, "failed to set memo attachments") + } + } + if len(request.Memo.Relations) > 0 { + _, err := s.SetMemoRelations(ctx, &v1pb.SetMemoRelationsRequest{ + Name: fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID), + Relations: request.Memo.Relations, + }) + if err != nil { + return nil, errors.Wrap(err, "failed to set memo relations") + } + } + + memoMessage, err := s.convertMemoFromStore(ctx, memo) + if err != nil { + return nil, errors.Wrap(err, "failed to convert memo") + } + // Try to dispatch webhook when memo is created. + if err := s.DispatchMemoCreatedWebhook(ctx, memoMessage); err != nil { + slog.Warn("Failed to dispatch memo created webhook", slog.Any("err", err)) + } + + return memoMessage, nil +} + +func (s *APIV1Service) ListMemos(ctx context.Context, request *v1pb.ListMemosRequest) (*v1pb.ListMemosResponse, error) { + memoFind := &store.FindMemo{ + // Exclude comments by default. + ExcludeComments: true, + } + // Handle deprecated old_filter for backward compatibility + if request.OldFilter != "" && request.Filter == "" { + //nolint:staticcheck // SA1019: Using deprecated field for backward compatibility + if err := s.buildMemoFindWithFilter(ctx, memoFind, request.OldFilter); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "failed to build find memos with filter: %v", err) + } + } + if request.Parent != "" && request.Parent != "users/-" { + userID, err := ExtractUserIDFromName(request.Parent) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid parent: %v", err) + } + memoFind.CreatorID = &userID + memoFind.OrderByPinned = true + } + if request.State == v1pb.State_ARCHIVED { + state := store.Archived + memoFind.RowStatus = &state + } else { + state := store.Normal + memoFind.RowStatus = &state + } + + // Parse order_by field (replaces the old sort and direction fields) + if request.OrderBy != "" { + if err := s.parseMemoOrderBy(request.OrderBy, memoFind); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid order_by: %v", err) + } + } else { + // Default ordering by display_time desc + memoFind.OrderByTimeAsc = false + } + + if request.Filter != "" { + if err := s.validateFilter(ctx, request.Filter); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid filter: %v", err) + } + memoFind.Filter = &request.Filter + } + + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user") + } + if currentUser == nil { + memoFind.VisibilityList = []store.Visibility{store.Public} + } else { + if memoFind.CreatorID == nil { + internalFilter := fmt.Sprintf(`creator_id == %d || visibility in ["PUBLIC", "PROTECTED"]`, currentUser.ID) + if memoFind.Filter != nil { + filter := fmt.Sprintf("(%s) && (%s)", *memoFind.Filter, internalFilter) + memoFind.Filter = &filter + } else { + memoFind.Filter = &internalFilter + } + } else if *memoFind.CreatorID != currentUser.ID { + memoFind.VisibilityList = []store.Visibility{store.Public, store.Protected} + } + } + + workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get workspace memo related setting") + } + if workspaceMemoRelatedSetting.DisplayWithUpdateTime { + memoFind.OrderByUpdatedTs = true + } + + var limit, offset int + if request.PageToken != "" { + var pageToken v1pb.PageToken + if err := unmarshalPageToken(request.PageToken, &pageToken); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid page token: %v", err) + } + limit = int(pageToken.Limit) + offset = int(pageToken.Offset) + } else { + limit = int(request.PageSize) + } + if limit <= 0 { + limit = DefaultPageSize + } + limitPlusOne := limit + 1 + memoFind.Limit = &limitPlusOne + memoFind.Offset = &offset + memos, err := s.Store.ListMemos(ctx, memoFind) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list memos: %v", err) + } + + memoMessages := []*v1pb.Memo{} + nextPageToken := "" + if len(memos) == limitPlusOne { + memos = memos[:limit] + nextPageToken, err = getPageToken(limit, offset+limit) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get next page token, error: %v", err) + } + } + for _, memo := range memos { + memoMessage, err := s.convertMemoFromStore(ctx, memo) + if err != nil { + return nil, errors.Wrap(err, "failed to convert memo") + } + memoMessages = append(memoMessages, memoMessage) + } + + response := &v1pb.ListMemosResponse{ + Memos: memoMessages, + NextPageToken: nextPageToken, + } + return response, nil +} + +func (s *APIV1Service) GetMemo(ctx context.Context, request *v1pb.GetMemoRequest) (*v1pb.Memo, error) { + memoUID, err := ExtractMemoUIDFromName(request.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err) + } + memo, err := s.Store.GetMemo(ctx, &store.FindMemo{ + UID: &memoUID, + }) + if err != nil { + return nil, err + } + if memo == nil { + return nil, status.Errorf(codes.NotFound, "memo not found") + } + if memo.Visibility != store.Public { + user, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user") + } + if user == nil { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + if memo.Visibility == store.Private && memo.CreatorID != user.ID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + } + + memoMessage, err := s.convertMemoFromStore(ctx, memo) + if err != nil { + return nil, errors.Wrap(err, "failed to convert memo") + } + return memoMessage, nil +} + +func (s *APIV1Service) UpdateMemo(ctx context.Context, request *v1pb.UpdateMemoRequest) (*v1pb.Memo, error) { + memoUID, err := ExtractMemoUIDFromName(request.Memo.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err) + } + if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 { + return nil, status.Errorf(codes.InvalidArgument, "update mask is required") + } + + memo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID}) + if err != nil { + return nil, err + } + if memo == nil { + return nil, status.Errorf(codes.NotFound, "memo not found") + } + + user, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user") + } + // Only the creator or admin can update the memo. + if memo.CreatorID != user.ID && !isSuperUser(user) { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + update := &store.UpdateMemo{ + ID: memo.ID, + } + for _, path := range request.UpdateMask.Paths { + if path == "content" { + contentLengthLimit, err := s.getContentLengthLimit(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get content length limit") + } + if len(request.Memo.Content) > contentLengthLimit { + return nil, status.Errorf(codes.InvalidArgument, "content too long (max %d characters)", contentLengthLimit) + } + memo.Content = request.Memo.Content + if err := memopayload.RebuildMemoPayload(memo); err != nil { + return nil, status.Errorf(codes.Internal, "failed to rebuild memo payload: %v", err) + } + update.Content = &memo.Content + update.Payload = memo.Payload + } else if path == "visibility" { + workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get workspace memo related setting") + } + visibility := convertVisibilityToStore(request.Memo.Visibility) + if workspaceMemoRelatedSetting.DisallowPublicVisibility && visibility == store.Public { + return nil, status.Errorf(codes.PermissionDenied, "disable public memos system setting is enabled") + } + update.Visibility = &visibility + } else if path == "pinned" { + update.Pinned = &request.Memo.Pinned + } else if path == "state" { + rowStatus := convertStateToStore(request.Memo.State) + update.RowStatus = &rowStatus + } else if path == "create_time" { + createdTs := request.Memo.CreateTime.AsTime().Unix() + update.CreatedTs = &createdTs + } else if path == "update_time" { + updatedTs := time.Now().Unix() + if request.Memo.UpdateTime != nil { + updatedTs = request.Memo.UpdateTime.AsTime().Unix() + } + update.UpdatedTs = &updatedTs + } else if path == "display_time" { + displayTs := request.Memo.DisplayTime.AsTime().Unix() + memoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get workspace memo related setting") + } + if memoRelatedSetting.DisplayWithUpdateTime { + update.UpdatedTs = &displayTs + } else { + update.CreatedTs = &displayTs + } + } else if path == "location" { + payload := memo.Payload + payload.Location = convertLocationToStore(request.Memo.Location) + update.Payload = payload + } else if path == "attachments" { + _, err := s.SetMemoAttachments(ctx, &v1pb.SetMemoAttachmentsRequest{ + Name: request.Memo.Name, + Attachments: request.Memo.Attachments, + }) + if err != nil { + return nil, errors.Wrap(err, "failed to set memo attachments") + } + } else if path == "relations" { + _, err := s.SetMemoRelations(ctx, &v1pb.SetMemoRelationsRequest{ + Name: request.Memo.Name, + Relations: request.Memo.Relations, + }) + if err != nil { + return nil, errors.Wrap(err, "failed to set memo relations") + } + } + } + + if err = s.Store.UpdateMemo(ctx, update); err != nil { + return nil, status.Errorf(codes.Internal, "failed to update memo") + } + + memo, err = s.Store.GetMemo(ctx, &store.FindMemo{ + ID: &memo.ID, + }) + if err != nil { + return nil, errors.Wrap(err, "failed to get memo") + } + memoMessage, err := s.convertMemoFromStore(ctx, memo) + if err != nil { + return nil, errors.Wrap(err, "failed to convert memo") + } + // Try to dispatch webhook when memo is updated. + if err := s.DispatchMemoUpdatedWebhook(ctx, memoMessage); err != nil { + slog.Warn("Failed to dispatch memo updated webhook", slog.Any("err", err)) + } + + return memoMessage, nil +} + +func (s *APIV1Service) DeleteMemo(ctx context.Context, request *v1pb.DeleteMemoRequest) (*emptypb.Empty, error) { + memoUID, err := ExtractMemoUIDFromName(request.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err) + } + memo, err := s.Store.GetMemo(ctx, &store.FindMemo{ + UID: &memoUID, + }) + if err != nil { + return nil, err + } + if memo == nil { + return nil, status.Errorf(codes.NotFound, "memo not found") + } + + user, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user") + } + // Only the creator or admin can update the memo. + if memo.CreatorID != user.ID && !isSuperUser(user) { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + if memoMessage, err := s.convertMemoFromStore(ctx, memo); err == nil { + // Try to dispatch webhook when memo is deleted. + if err := s.DispatchMemoDeletedWebhook(ctx, memoMessage); err != nil { + slog.Warn("Failed to dispatch memo deleted webhook", slog.Any("err", err)) + } + } + + if err = s.Store.DeleteMemo(ctx, &store.DeleteMemo{ID: memo.ID}); err != nil { + return nil, status.Errorf(codes.Internal, "failed to delete memo") + } + + // Delete memo relation + if err := s.Store.DeleteMemoRelation(ctx, &store.DeleteMemoRelation{MemoID: &memo.ID}); err != nil { + return nil, status.Errorf(codes.Internal, "failed to delete memo relations") + } + + // Delete related attachments. + attachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{MemoID: &memo.ID}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list attachments") + } + for _, attachment := range attachments { + if err := s.Store.DeleteAttachment(ctx, &store.DeleteAttachment{ID: attachment.ID}); err != nil { + return nil, status.Errorf(codes.Internal, "failed to delete attachment") + } + } + + // Delete memo comments + commentType := store.MemoRelationComment + relations, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{RelatedMemoID: &memo.ID, Type: &commentType}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list memo comments") + } + for _, relation := range relations { + if err := s.Store.DeleteMemo(ctx, &store.DeleteMemo{ID: relation.MemoID}); err != nil { + return nil, status.Errorf(codes.Internal, "failed to delete memo comment") + } + } + + // Delete memo references + referenceType := store.MemoRelationReference + if err := s.Store.DeleteMemoRelation(ctx, &store.DeleteMemoRelation{RelatedMemoID: &memo.ID, Type: &referenceType}); err != nil { + return nil, status.Errorf(codes.Internal, "failed to delete memo references") + } + + return &emptypb.Empty{}, nil +} + +func (s *APIV1Service) CreateMemoComment(ctx context.Context, request *v1pb.CreateMemoCommentRequest) (*v1pb.Memo, error) { + memoUID, err := ExtractMemoUIDFromName(request.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err) + } + relatedMemo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get memo") + } + + // Create the memo comment first. + memoComment, err := s.CreateMemo(ctx, &v1pb.CreateMemoRequest{Memo: request.Comment}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to create memo") + } + memoUID, err = ExtractMemoUIDFromName(memoComment.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err) + } + memo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get memo") + } + + // Build the relation between the comment memo and the original memo. + _, err = s.Store.UpsertMemoRelation(ctx, &store.MemoRelation{ + MemoID: memo.ID, + RelatedMemoID: relatedMemo.ID, + Type: store.MemoRelationComment, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to create memo relation") + } + creatorID, err := ExtractUserIDFromName(memoComment.Creator) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid memo creator") + } + if memoComment.Visibility != v1pb.Visibility_PRIVATE && creatorID != relatedMemo.CreatorID { + activity, err := s.Store.CreateActivity(ctx, &store.Activity{ + CreatorID: creatorID, + Type: store.ActivityTypeMemoComment, + Level: store.ActivityLevelInfo, + Payload: &storepb.ActivityPayload{ + MemoComment: &storepb.ActivityMemoCommentPayload{ + MemoId: memo.ID, + RelatedMemoId: relatedMemo.ID, + }, + }, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to create activity") + } + if _, err := s.Store.CreateInbox(ctx, &store.Inbox{ + SenderID: creatorID, + ReceiverID: relatedMemo.CreatorID, + Status: store.UNREAD, + Message: &storepb.InboxMessage{ + Type: storepb.InboxMessage_MEMO_COMMENT, + ActivityId: &activity.ID, + }, + }); err != nil { + return nil, status.Errorf(codes.Internal, "failed to create inbox") + } + } + + return memoComment, nil +} + +func (s *APIV1Service) ListMemoComments(ctx context.Context, request *v1pb.ListMemoCommentsRequest) (*v1pb.ListMemoCommentsResponse, error) { + memoUID, err := ExtractMemoUIDFromName(request.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err) + } + memo, err := s.Store.GetMemo(ctx, &store.FindMemo{UID: &memoUID}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get memo") + } + + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user") + } + var memoFilter string + if currentUser == nil { + memoFilter = `visibility == "PUBLIC"` + } else { + memoFilter = fmt.Sprintf(`creator_id == %d || visibility in ["PUBLIC", "PROTECTED"]`, currentUser.ID) + } + memoRelationComment := store.MemoRelationComment + memoRelations, err := s.Store.ListMemoRelations(ctx, &store.FindMemoRelation{ + RelatedMemoID: &memo.ID, + Type: &memoRelationComment, + MemoFilter: &memoFilter, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list memo relations") + } + + var memos []*v1pb.Memo + for _, memoRelation := range memoRelations { + memo, err := s.Store.GetMemo(ctx, &store.FindMemo{ + ID: &memoRelation.MemoID, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get memo") + } + if memo != nil { + memoMessage, err := s.convertMemoFromStore(ctx, memo) + if err != nil { + return nil, errors.Wrap(err, "failed to convert memo") + } + memos = append(memos, memoMessage) + } + } + + response := &v1pb.ListMemoCommentsResponse{ + Memos: memos, + } + return response, nil +} + +func (s *APIV1Service) RenameMemoTag(ctx context.Context, request *v1pb.RenameMemoTagRequest) (*emptypb.Empty, error) { + user, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user") + } + + memoFind := &store.FindMemo{ + CreatorID: &user.ID, + PayloadFind: &store.FindMemoPayload{TagSearch: []string{request.OldTag}}, + ExcludeComments: true, + } + if (request.Parent) != "memos/-" { + memoUID, err := ExtractMemoUIDFromName(request.Parent) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err) + } + memoFind.UID = &memoUID + } + + memos, err := s.Store.ListMemos(ctx, memoFind) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list memos") + } + + for _, memo := range memos { + nodes, err := parser.Parse(tokenizer.Tokenize(memo.Content)) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to parse memo: %v", err) + } + memopayload.TraverseASTNodes(nodes, func(node ast.Node) { + if tag, ok := node.(*ast.Tag); ok && tag.Content == request.OldTag { + tag.Content = request.NewTag + } + }) + memo.Content = restore.Restore(nodes) + if err := memopayload.RebuildMemoPayload(memo); err != nil { + return nil, status.Errorf(codes.Internal, "failed to rebuild memo payload: %v", err) + } + if err := s.Store.UpdateMemo(ctx, &store.UpdateMemo{ + ID: memo.ID, + Content: &memo.Content, + Payload: memo.Payload, + }); err != nil { + return nil, status.Errorf(codes.Internal, "failed to update memo: %v", err) + } + } + + return &emptypb.Empty{}, nil +} + +func (s *APIV1Service) DeleteMemoTag(ctx context.Context, request *v1pb.DeleteMemoTagRequest) (*emptypb.Empty, error) { + user, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user") + } + + memoFind := &store.FindMemo{ + CreatorID: &user.ID, + PayloadFind: &store.FindMemoPayload{TagSearch: []string{request.Tag}}, + ExcludeContent: true, + ExcludeComments: true, + } + if request.Parent != "memos/-" { + memoUID, err := ExtractMemoUIDFromName(request.Parent) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid memo name: %v", err) + } + memoFind.UID = &memoUID + } + + memos, err := s.Store.ListMemos(ctx, memoFind) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list memos") + } + + for _, memo := range memos { + if request.DeleteRelatedMemos { + err := s.Store.DeleteMemo(ctx, &store.DeleteMemo{ID: memo.ID}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to delete memo") + } + } else { + archived := store.Archived + err := s.Store.UpdateMemo(ctx, &store.UpdateMemo{ + ID: memo.ID, + RowStatus: &archived, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to update memo") + } + } + } + + return &emptypb.Empty{}, nil +} + +func (s *APIV1Service) getContentLengthLimit(ctx context.Context) (int, error) { + workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx) + if err != nil { + return 0, status.Errorf(codes.Internal, "failed to get workspace memo related setting") + } + return int(workspaceMemoRelatedSetting.ContentLengthLimit), nil +} + +// DispatchMemoCreatedWebhook dispatches webhook when memo is created. +func (s *APIV1Service) DispatchMemoCreatedWebhook(ctx context.Context, memo *v1pb.Memo) error { + return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.created") +} + +// DispatchMemoUpdatedWebhook dispatches webhook when memo is updated. +func (s *APIV1Service) DispatchMemoUpdatedWebhook(ctx context.Context, memo *v1pb.Memo) error { + return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.updated") +} + +// DispatchMemoDeletedWebhook dispatches webhook when memo is deleted. +func (s *APIV1Service) DispatchMemoDeletedWebhook(ctx context.Context, memo *v1pb.Memo) error { + return s.dispatchMemoRelatedWebhook(ctx, memo, "memos.memo.deleted") +} + +func (s *APIV1Service) dispatchMemoRelatedWebhook(ctx context.Context, memo *v1pb.Memo, activityType string) error { + creatorID, err := ExtractUserIDFromName(memo.Creator) + if err != nil { + return status.Errorf(codes.InvalidArgument, "invalid memo creator") + } + webhooks, err := s.Store.GetUserWebhooks(ctx, creatorID) + if err != nil { + return err + } + for _, hook := range webhooks { + payload, err := convertMemoToWebhookPayload(memo) + if err != nil { + return errors.Wrap(err, "failed to convert memo to webhook payload") + } + payload.ActivityType = activityType + payload.URL = hook.Url + + // Use asynchronous webhook dispatch + webhook.PostAsync(payload) + } + return nil +} + +func convertMemoToWebhookPayload(memo *v1pb.Memo) (*webhook.WebhookRequestPayload, error) { + creatorID, err := ExtractUserIDFromName(memo.Creator) + if err != nil { + return nil, errors.Wrap(err, "invalid memo creator") + } + return &webhook.WebhookRequestPayload{ + Creator: fmt.Sprintf("%s%d", UserNamePrefix, creatorID), + Memo: memo, + }, nil +} + +func getMemoContentSnippet(content string) (string, error) { + nodes, err := parser.Parse(tokenizer.Tokenize(content)) + if err != nil { + return "", errors.Wrap(err, "failed to parse content") + } + + plainText := renderer.NewStringRenderer().Render(nodes) + if len(plainText) > 64 { + return substring(plainText, 64) + "...", nil + } + return plainText, nil +} + +func substring(s string, length int) string { + if length <= 0 { + return "" + } + + runeCount := 0 + byteIndex := 0 + for byteIndex < len(s) { + _, size := utf8.DecodeRuneInString(s[byteIndex:]) + byteIndex += size + runeCount++ + if runeCount == length { + break + } + } + + return s[:byteIndex] +} + +// parseMemoOrderBy parses the order_by field and sets the appropriate ordering in memoFind. +func (*APIV1Service) parseMemoOrderBy(orderBy string, memoFind *store.FindMemo) error { + // Parse order_by field like "display_time desc" or "create_time asc" + parts := strings.Fields(strings.TrimSpace(orderBy)) + if len(parts) == 0 { + return errors.New("empty order_by") + } + + field := parts[0] + direction := "desc" // default + if len(parts) > 1 { + direction = strings.ToLower(parts[1]) + if direction != "asc" && direction != "desc" { + return errors.Errorf("invalid order direction: %s, must be 'asc' or 'desc'", parts[1]) + } + } + + switch field { + case "display_time": + memoFind.OrderByTimeAsc = direction == "asc" + case "create_time": + memoFind.OrderByTimeAsc = direction == "asc" + case "update_time": + memoFind.OrderByUpdatedTs = true + memoFind.OrderByTimeAsc = direction == "asc" + case "name": + // For ordering by memo name/id - not commonly used but supported + memoFind.OrderByTimeAsc = direction == "asc" + default: + return errors.Errorf("unsupported order field: %s, supported fields are: display_time, create_time, update_time, name", field) + } + + return nil +} diff --git a/server/router/api/v1/memo_service_converter.go b/server/router/api/v1/memo_service_converter.go new file mode 100644 index 0000000..c8433c3 --- /dev/null +++ b/server/router/api/v1/memo_service_converter.go @@ -0,0 +1,149 @@ +package v1 + +import ( + "context" + "fmt" + "time" + + "github.com/pkg/errors" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/usememos/gomark/parser" + "github.com/usememos/gomark/parser/tokenizer" + + v1pb "github.com/usememos/memos/proto/gen/api/v1" + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func (s *APIV1Service) convertMemoFromStore(ctx context.Context, memo *store.Memo) (*v1pb.Memo, error) { + displayTs := memo.CreatedTs + workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to get workspace memo related setting") + } + if workspaceMemoRelatedSetting.DisplayWithUpdateTime { + displayTs = memo.UpdatedTs + } + + name := fmt.Sprintf("%s%s", MemoNamePrefix, memo.UID) + memoMessage := &v1pb.Memo{ + Name: name, + State: convertStateFromStore(memo.RowStatus), + Creator: fmt.Sprintf("%s%d", UserNamePrefix, memo.CreatorID), + CreateTime: timestamppb.New(time.Unix(memo.CreatedTs, 0)), + UpdateTime: timestamppb.New(time.Unix(memo.UpdatedTs, 0)), + DisplayTime: timestamppb.New(time.Unix(displayTs, 0)), + Content: memo.Content, + Visibility: convertVisibilityFromStore(memo.Visibility), + Pinned: memo.Pinned, + } + if memo.Payload != nil { + memoMessage.Tags = memo.Payload.Tags + memoMessage.Property = convertMemoPropertyFromStore(memo.Payload.Property) + memoMessage.Location = convertLocationFromStore(memo.Payload.Location) + } + if memo.ParentID != nil { + parent, err := s.Store.GetMemo(ctx, &store.FindMemo{ + ID: memo.ParentID, + ExcludeContent: true, + }) + if err != nil { + return nil, errors.Wrap(err, "failed to get parent memo") + } + parentName := fmt.Sprintf("%s%s", MemoNamePrefix, parent.UID) + memoMessage.Parent = &parentName + } + + listMemoRelationsResponse, err := s.ListMemoRelations(ctx, &v1pb.ListMemoRelationsRequest{Name: name}) + if err != nil { + return nil, errors.Wrap(err, "failed to list memo relations") + } + memoMessage.Relations = listMemoRelationsResponse.Relations + + listMemoAttachmentsResponse, err := s.ListMemoAttachments(ctx, &v1pb.ListMemoAttachmentsRequest{Name: name}) + if err != nil { + return nil, errors.Wrap(err, "failed to list memo attachments") + } + memoMessage.Attachments = listMemoAttachmentsResponse.Attachments + + listMemoReactionsResponse, err := s.ListMemoReactions(ctx, &v1pb.ListMemoReactionsRequest{Name: name}) + if err != nil { + return nil, errors.Wrap(err, "failed to list memo reactions") + } + memoMessage.Reactions = listMemoReactionsResponse.Reactions + + nodes, err := parser.Parse(tokenizer.Tokenize(memo.Content)) + if err != nil { + return nil, errors.Wrap(err, "failed to parse content") + } + memoMessage.Nodes = convertFromASTNodes(nodes) + + snippet, err := getMemoContentSnippet(memo.Content) + if err != nil { + return nil, errors.Wrap(err, "failed to get memo content snippet") + } + memoMessage.Snippet = snippet + + return memoMessage, nil +} + +func convertMemoPropertyFromStore(property *storepb.MemoPayload_Property) *v1pb.Memo_Property { + if property == nil { + return nil + } + return &v1pb.Memo_Property{ + HasLink: property.HasLink, + HasTaskList: property.HasTaskList, + HasCode: property.HasCode, + HasIncompleteTasks: property.HasIncompleteTasks, + } +} + +func convertLocationFromStore(location *storepb.MemoPayload_Location) *v1pb.Location { + if location == nil { + return nil + } + return &v1pb.Location{ + Placeholder: location.Placeholder, + Latitude: location.Latitude, + Longitude: location.Longitude, + } +} + +func convertLocationToStore(location *v1pb.Location) *storepb.MemoPayload_Location { + if location == nil { + return nil + } + return &storepb.MemoPayload_Location{ + Placeholder: location.Placeholder, + Latitude: location.Latitude, + Longitude: location.Longitude, + } +} + +func convertVisibilityFromStore(visibility store.Visibility) v1pb.Visibility { + switch visibility { + case store.Private: + return v1pb.Visibility_PRIVATE + case store.Protected: + return v1pb.Visibility_PROTECTED + case store.Public: + return v1pb.Visibility_PUBLIC + default: + return v1pb.Visibility_VISIBILITY_UNSPECIFIED + } +} + +func convertVisibilityToStore(visibility v1pb.Visibility) store.Visibility { + switch visibility { + case v1pb.Visibility_PRIVATE: + return store.Private + case v1pb.Visibility_PROTECTED: + return store.Protected + case v1pb.Visibility_PUBLIC: + return store.Public + default: + return store.Private + } +} diff --git a/server/router/api/v1/memo_service_filter.go b/server/router/api/v1/memo_service_filter.go new file mode 100644 index 0000000..b4f261f --- /dev/null +++ b/server/router/api/v1/memo_service_filter.go @@ -0,0 +1,168 @@ +package v1 + +import ( + "context" + + "github.com/google/cel-go/cel" + "github.com/pkg/errors" + exprv1 "google.golang.org/genproto/googleapis/api/expr/v1alpha1" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/usememos/memos/store" +) + +func (s *APIV1Service) buildMemoFindWithFilter(ctx context.Context, find *store.FindMemo, filter string) error { + if find.PayloadFind == nil { + find.PayloadFind = &store.FindMemoPayload{} + } + if filter != "" { + filterExpr, err := parseMemoFilter(filter) + if err != nil { + return status.Errorf(codes.InvalidArgument, "invalid filter: %v", err) + } + if len(filterExpr.ContentSearch) > 0 { + find.ContentSearch = filterExpr.ContentSearch + } + if filterExpr.TagSearch != nil { + if find.PayloadFind == nil { + find.PayloadFind = &store.FindMemoPayload{} + } + find.PayloadFind.TagSearch = filterExpr.TagSearch + } + if filterExpr.DisplayTimeAfter != nil { + workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx) + if err != nil { + return status.Errorf(codes.Internal, "failed to get workspace memo related setting") + } + if workspaceMemoRelatedSetting.DisplayWithUpdateTime { + find.UpdatedTsAfter = filterExpr.DisplayTimeAfter + } else { + find.CreatedTsAfter = filterExpr.DisplayTimeAfter + } + } + if filterExpr.DisplayTimeBefore != nil { + workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx) + if err != nil { + return status.Errorf(codes.Internal, "failed to get workspace memo related setting") + } + if workspaceMemoRelatedSetting.DisplayWithUpdateTime { + find.UpdatedTsBefore = filterExpr.DisplayTimeBefore + } else { + find.CreatedTsBefore = filterExpr.DisplayTimeBefore + } + } + if filterExpr.Pinned { + pinned := true + find.Pinned = &pinned + } + if filterExpr.HasLink { + find.PayloadFind.HasLink = true + } + if filterExpr.HasTaskList { + find.PayloadFind.HasTaskList = true + } + if filterExpr.HasCode { + find.PayloadFind.HasCode = true + } + if filterExpr.HasIncompleteTasks { + find.PayloadFind.HasIncompleteTasks = true + } + } + return nil +} + +// MemoFilterCELAttributes are the CEL attributes. +var MemoFilterCELAttributes = []cel.EnvOption{ + cel.Variable("content_search", cel.ListType(cel.StringType)), + cel.Variable("tag_search", cel.ListType(cel.StringType)), + cel.Variable("display_time_before", cel.IntType), + cel.Variable("display_time_after", cel.IntType), + cel.Variable("pinned", cel.BoolType), + cel.Variable("has_link", cel.BoolType), + cel.Variable("has_task_list", cel.BoolType), + cel.Variable("has_code", cel.BoolType), + cel.Variable("has_incomplete_tasks", cel.BoolType), +} + +type MemoFilter struct { + ContentSearch []string + TagSearch []string + DisplayTimeBefore *int64 + DisplayTimeAfter *int64 + Pinned bool + HasLink bool + HasTaskList bool + HasCode bool + HasIncompleteTasks bool +} + +func parseMemoFilter(expression string) (*MemoFilter, error) { + e, err := cel.NewEnv(MemoFilterCELAttributes...) + if err != nil { + return nil, err + } + ast, issues := e.Compile(expression) + if issues != nil { + return nil, errors.Errorf("found issue %v", issues) + } + filter := &MemoFilter{} + parsedExpr, err := cel.AstToParsedExpr(ast) + if err != nil { + return nil, err + } + callExpr := parsedExpr.GetExpr().GetCallExpr() + findMemoField(callExpr, filter) + return filter, nil +} + +func findMemoField(callExpr *exprv1.Expr_Call, filter *MemoFilter) { + if len(callExpr.Args) == 2 { + idExpr := callExpr.Args[0].GetIdentExpr() + if idExpr != nil { + if idExpr.Name == "content_search" { + contentSearch := []string{} + for _, expr := range callExpr.Args[1].GetListExpr().GetElements() { + value := expr.GetConstExpr().GetStringValue() + contentSearch = append(contentSearch, value) + } + filter.ContentSearch = contentSearch + } else if idExpr.Name == "tag_search" { + tagSearch := []string{} + for _, expr := range callExpr.Args[1].GetListExpr().GetElements() { + value := expr.GetConstExpr().GetStringValue() + tagSearch = append(tagSearch, value) + } + filter.TagSearch = tagSearch + } else if idExpr.Name == "display_time_before" { + displayTimeBefore := callExpr.Args[1].GetConstExpr().GetInt64Value() + filter.DisplayTimeBefore = &displayTimeBefore + } else if idExpr.Name == "display_time_after" { + displayTimeAfter := callExpr.Args[1].GetConstExpr().GetInt64Value() + filter.DisplayTimeAfter = &displayTimeAfter + } else if idExpr.Name == "pinned" { + value := callExpr.Args[1].GetConstExpr().GetBoolValue() + filter.Pinned = value + } else if idExpr.Name == "has_link" { + value := callExpr.Args[1].GetConstExpr().GetBoolValue() + filter.HasLink = value + } else if idExpr.Name == "has_task_list" { + value := callExpr.Args[1].GetConstExpr().GetBoolValue() + filter.HasTaskList = value + } else if idExpr.Name == "has_code" { + value := callExpr.Args[1].GetConstExpr().GetBoolValue() + filter.HasCode = value + } else if idExpr.Name == "has_incomplete_tasks" { + value := callExpr.Args[1].GetConstExpr().GetBoolValue() + filter.HasIncompleteTasks = value + } + return + } + } + for _, arg := range callExpr.Args { + callExpr := arg.GetCallExpr() + if callExpr != nil { + findMemoField(callExpr, filter) + } + } +} diff --git a/server/router/api/v1/reaction_service.go b/server/router/api/v1/reaction_service.go new file mode 100644 index 0000000..cd98634 --- /dev/null +++ b/server/router/api/v1/reaction_service.go @@ -0,0 +1,90 @@ +package v1 + +import ( + "context" + "fmt" + "time" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/timestamppb" + + v1pb "github.com/usememos/memos/proto/gen/api/v1" + "github.com/usememos/memos/store" +) + +func (s *APIV1Service) ListMemoReactions(ctx context.Context, request *v1pb.ListMemoReactionsRequest) (*v1pb.ListMemoReactionsResponse, error) { + reactions, err := s.Store.ListReactions(ctx, &store.FindReaction{ + ContentID: &request.Name, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list reactions") + } + + response := &v1pb.ListMemoReactionsResponse{ + Reactions: []*v1pb.Reaction{}, + } + for _, reaction := range reactions { + reactionMessage, err := s.convertReactionFromStore(ctx, reaction) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to convert reaction") + } + response.Reactions = append(response.Reactions, reactionMessage) + } + return response, nil +} + +func (s *APIV1Service) UpsertMemoReaction(ctx context.Context, request *v1pb.UpsertMemoReactionRequest) (*v1pb.Reaction, error) { + user, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user") + } + reaction, err := s.Store.UpsertReaction(ctx, &store.Reaction{ + CreatorID: user.ID, + ContentID: request.Reaction.ContentId, + ReactionType: request.Reaction.ReactionType, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to upsert reaction") + } + + reactionMessage, err := s.convertReactionFromStore(ctx, reaction) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to convert reaction") + } + return reactionMessage, nil +} + +func (s *APIV1Service) DeleteMemoReaction(ctx context.Context, request *v1pb.DeleteMemoReactionRequest) (*emptypb.Empty, error) { + reactionID, err := ExtractReactionIDFromName(request.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid reaction name: %v", err) + } + + if err := s.Store.DeleteReaction(ctx, &store.DeleteReaction{ + ID: reactionID, + }); err != nil { + return nil, status.Errorf(codes.Internal, "failed to delete reaction") + } + + return &emptypb.Empty{}, nil +} + +func (s *APIV1Service) convertReactionFromStore(ctx context.Context, reaction *store.Reaction) (*v1pb.Reaction, error) { + creator, err := s.Store.GetUser(ctx, &store.FindUser{ + ID: &reaction.CreatorID, + }) + if err != nil { + return nil, err + } + + reactionUID := fmt.Sprintf("%d", reaction.ID) + return &v1pb.Reaction{ + Name: fmt.Sprintf("%s%s", ReactionNamePrefix, reactionUID), + Creator: fmt.Sprintf("%s%d", UserNamePrefix, creator.ID), + ContentId: reaction.ContentID, + ReactionType: reaction.ReactionType, + CreateTime: timestamppb.New(time.Unix(reaction.CreatedTs, 0)), + }, nil +} diff --git a/server/router/api/v1/resource_name.go b/server/router/api/v1/resource_name.go new file mode 100644 index 0000000..8a96dd8 --- /dev/null +++ b/server/router/api/v1/resource_name.go @@ -0,0 +1,162 @@ +package v1 + +import ( + "fmt" + "strings" + + "github.com/pkg/errors" + + "github.com/usememos/memos/internal/util" +) + +const ( + WorkspaceSettingNamePrefix = "workspace/settings/" + UserNamePrefix = "users/" + MemoNamePrefix = "memos/" + AttachmentNamePrefix = "attachments/" + ReactionNamePrefix = "reactions/" + InboxNamePrefix = "inboxes/" + IdentityProviderNamePrefix = "identityProviders/" + ActivityNamePrefix = "activities/" + WebhookNamePrefix = "webhooks/" +) + +// GetNameParentTokens returns the tokens from a resource name. +func GetNameParentTokens(name string, tokenPrefixes ...string) ([]string, error) { + parts := strings.Split(name, "/") + if len(parts) != 2*len(tokenPrefixes) { + return nil, errors.Errorf("invalid request %q", name) + } + + var tokens []string + for i, tokenPrefix := range tokenPrefixes { + if fmt.Sprintf("%s/", parts[2*i]) != tokenPrefix { + return nil, errors.Errorf("invalid prefix %q in request %q", tokenPrefix, name) + } + if parts[2*i+1] == "" { + return nil, errors.Errorf("invalid request %q with empty prefix %q", name, tokenPrefix) + } + tokens = append(tokens, parts[2*i+1]) + } + return tokens, nil +} + +func ExtractWorkspaceSettingKeyFromName(name string) (string, error) { + const prefix = "workspace/settings/" + if !strings.HasPrefix(name, prefix) { + return "", errors.Errorf("invalid workspace setting name: expected prefix %q, got %q", prefix, name) + } + + settingKey := strings.TrimPrefix(name, prefix) + if settingKey == "" { + return "", errors.Errorf("invalid workspace setting name: empty setting key in %q", name) + } + + // Ensure there are no additional path segments + if strings.Contains(settingKey, "/") { + return "", errors.Errorf("invalid workspace setting name: setting key cannot contain '/' in %q", name) + } + + return settingKey, nil +} + +// ExtractUserIDFromName returns the uid from a resource name. +func ExtractUserIDFromName(name string) (int32, error) { + tokens, err := GetNameParentTokens(name, UserNamePrefix) + if err != nil { + return 0, err + } + id, err := util.ConvertStringToInt32(tokens[0]) + if err != nil { + return 0, errors.Errorf("invalid user ID %q", tokens[0]) + } + return id, nil +} + +// ExtractMemoUIDFromName returns the memo UID from a resource name. +// e.g., "memos/uuid" -> "uuid". +func ExtractMemoUIDFromName(name string) (string, error) { + tokens, err := GetNameParentTokens(name, MemoNamePrefix) + if err != nil { + return "", err + } + id := tokens[0] + return id, nil +} + +// ExtractAttachmentUIDFromName returns the attachment UID from a resource name. +func ExtractAttachmentUIDFromName(name string) (string, error) { + tokens, err := GetNameParentTokens(name, AttachmentNamePrefix) + if err != nil { + return "", err + } + id := tokens[0] + return id, nil +} + +// ExtractReactionIDFromName returns the reaction ID from a resource name. +// e.g., "reactions/123" -> 123. +func ExtractReactionIDFromName(name string) (int32, error) { + tokens, err := GetNameParentTokens(name, ReactionNamePrefix) + if err != nil { + return 0, err + } + id, err := util.ConvertStringToInt32(tokens[0]) + if err != nil { + return 0, errors.Errorf("invalid reaction ID %q", tokens[0]) + } + return id, nil +} + +// ExtractInboxIDFromName returns the inbox ID from a resource name. +func ExtractInboxIDFromName(name string) (int32, error) { + tokens, err := GetNameParentTokens(name, InboxNamePrefix) + if err != nil { + return 0, err + } + id, err := util.ConvertStringToInt32(tokens[0]) + if err != nil { + return 0, errors.Errorf("invalid inbox ID %q", tokens[0]) + } + return id, nil +} + +func ExtractIdentityProviderIDFromName(name string) (int32, error) { + tokens, err := GetNameParentTokens(name, IdentityProviderNamePrefix) + if err != nil { + return 0, err + } + id, err := util.ConvertStringToInt32(tokens[0]) + if err != nil { + return 0, errors.Errorf("invalid identity provider ID %q", tokens[0]) + } + return id, nil +} + +func ExtractActivityIDFromName(name string) (int32, error) { + tokens, err := GetNameParentTokens(name, ActivityNamePrefix) + if err != nil { + return 0, err + } + id, err := util.ConvertStringToInt32(tokens[0]) + if err != nil { + return 0, errors.Errorf("invalid activity ID %q", tokens[0]) + } + return id, nil +} + +// ExtractWebhookIDFromName returns the webhook ID from a resource name. +func ExtractWebhookIDFromName(name string) (string, error) { + tokens, err := GetNameParentTokens(name, UserNamePrefix, WebhookNamePrefix) + if err != nil { + return "", err + } + if len(tokens) != 2 { + return "", errors.Errorf("invalid webhook name format: %q", name) + } + webhookID := tokens[1] + if webhookID == "" { + return "", errors.Errorf("invalid webhook ID %q", webhookID) + } + return webhookID, nil +} diff --git a/server/router/api/v1/shortcut_service.go b/server/router/api/v1/shortcut_service.go new file mode 100644 index 0000000..e0146e5 --- /dev/null +++ b/server/router/api/v1/shortcut_service.go @@ -0,0 +1,337 @@ +package v1 + +import ( + "context" + "fmt" + "strings" + + "github.com/pkg/errors" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + + "github.com/usememos/memos/internal/util" + "github.com/usememos/memos/plugin/filter" + v1pb "github.com/usememos/memos/proto/gen/api/v1" + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +// Helper function to extract user ID and shortcut ID from shortcut resource name. +// Format: users/{user}/shortcuts/{shortcut}. +func extractUserAndShortcutIDFromName(name string) (int32, string, error) { + parts := strings.Split(name, "/") + if len(parts) != 4 || parts[0] != "users" || parts[2] != "shortcuts" { + return 0, "", errors.Errorf("invalid shortcut name format: %s", name) + } + + userID, err := util.ConvertStringToInt32(parts[1]) + if err != nil { + return 0, "", errors.Errorf("invalid user ID %q", parts[1]) + } + + shortcutID := parts[3] + if shortcutID == "" { + return 0, "", errors.Errorf("empty shortcut ID in name: %s", name) + } + + return userID, shortcutID, nil +} + +// Helper function to construct shortcut resource name. +func constructShortcutName(userID int32, shortcutID string) string { + return fmt.Sprintf("users/%d/shortcuts/%s", userID, shortcutID) +} + +func (s *APIV1Service) ListShortcuts(ctx context.Context, request *v1pb.ListShortcutsRequest) (*v1pb.ListShortcutsResponse, error) { + userID, err := ExtractUserIDFromName(request.Parent) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err) + } + + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) + } + if currentUser == nil || currentUser.ID != userID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + userSetting, err := s.Store.GetUserSetting(ctx, &store.FindUserSetting{ + UserID: &userID, + Key: storepb.UserSetting_SHORTCUTS, + }) + if err != nil { + return nil, err + } + if userSetting == nil { + return &v1pb.ListShortcutsResponse{ + Shortcuts: []*v1pb.Shortcut{}, + }, nil + } + + shortcutsUserSetting := userSetting.GetShortcuts() + shortcuts := []*v1pb.Shortcut{} + for _, shortcut := range shortcutsUserSetting.GetShortcuts() { + shortcuts = append(shortcuts, &v1pb.Shortcut{ + Name: constructShortcutName(userID, shortcut.GetId()), + Title: shortcut.GetTitle(), + Filter: shortcut.GetFilter(), + }) + } + + return &v1pb.ListShortcutsResponse{ + Shortcuts: shortcuts, + }, nil +} + +func (s *APIV1Service) GetShortcut(ctx context.Context, request *v1pb.GetShortcutRequest) (*v1pb.Shortcut, error) { + userID, shortcutID, err := extractUserAndShortcutIDFromName(request.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid shortcut name: %v", err) + } + + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) + } + if currentUser == nil || currentUser.ID != userID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + userSetting, err := s.Store.GetUserSetting(ctx, &store.FindUserSetting{ + UserID: &userID, + Key: storepb.UserSetting_SHORTCUTS, + }) + if err != nil { + return nil, err + } + if userSetting == nil { + return nil, status.Errorf(codes.NotFound, "shortcut not found") + } + + shortcutsUserSetting := userSetting.GetShortcuts() + for _, shortcut := range shortcutsUserSetting.GetShortcuts() { + if shortcut.GetId() == shortcutID { + return &v1pb.Shortcut{ + Name: constructShortcutName(userID, shortcut.GetId()), + Title: shortcut.GetTitle(), + Filter: shortcut.GetFilter(), + }, nil + } + } + + return nil, status.Errorf(codes.NotFound, "shortcut not found") +} + +func (s *APIV1Service) CreateShortcut(ctx context.Context, request *v1pb.CreateShortcutRequest) (*v1pb.Shortcut, error) { + userID, err := ExtractUserIDFromName(request.Parent) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err) + } + + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) + } + if currentUser == nil || currentUser.ID != userID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + newShortcut := &storepb.ShortcutsUserSetting_Shortcut{ + Id: util.GenUUID(), + Title: request.Shortcut.GetTitle(), + Filter: request.Shortcut.GetFilter(), + } + if newShortcut.Title == "" { + return nil, status.Errorf(codes.InvalidArgument, "title is required") + } + if err := s.validateFilter(ctx, newShortcut.Filter); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid filter: %v", err) + } + if request.ValidateOnly { + return &v1pb.Shortcut{ + Name: constructShortcutName(userID, newShortcut.GetId()), + Title: newShortcut.GetTitle(), + Filter: newShortcut.GetFilter(), + }, nil + } + + userSetting, err := s.Store.GetUserSetting(ctx, &store.FindUserSetting{ + UserID: &userID, + Key: storepb.UserSetting_SHORTCUTS, + }) + if err != nil { + return nil, err + } + if userSetting == nil { + userSetting = &storepb.UserSetting{ + UserId: userID, + Key: storepb.UserSetting_SHORTCUTS, + Value: &storepb.UserSetting_Shortcuts{ + Shortcuts: &storepb.ShortcutsUserSetting{ + Shortcuts: []*storepb.ShortcutsUserSetting_Shortcut{}, + }, + }, + } + } + shortcutsUserSetting := userSetting.GetShortcuts() + shortcuts := shortcutsUserSetting.GetShortcuts() + shortcuts = append(shortcuts, newShortcut) + shortcutsUserSetting.Shortcuts = shortcuts + + userSetting.Value = &storepb.UserSetting_Shortcuts{ + Shortcuts: shortcutsUserSetting, + } + + _, err = s.Store.UpsertUserSetting(ctx, userSetting) + if err != nil { + return nil, err + } + + return &v1pb.Shortcut{ + Name: constructShortcutName(userID, newShortcut.GetId()), + Title: newShortcut.GetTitle(), + Filter: newShortcut.GetFilter(), + }, nil +} + +func (s *APIV1Service) UpdateShortcut(ctx context.Context, request *v1pb.UpdateShortcutRequest) (*v1pb.Shortcut, error) { + userID, shortcutID, err := extractUserAndShortcutIDFromName(request.Shortcut.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid shortcut name: %v", err) + } + + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) + } + if currentUser == nil || currentUser.ID != userID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 { + return nil, status.Errorf(codes.InvalidArgument, "update mask is required") + } + + userSetting, err := s.Store.GetUserSetting(ctx, &store.FindUserSetting{ + UserID: &userID, + Key: storepb.UserSetting_SHORTCUTS, + }) + if err != nil { + return nil, err + } + if userSetting == nil { + return nil, status.Errorf(codes.NotFound, "shortcut not found") + } + + shortcutsUserSetting := userSetting.GetShortcuts() + shortcuts := shortcutsUserSetting.GetShortcuts() + var foundShortcut *storepb.ShortcutsUserSetting_Shortcut + newShortcuts := make([]*storepb.ShortcutsUserSetting_Shortcut, 0, len(shortcuts)) + for _, shortcut := range shortcuts { + if shortcut.GetId() == shortcutID { + foundShortcut = shortcut + for _, field := range request.UpdateMask.Paths { + if field == "title" { + if request.Shortcut.GetTitle() == "" { + return nil, status.Errorf(codes.InvalidArgument, "title is required") + } + shortcut.Title = request.Shortcut.GetTitle() + } else if field == "filter" { + if err := s.validateFilter(ctx, request.Shortcut.GetFilter()); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid filter: %v", err) + } + shortcut.Filter = request.Shortcut.GetFilter() + } + } + } + newShortcuts = append(newShortcuts, shortcut) + } + + if foundShortcut == nil { + return nil, status.Errorf(codes.NotFound, "shortcut not found") + } + + shortcutsUserSetting.Shortcuts = newShortcuts + userSetting.Value = &storepb.UserSetting_Shortcuts{ + Shortcuts: shortcutsUserSetting, + } + _, err = s.Store.UpsertUserSetting(ctx, userSetting) + if err != nil { + return nil, err + } + + return &v1pb.Shortcut{ + Name: constructShortcutName(userID, foundShortcut.GetId()), + Title: foundShortcut.GetTitle(), + Filter: foundShortcut.GetFilter(), + }, nil +} + +func (s *APIV1Service) DeleteShortcut(ctx context.Context, request *v1pb.DeleteShortcutRequest) (*emptypb.Empty, error) { + userID, shortcutID, err := extractUserAndShortcutIDFromName(request.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid shortcut name: %v", err) + } + + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) + } + if currentUser == nil || currentUser.ID != userID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + userSetting, err := s.Store.GetUserSetting(ctx, &store.FindUserSetting{ + UserID: &userID, + Key: storepb.UserSetting_SHORTCUTS, + }) + if err != nil { + return nil, err + } + if userSetting == nil { + return nil, status.Errorf(codes.NotFound, "shortcut not found") + } + + shortcutsUserSetting := userSetting.GetShortcuts() + shortcuts := shortcutsUserSetting.GetShortcuts() + newShortcuts := make([]*storepb.ShortcutsUserSetting_Shortcut, 0, len(shortcuts)) + found := false + for _, shortcut := range shortcuts { + if shortcut.GetId() != shortcutID { + newShortcuts = append(newShortcuts, shortcut) + } else { + found = true + } + } + if !found { + return nil, status.Errorf(codes.NotFound, "shortcut not found") + } + shortcutsUserSetting.Shortcuts = newShortcuts + userSetting.Value = &storepb.UserSetting_Shortcuts{ + Shortcuts: shortcutsUserSetting, + } + _, err = s.Store.UpsertUserSetting(ctx, userSetting) + if err != nil { + return nil, err + } + + return &emptypb.Empty{}, nil +} + +func (s *APIV1Service) validateFilter(_ context.Context, filterStr string) error { + if filterStr == "" { + return errors.New("filter cannot be empty") + } + // Validate the filter. + parsedExpr, err := filter.Parse(filterStr, filter.MemoFilterCELAttributes...) + if err != nil { + return errors.Wrap(err, "failed to parse filter") + } + convertCtx := filter.NewConvertContext() + err = s.Store.GetDriver().ConvertExprToSQL(convertCtx, parsedExpr.GetExpr()) + if err != nil { + return errors.Wrap(err, "failed to convert filter to SQL") + } + return nil +} diff --git a/server/router/api/v1/test/idp_service_test.go b/server/router/api/v1/test/idp_service_test.go new file mode 100644 index 0000000..fb37631 --- /dev/null +++ b/server/router/api/v1/test/idp_service_test.go @@ -0,0 +1,519 @@ +package v1 + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/fieldmaskpb" + + v1pb "github.com/usememos/memos/proto/gen/api/v1" +) + +func TestCreateIdentityProvider(t *testing.T) { + ctx := context.Background() + + t.Run("CreateIdentityProvider success", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create host user + hostUser, err := ts.CreateHostUser(ctx, "admin") + require.NoError(t, err) + + // Set user context + ctx := ts.CreateUserContext(ctx, hostUser.ID) + + // Create OAuth2 identity provider + req := &v1pb.CreateIdentityProviderRequest{ + IdentityProvider: &v1pb.IdentityProvider{ + Title: "Test OAuth2 Provider", + IdentifierFilter: "", + Type: v1pb.IdentityProvider_OAUTH2, + Config: &v1pb.IdentityProviderConfig{ + Config: &v1pb.IdentityProviderConfig_Oauth2Config{ + Oauth2Config: &v1pb.OAuth2Config{ + ClientId: "test-client-id", + ClientSecret: "test-client-secret", + AuthUrl: "https://example.com/oauth/authorize", + TokenUrl: "https://example.com/oauth/token", + UserInfoUrl: "https://example.com/oauth/userinfo", + Scopes: []string{"openid", "profile", "email"}, + FieldMapping: &v1pb.FieldMapping{ + Identifier: "id", + DisplayName: "name", + Email: "email", + AvatarUrl: "avatar_url", + }, + }, + }, + }, + }, + } + + resp, err := ts.Service.CreateIdentityProvider(ctx, req) + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, "Test OAuth2 Provider", resp.Title) + require.Equal(t, v1pb.IdentityProvider_OAUTH2, resp.Type) + require.Contains(t, resp.Name, "identityProviders/") + require.NotNil(t, resp.Config.GetOauth2Config()) + require.Equal(t, "test-client-id", resp.Config.GetOauth2Config().ClientId) + }) + + t.Run("CreateIdentityProvider permission denied for non-host user", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create regular user + regularUser, err := ts.CreateRegularUser(ctx, "user") + require.NoError(t, err) + + // Set user context + ctx := ts.CreateUserContext(ctx, regularUser.ID) + + req := &v1pb.CreateIdentityProviderRequest{ + IdentityProvider: &v1pb.IdentityProvider{ + Title: "Test Provider", + Type: v1pb.IdentityProvider_OAUTH2, + }, + } + + _, err = ts.Service.CreateIdentityProvider(ctx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "permission denied") + }) + + t.Run("CreateIdentityProvider unauthenticated", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + req := &v1pb.CreateIdentityProviderRequest{ + IdentityProvider: &v1pb.IdentityProvider{ + Title: "Test Provider", + Type: v1pb.IdentityProvider_OAUTH2, + }, + } + + _, err := ts.Service.CreateIdentityProvider(ctx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "permission denied") + }) +} + +func TestListIdentityProviders(t *testing.T) { + ctx := context.Background() + + t.Run("ListIdentityProviders empty", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + req := &v1pb.ListIdentityProvidersRequest{} + resp, err := ts.Service.ListIdentityProviders(ctx, req) + require.NoError(t, err) + require.NotNil(t, resp) + require.Empty(t, resp.IdentityProviders) + }) + + t.Run("ListIdentityProviders with providers", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create host user + hostUser, err := ts.CreateHostUser(ctx, "admin") + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, hostUser.ID) + + // Create a couple of identity providers + createReq1 := &v1pb.CreateIdentityProviderRequest{ + IdentityProvider: &v1pb.IdentityProvider{ + Title: "Provider 1", + Type: v1pb.IdentityProvider_OAUTH2, + Config: &v1pb.IdentityProviderConfig{ + Config: &v1pb.IdentityProviderConfig_Oauth2Config{ + Oauth2Config: &v1pb.OAuth2Config{ + ClientId: "client1", + AuthUrl: "https://example1.com/auth", + TokenUrl: "https://example1.com/token", + UserInfoUrl: "https://example1.com/user", + FieldMapping: &v1pb.FieldMapping{ + Identifier: "id", + }, + }, + }, + }, + }, + } + + createReq2 := &v1pb.CreateIdentityProviderRequest{ + IdentityProvider: &v1pb.IdentityProvider{ + Title: "Provider 2", + Type: v1pb.IdentityProvider_OAUTH2, + Config: &v1pb.IdentityProviderConfig{ + Config: &v1pb.IdentityProviderConfig_Oauth2Config{ + Oauth2Config: &v1pb.OAuth2Config{ + ClientId: "client2", + AuthUrl: "https://example2.com/auth", + TokenUrl: "https://example2.com/token", + UserInfoUrl: "https://example2.com/user", + FieldMapping: &v1pb.FieldMapping{ + Identifier: "id", + }, + }, + }, + }, + }, + } + + _, err = ts.Service.CreateIdentityProvider(userCtx, createReq1) + require.NoError(t, err) + _, err = ts.Service.CreateIdentityProvider(userCtx, createReq2) + require.NoError(t, err) + + // List providers + listReq := &v1pb.ListIdentityProvidersRequest{} + resp, err := ts.Service.ListIdentityProviders(ctx, listReq) + require.NoError(t, err) + require.NotNil(t, resp) + require.Len(t, resp.IdentityProviders, 2) + + // Verify response contains expected providers + titles := []string{resp.IdentityProviders[0].Title, resp.IdentityProviders[1].Title} + require.Contains(t, titles, "Provider 1") + require.Contains(t, titles, "Provider 2") + }) +} + +func TestGetIdentityProvider(t *testing.T) { + ctx := context.Background() + + t.Run("GetIdentityProvider success", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create host user + hostUser, err := ts.CreateHostUser(ctx, "admin") + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, hostUser.ID) + + // Create identity provider + createReq := &v1pb.CreateIdentityProviderRequest{ + IdentityProvider: &v1pb.IdentityProvider{ + Title: "Test Provider", + Type: v1pb.IdentityProvider_OAUTH2, + Config: &v1pb.IdentityProviderConfig{ + Config: &v1pb.IdentityProviderConfig_Oauth2Config{ + Oauth2Config: &v1pb.OAuth2Config{ + ClientId: "test-client", + ClientSecret: "test-secret", + AuthUrl: "https://example.com/auth", + TokenUrl: "https://example.com/token", + UserInfoUrl: "https://example.com/user", + Scopes: []string{"openid", "profile"}, + FieldMapping: &v1pb.FieldMapping{ + Identifier: "id", + DisplayName: "name", + Email: "email", + }, + }, + }, + }, + }, + } + + created, err := ts.Service.CreateIdentityProvider(userCtx, createReq) + require.NoError(t, err) + + // Get identity provider + getReq := &v1pb.GetIdentityProviderRequest{ + Name: created.Name, + } + + resp, err := ts.Service.GetIdentityProvider(ctx, getReq) + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, created.Name, resp.Name) + require.Equal(t, "Test Provider", resp.Title) + require.Equal(t, v1pb.IdentityProvider_OAUTH2, resp.Type) + require.NotNil(t, resp.Config.GetOauth2Config()) + require.Equal(t, "test-client", resp.Config.GetOauth2Config().ClientId) + require.Equal(t, "test-secret", resp.Config.GetOauth2Config().ClientSecret) + }) + + t.Run("GetIdentityProvider not found", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + req := &v1pb.GetIdentityProviderRequest{ + Name: "identityProviders/999", + } + + _, err := ts.Service.GetIdentityProvider(ctx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "not found") + }) + + t.Run("GetIdentityProvider invalid name", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + req := &v1pb.GetIdentityProviderRequest{ + Name: "invalid-name", + } + + _, err := ts.Service.GetIdentityProvider(ctx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid identity provider name") + }) +} + +func TestUpdateIdentityProvider(t *testing.T) { + ctx := context.Background() + + t.Run("UpdateIdentityProvider success", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create host user + hostUser, err := ts.CreateHostUser(ctx, "admin") + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, hostUser.ID) + + // Create identity provider + createReq := &v1pb.CreateIdentityProviderRequest{ + IdentityProvider: &v1pb.IdentityProvider{ + Title: "Original Provider", + IdentifierFilter: "", + Type: v1pb.IdentityProvider_OAUTH2, + Config: &v1pb.IdentityProviderConfig{ + Config: &v1pb.IdentityProviderConfig_Oauth2Config{ + Oauth2Config: &v1pb.OAuth2Config{ + ClientId: "original-client", + AuthUrl: "https://original.com/auth", + TokenUrl: "https://original.com/token", + UserInfoUrl: "https://original.com/user", + FieldMapping: &v1pb.FieldMapping{ + Identifier: "id", + }, + }, + }, + }, + }, + } + + created, err := ts.Service.CreateIdentityProvider(userCtx, createReq) + require.NoError(t, err) + + // Update identity provider + updateReq := &v1pb.UpdateIdentityProviderRequest{ + IdentityProvider: &v1pb.IdentityProvider{ + Name: created.Name, + Title: "Updated Provider", + IdentifierFilter: "test@example.com", + Type: v1pb.IdentityProvider_OAUTH2, + Config: &v1pb.IdentityProviderConfig{ + Config: &v1pb.IdentityProviderConfig_Oauth2Config{ + Oauth2Config: &v1pb.OAuth2Config{ + ClientId: "updated-client", + ClientSecret: "updated-secret", + AuthUrl: "https://updated.com/auth", + TokenUrl: "https://updated.com/token", + UserInfoUrl: "https://updated.com/user", + Scopes: []string{"openid", "profile", "email"}, + FieldMapping: &v1pb.FieldMapping{ + Identifier: "sub", + DisplayName: "given_name", + Email: "email", + AvatarUrl: "picture", + }, + }, + }, + }, + }, + UpdateMask: &fieldmaskpb.FieldMask{ + Paths: []string{"title", "identifier_filter", "config"}, + }, + } + + updated, err := ts.Service.UpdateIdentityProvider(userCtx, updateReq) + require.NoError(t, err) + require.NotNil(t, updated) + require.Equal(t, "Updated Provider", updated.Title) + require.Equal(t, "test@example.com", updated.IdentifierFilter) + require.Equal(t, "updated-client", updated.Config.GetOauth2Config().ClientId) + }) + + t.Run("UpdateIdentityProvider missing update mask", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + req := &v1pb.UpdateIdentityProviderRequest{ + IdentityProvider: &v1pb.IdentityProvider{ + Name: "identityProviders/1", + Title: "Updated Provider", + }, + } + + _, err := ts.Service.UpdateIdentityProvider(ctx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "update_mask is required") + }) + + t.Run("UpdateIdentityProvider invalid name", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + req := &v1pb.UpdateIdentityProviderRequest{ + IdentityProvider: &v1pb.IdentityProvider{ + Name: "invalid-name", + Title: "Updated Provider", + }, + UpdateMask: &fieldmaskpb.FieldMask{ + Paths: []string{"title"}, + }, + } + + _, err := ts.Service.UpdateIdentityProvider(ctx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid identity provider name") + }) +} + +func TestDeleteIdentityProvider(t *testing.T) { + ctx := context.Background() + + t.Run("DeleteIdentityProvider success", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create host user + hostUser, err := ts.CreateHostUser(ctx, "admin") + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, hostUser.ID) + + // Create identity provider + createReq := &v1pb.CreateIdentityProviderRequest{ + IdentityProvider: &v1pb.IdentityProvider{ + Title: "Provider to Delete", + Type: v1pb.IdentityProvider_OAUTH2, + Config: &v1pb.IdentityProviderConfig{ + Config: &v1pb.IdentityProviderConfig_Oauth2Config{ + Oauth2Config: &v1pb.OAuth2Config{ + ClientId: "client-to-delete", + AuthUrl: "https://example.com/auth", + TokenUrl: "https://example.com/token", + UserInfoUrl: "https://example.com/user", + FieldMapping: &v1pb.FieldMapping{ + Identifier: "id", + }, + }, + }, + }, + }, + } + + created, err := ts.Service.CreateIdentityProvider(userCtx, createReq) + require.NoError(t, err) + + // Delete identity provider + deleteReq := &v1pb.DeleteIdentityProviderRequest{ + Name: created.Name, + } + + _, err = ts.Service.DeleteIdentityProvider(userCtx, deleteReq) + require.NoError(t, err) + + // Verify deletion + getReq := &v1pb.GetIdentityProviderRequest{ + Name: created.Name, + } + + _, err = ts.Service.GetIdentityProvider(ctx, getReq) + require.Error(t, err) + require.Contains(t, err.Error(), "not found") + }) + + t.Run("DeleteIdentityProvider invalid name", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + req := &v1pb.DeleteIdentityProviderRequest{ + Name: "invalid-name", + } + + _, err := ts.Service.DeleteIdentityProvider(ctx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid identity provider name") + }) + + t.Run("DeleteIdentityProvider not found", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create host user + hostUser, err := ts.CreateHostUser(ctx, "admin") + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, hostUser.ID) + + req := &v1pb.DeleteIdentityProviderRequest{ + Name: "identityProviders/999", + } + + _, err = ts.Service.DeleteIdentityProvider(userCtx, req) + require.Error(t, err) + // Note: Delete might succeed even if item doesn't exist, depending on store implementation + }) +} + +func TestIdentityProviderPermissions(t *testing.T) { + ctx := context.Background() + + t.Run("Only host users can create identity providers", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create regular user + regularUser, err := ts.CreateRegularUser(ctx, "regularuser") + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, regularUser.ID) + + req := &v1pb.CreateIdentityProviderRequest{ + IdentityProvider: &v1pb.IdentityProvider{ + Title: "Test Provider", + Type: v1pb.IdentityProvider_OAUTH2, + }, + } + + _, err = ts.Service.CreateIdentityProvider(userCtx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "permission denied") + }) + + t.Run("Authentication required", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + req := &v1pb.CreateIdentityProviderRequest{ + IdentityProvider: &v1pb.IdentityProvider{ + Title: "Test Provider", + Type: v1pb.IdentityProvider_OAUTH2, + }, + } + + _, err := ts.Service.CreateIdentityProvider(ctx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "permission denied") + }) +} diff --git a/server/router/api/v1/test/inbox_service_test.go b/server/router/api/v1/test/inbox_service_test.go new file mode 100644 index 0000000..44cc82a --- /dev/null +++ b/server/router/api/v1/test/inbox_service_test.go @@ -0,0 +1,559 @@ +package v1 + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/fieldmaskpb" + + v1pb "github.com/usememos/memos/proto/gen/api/v1" + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func TestListInboxes(t *testing.T) { + ctx := context.Background() + + t.Run("ListInboxes success", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create a user + user, err := ts.CreateRegularUser(ctx, "testuser") + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, user.ID) + + // List inboxes (should be empty initially) + req := &v1pb.ListInboxesRequest{ + Parent: fmt.Sprintf("users/%d", user.ID), + } + + resp, err := ts.Service.ListInboxes(userCtx, req) + require.NoError(t, err) + require.NotNil(t, resp) + require.Empty(t, resp.Inboxes) + require.Equal(t, int32(0), resp.TotalSize) + }) + + t.Run("ListInboxes with pagination", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create a user + user, err := ts.CreateRegularUser(ctx, "testuser") + require.NoError(t, err) + + // Create some inbox entries + const systemBotID int32 = 0 + for i := 0; i < 3; i++ { + _, err := ts.Store.CreateInbox(ctx, &store.Inbox{ + SenderID: systemBotID, + ReceiverID: user.ID, + Status: store.UNREAD, + Message: &storepb.InboxMessage{ + Type: storepb.InboxMessage_MEMO_COMMENT, + }, + }) + require.NoError(t, err) + } + + // Set user context + userCtx := ts.CreateUserContext(ctx, user.ID) + + // List inboxes with page size limit + req := &v1pb.ListInboxesRequest{ + Parent: fmt.Sprintf("users/%d", user.ID), + PageSize: 2, + } + + resp, err := ts.Service.ListInboxes(userCtx, req) + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, 2, len(resp.Inboxes)) + require.NotEmpty(t, resp.NextPageToken) + }) + + t.Run("ListInboxes permission denied for different user", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create two users + user1, err := ts.CreateRegularUser(ctx, "user1") + require.NoError(t, err) + user2, err := ts.CreateRegularUser(ctx, "user2") + require.NoError(t, err) + + // Set user1 context but try to list user2's inboxes + userCtx := ts.CreateUserContext(ctx, user1.ID) + + req := &v1pb.ListInboxesRequest{ + Parent: fmt.Sprintf("users/%d", user2.ID), + } + + _, err = ts.Service.ListInboxes(userCtx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "cannot access inboxes") + }) + + t.Run("ListInboxes host can access other users' inboxes", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create a host user and a regular user + hostUser, err := ts.CreateHostUser(ctx, "hostuser") + require.NoError(t, err) + regularUser, err := ts.CreateRegularUser(ctx, "regularuser") + require.NoError(t, err) + + // Create an inbox for the regular user + const systemBotID int32 = 0 + _, err = ts.Store.CreateInbox(ctx, &store.Inbox{ + SenderID: systemBotID, + ReceiverID: regularUser.ID, + Status: store.UNREAD, + Message: &storepb.InboxMessage{ + Type: storepb.InboxMessage_MEMO_COMMENT, + }, + }) + require.NoError(t, err) + + // Set host user context and try to list regular user's inboxes + hostCtx := ts.CreateUserContext(ctx, hostUser.ID) + + req := &v1pb.ListInboxesRequest{ + Parent: fmt.Sprintf("users/%d", regularUser.ID), + } + + resp, err := ts.Service.ListInboxes(hostCtx, req) + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, 1, len(resp.Inboxes)) + }) + + t.Run("ListInboxes invalid parent format", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create a user + user, err := ts.CreateRegularUser(ctx, "testuser") + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, user.ID) + + req := &v1pb.ListInboxesRequest{ + Parent: "invalid-parent-format", + } + + _, err = ts.Service.ListInboxes(userCtx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid parent name") + }) + + t.Run("ListInboxes unauthenticated", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + req := &v1pb.ListInboxesRequest{ + Parent: "users/1", + } + + _, err := ts.Service.ListInboxes(ctx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "user not authenticated") + }) +} + +func TestUpdateInbox(t *testing.T) { + ctx := context.Background() + + t.Run("UpdateInbox success", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create a user + user, err := ts.CreateRegularUser(ctx, "testuser") + require.NoError(t, err) + + // Create an inbox entry + const systemBotID int32 = 0 + inbox, err := ts.Store.CreateInbox(ctx, &store.Inbox{ + SenderID: systemBotID, + ReceiverID: user.ID, + Status: store.UNREAD, + Message: &storepb.InboxMessage{ + Type: storepb.InboxMessage_MEMO_COMMENT, + }, + }) + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, user.ID) + + // Update inbox status + req := &v1pb.UpdateInboxRequest{ + Inbox: &v1pb.Inbox{ + Name: fmt.Sprintf("inboxes/%d", inbox.ID), + Status: v1pb.Inbox_ARCHIVED, + }, + UpdateMask: &fieldmaskpb.FieldMask{ + Paths: []string{"status"}, + }, + } + + resp, err := ts.Service.UpdateInbox(userCtx, req) + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, v1pb.Inbox_ARCHIVED, resp.Status) + }) + + t.Run("UpdateInbox permission denied for different user", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create two users + user1, err := ts.CreateRegularUser(ctx, "user1") + require.NoError(t, err) + user2, err := ts.CreateRegularUser(ctx, "user2") + require.NoError(t, err) + + // Create an inbox entry for user2 + const systemBotID int32 = 0 + inbox, err := ts.Store.CreateInbox(ctx, &store.Inbox{ + SenderID: systemBotID, + ReceiverID: user2.ID, + Status: store.UNREAD, + Message: &storepb.InboxMessage{ + Type: storepb.InboxMessage_MEMO_COMMENT, + }, + }) + require.NoError(t, err) + + // Set user1 context but try to update user2's inbox + userCtx := ts.CreateUserContext(ctx, user1.ID) + + req := &v1pb.UpdateInboxRequest{ + Inbox: &v1pb.Inbox{ + Name: fmt.Sprintf("inboxes/%d", inbox.ID), + Status: v1pb.Inbox_ARCHIVED, + }, + UpdateMask: &fieldmaskpb.FieldMask{ + Paths: []string{"status"}, + }, + } + + _, err = ts.Service.UpdateInbox(userCtx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "cannot update inbox") + }) + + t.Run("UpdateInbox missing update mask", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create a user + user, err := ts.CreateRegularUser(ctx, "testuser") + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, user.ID) + + req := &v1pb.UpdateInboxRequest{ + Inbox: &v1pb.Inbox{ + Name: "inboxes/1", + Status: v1pb.Inbox_ARCHIVED, + }, + } + + _, err = ts.Service.UpdateInbox(userCtx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "update mask is required") + }) + + t.Run("UpdateInbox invalid name format", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create a user + user, err := ts.CreateRegularUser(ctx, "testuser") + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, user.ID) + + req := &v1pb.UpdateInboxRequest{ + Inbox: &v1pb.Inbox{ + Name: "invalid-inbox-name", + Status: v1pb.Inbox_ARCHIVED, + }, + UpdateMask: &fieldmaskpb.FieldMask{ + Paths: []string{"status"}, + }, + } + + _, err = ts.Service.UpdateInbox(userCtx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid inbox name") + }) + + t.Run("UpdateInbox not found", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create a user + user, err := ts.CreateRegularUser(ctx, "testuser") + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, user.ID) + + req := &v1pb.UpdateInboxRequest{ + Inbox: &v1pb.Inbox{ + Name: "inboxes/99999", // Non-existent inbox + Status: v1pb.Inbox_ARCHIVED, + }, + UpdateMask: &fieldmaskpb.FieldMask{ + Paths: []string{"status"}, + }, + } + + _, err = ts.Service.UpdateInbox(userCtx, req) + require.Error(t, err) + st, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, codes.NotFound, st.Code()) + }) + + t.Run("UpdateInbox unsupported field", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create a user + user, err := ts.CreateRegularUser(ctx, "testuser") + require.NoError(t, err) + + // Create an inbox entry + const systemBotID int32 = 0 + inbox, err := ts.Store.CreateInbox(ctx, &store.Inbox{ + SenderID: systemBotID, + ReceiverID: user.ID, + Status: store.UNREAD, + Message: &storepb.InboxMessage{ + Type: storepb.InboxMessage_MEMO_COMMENT, + }, + }) + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, user.ID) + + req := &v1pb.UpdateInboxRequest{ + Inbox: &v1pb.Inbox{ + Name: fmt.Sprintf("inboxes/%d", inbox.ID), + Status: v1pb.Inbox_ARCHIVED, + }, + UpdateMask: &fieldmaskpb.FieldMask{ + Paths: []string{"unsupported_field"}, + }, + } + + _, err = ts.Service.UpdateInbox(userCtx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "unsupported field") + }) +} + +func TestDeleteInbox(t *testing.T) { + ctx := context.Background() + + t.Run("DeleteInbox success", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create a user + user, err := ts.CreateRegularUser(ctx, "testuser") + require.NoError(t, err) + + // Create an inbox entry + const systemBotID int32 = 0 + inbox, err := ts.Store.CreateInbox(ctx, &store.Inbox{ + SenderID: systemBotID, + ReceiverID: user.ID, + Status: store.UNREAD, + Message: &storepb.InboxMessage{ + Type: storepb.InboxMessage_MEMO_COMMENT, + }, + }) + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, user.ID) + + // Delete inbox + req := &v1pb.DeleteInboxRequest{ + Name: fmt.Sprintf("inboxes/%d", inbox.ID), + } + + _, err = ts.Service.DeleteInbox(userCtx, req) + require.NoError(t, err) + + // Verify inbox is deleted + inboxes, err := ts.Store.ListInboxes(ctx, &store.FindInbox{ + ReceiverID: &user.ID, + }) + require.NoError(t, err) + require.Equal(t, 0, len(inboxes)) + }) + + t.Run("DeleteInbox permission denied for different user", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create two users + user1, err := ts.CreateRegularUser(ctx, "user1") + require.NoError(t, err) + user2, err := ts.CreateRegularUser(ctx, "user2") + require.NoError(t, err) + + // Create an inbox entry for user2 + const systemBotID int32 = 0 + inbox, err := ts.Store.CreateInbox(ctx, &store.Inbox{ + SenderID: systemBotID, + ReceiverID: user2.ID, + Status: store.UNREAD, + Message: &storepb.InboxMessage{ + Type: storepb.InboxMessage_MEMO_COMMENT, + }, + }) + require.NoError(t, err) + + // Set user1 context but try to delete user2's inbox + userCtx := ts.CreateUserContext(ctx, user1.ID) + + req := &v1pb.DeleteInboxRequest{ + Name: fmt.Sprintf("inboxes/%d", inbox.ID), + } + + _, err = ts.Service.DeleteInbox(userCtx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "cannot delete inbox") + }) + + t.Run("DeleteInbox invalid name format", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create a user + user, err := ts.CreateRegularUser(ctx, "testuser") + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, user.ID) + + req := &v1pb.DeleteInboxRequest{ + Name: "invalid-inbox-name", + } + + _, err = ts.Service.DeleteInbox(userCtx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid inbox name") + }) + + t.Run("DeleteInbox not found", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create a user + user, err := ts.CreateRegularUser(ctx, "testuser") + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, user.ID) + + req := &v1pb.DeleteInboxRequest{ + Name: "inboxes/99999", // Non-existent inbox + } + + _, err = ts.Service.DeleteInbox(userCtx, req) + require.Error(t, err) + st, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, codes.NotFound, st.Code()) + }) +} + +func TestInboxCRUDComplete(t *testing.T) { + ctx := context.Background() + + t.Run("Complete CRUD lifecycle", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create a user + user, err := ts.CreateRegularUser(ctx, "testuser") + require.NoError(t, err) + + // Create an inbox entry directly in store + const systemBotID int32 = 0 + inbox, err := ts.Store.CreateInbox(ctx, &store.Inbox{ + SenderID: systemBotID, + ReceiverID: user.ID, + Status: store.UNREAD, + Message: &storepb.InboxMessage{ + Type: storepb.InboxMessage_MEMO_COMMENT, + }, + }) + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, user.ID) + + // 1. List inboxes - should have 1 + listReq := &v1pb.ListInboxesRequest{ + Parent: fmt.Sprintf("users/%d", user.ID), + } + listResp, err := ts.Service.ListInboxes(userCtx, listReq) + require.NoError(t, err) + require.Equal(t, 1, len(listResp.Inboxes)) + require.Equal(t, v1pb.Inbox_UNREAD, listResp.Inboxes[0].Status) + + // 2. Update inbox status to ARCHIVED + updateReq := &v1pb.UpdateInboxRequest{ + Inbox: &v1pb.Inbox{ + Name: fmt.Sprintf("inboxes/%d", inbox.ID), + Status: v1pb.Inbox_ARCHIVED, + }, + UpdateMask: &fieldmaskpb.FieldMask{ + Paths: []string{"status"}, + }, + } + updateResp, err := ts.Service.UpdateInbox(userCtx, updateReq) + require.NoError(t, err) + require.Equal(t, v1pb.Inbox_ARCHIVED, updateResp.Status) + + // 3. List inboxes again - should still have 1 but ARCHIVED + listResp, err = ts.Service.ListInboxes(userCtx, listReq) + require.NoError(t, err) + require.Equal(t, 1, len(listResp.Inboxes)) + require.Equal(t, v1pb.Inbox_ARCHIVED, listResp.Inboxes[0].Status) + + // 4. Delete inbox + deleteReq := &v1pb.DeleteInboxRequest{ + Name: fmt.Sprintf("inboxes/%d", inbox.ID), + } + _, err = ts.Service.DeleteInbox(userCtx, deleteReq) + require.NoError(t, err) + + // 5. List inboxes - should be empty + listResp, err = ts.Service.ListInboxes(userCtx, listReq) + require.NoError(t, err) + require.Equal(t, 0, len(listResp.Inboxes)) + require.Equal(t, int32(0), listResp.TotalSize) + }) +} diff --git a/server/router/api/v1/test/shortcut_service_test.go b/server/router/api/v1/test/shortcut_service_test.go new file mode 100644 index 0000000..90921cd --- /dev/null +++ b/server/router/api/v1/test/shortcut_service_test.go @@ -0,0 +1,819 @@ +package v1 + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/fieldmaskpb" + + v1pb "github.com/usememos/memos/proto/gen/api/v1" +) + +func TestListShortcuts(t *testing.T) { + ctx := context.Background() + + t.Run("ListShortcuts success", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create a user + user, err := ts.CreateRegularUser(ctx, "testuser") + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, user.ID) + + // List shortcuts (should be empty initially) + req := &v1pb.ListShortcutsRequest{ + Parent: fmt.Sprintf("users/%d", user.ID), + } + + resp, err := ts.Service.ListShortcuts(userCtx, req) + require.NoError(t, err) + require.NotNil(t, resp) + require.Empty(t, resp.Shortcuts) + }) + + t.Run("ListShortcuts permission denied for different user", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create two users + user1, err := ts.CreateRegularUser(ctx, "user1") + require.NoError(t, err) + user2, err := ts.CreateRegularUser(ctx, "user2") + require.NoError(t, err) + + // Set user1 context but try to list user2's shortcuts + userCtx := ts.CreateUserContext(ctx, user1.ID) + + req := &v1pb.ListShortcutsRequest{ + Parent: fmt.Sprintf("users/%d", user2.ID), + } + + _, err = ts.Service.ListShortcuts(userCtx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "permission denied") + }) + + t.Run("ListShortcuts invalid parent format", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create user + user, err := ts.CreateRegularUser(ctx, "testuser") + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, user.ID) + + req := &v1pb.ListShortcutsRequest{ + Parent: "invalid-parent-format", + } + + _, err = ts.Service.ListShortcuts(userCtx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid user name") + }) + + t.Run("ListShortcuts unauthenticated", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + req := &v1pb.ListShortcutsRequest{ + Parent: "users/1", + } + + _, err := ts.Service.ListShortcuts(ctx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "permission denied") + }) +} + +func TestGetShortcut(t *testing.T) { + ctx := context.Background() + + t.Run("GetShortcut success", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create a user + user, err := ts.CreateRegularUser(ctx, "testuser") + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, user.ID) + + // First create a shortcut + createReq := &v1pb.CreateShortcutRequest{ + Parent: fmt.Sprintf("users/%d", user.ID), + Shortcut: &v1pb.Shortcut{ + Title: "Test Shortcut", + Filter: "tag in [\"test\"]", + }, + } + + created, err := ts.Service.CreateShortcut(userCtx, createReq) + require.NoError(t, err) + + // Now get the shortcut + getReq := &v1pb.GetShortcutRequest{ + Name: created.Name, + } + + resp, err := ts.Service.GetShortcut(userCtx, getReq) + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, created.Name, resp.Name) + require.Equal(t, "Test Shortcut", resp.Title) + require.Equal(t, "tag in [\"test\"]", resp.Filter) + }) + + t.Run("GetShortcut permission denied for different user", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create two users + user1, err := ts.CreateRegularUser(ctx, "user1") + require.NoError(t, err) + user2, err := ts.CreateRegularUser(ctx, "user2") + require.NoError(t, err) + + // Create shortcut as user1 + user1Ctx := ts.CreateUserContext(ctx, user1.ID) + createReq := &v1pb.CreateShortcutRequest{ + Parent: fmt.Sprintf("users/%d", user1.ID), + Shortcut: &v1pb.Shortcut{ + Title: "User1 Shortcut", + Filter: "tag in [\"user1\"]", + }, + } + + created, err := ts.Service.CreateShortcut(user1Ctx, createReq) + require.NoError(t, err) + + // Try to get shortcut as user2 + user2Ctx := ts.CreateUserContext(ctx, user2.ID) + getReq := &v1pb.GetShortcutRequest{ + Name: created.Name, + } + + _, err = ts.Service.GetShortcut(user2Ctx, getReq) + require.Error(t, err) + require.Contains(t, err.Error(), "permission denied") + }) + + t.Run("GetShortcut invalid name format", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create user + user, err := ts.CreateRegularUser(ctx, "testuser") + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, user.ID) + + req := &v1pb.GetShortcutRequest{ + Name: "invalid-shortcut-name", + } + + _, err = ts.Service.GetShortcut(userCtx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid shortcut name") + }) + + t.Run("GetShortcut not found", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create user + user, err := ts.CreateRegularUser(ctx, "testuser") + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, user.ID) + + req := &v1pb.GetShortcutRequest{ + Name: fmt.Sprintf("users/%d", user.ID) + "/shortcuts/nonexistent", + } + + _, err = ts.Service.GetShortcut(userCtx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "not found") + }) +} + +func TestCreateShortcut(t *testing.T) { + ctx := context.Background() + + t.Run("CreateShortcut success", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create a user + user, err := ts.CreateRegularUser(ctx, "testuser") + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, user.ID) + + req := &v1pb.CreateShortcutRequest{ + Parent: fmt.Sprintf("users/%d", user.ID), + Shortcut: &v1pb.Shortcut{ + Title: "My Shortcut", + Filter: "tag in [\"important\"]", + }, + } + + resp, err := ts.Service.CreateShortcut(userCtx, req) + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, "My Shortcut", resp.Title) + require.Equal(t, "tag in [\"important\"]", resp.Filter) + require.Contains(t, resp.Name, fmt.Sprintf("users/%d/shortcuts/", user.ID)) + + // Verify the shortcut was created by listing + listReq := &v1pb.ListShortcutsRequest{ + Parent: fmt.Sprintf("users/%d", user.ID), + } + + listResp, err := ts.Service.ListShortcuts(userCtx, listReq) + require.NoError(t, err) + require.Len(t, listResp.Shortcuts, 1) + require.Equal(t, "My Shortcut", listResp.Shortcuts[0].Title) + }) + + t.Run("CreateShortcut permission denied for different user", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create two users + user1, err := ts.CreateRegularUser(ctx, "user1") + require.NoError(t, err) + user2, err := ts.CreateRegularUser(ctx, "user2") + require.NoError(t, err) + + // Set user1 context but try to create shortcut for user2 + userCtx := ts.CreateUserContext(ctx, user1.ID) + + req := &v1pb.CreateShortcutRequest{ + Parent: fmt.Sprintf("users/%d", user2.ID), + Shortcut: &v1pb.Shortcut{ + Title: "Forbidden Shortcut", + Filter: "tag in [\"forbidden\"]", + }, + } + + _, err = ts.Service.CreateShortcut(userCtx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "permission denied") + }) + + t.Run("CreateShortcut invalid parent format", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create user + user, err := ts.CreateRegularUser(ctx, "testuser") + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, user.ID) + + req := &v1pb.CreateShortcutRequest{ + Parent: "invalid-parent", + Shortcut: &v1pb.Shortcut{ + Title: "Test Shortcut", + Filter: "tag in [\"test\"]", + }, + } + + _, err = ts.Service.CreateShortcut(userCtx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid user name") + }) + + t.Run("CreateShortcut invalid filter", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create user + user, err := ts.CreateRegularUser(ctx, "testuser") + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, user.ID) + + req := &v1pb.CreateShortcutRequest{ + Parent: fmt.Sprintf("users/%d", user.ID), + Shortcut: &v1pb.Shortcut{ + Title: "Invalid Filter Shortcut", + Filter: "invalid||filter))syntax", + }, + } + + _, err = ts.Service.CreateShortcut(userCtx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid filter") + }) + + t.Run("CreateShortcut missing title", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create user + user, err := ts.CreateRegularUser(ctx, "testuser") + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, user.ID) + + req := &v1pb.CreateShortcutRequest{ + Parent: fmt.Sprintf("users/%d", user.ID), + Shortcut: &v1pb.Shortcut{ + Filter: "tag in [\"test\"]", + }, + } + + _, err = ts.Service.CreateShortcut(userCtx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "title is required") + }) +} + +func TestUpdateShortcut(t *testing.T) { + ctx := context.Background() + + t.Run("UpdateShortcut success", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create a user + user, err := ts.CreateRegularUser(ctx, "testuser") + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, user.ID) + + // Create a shortcut first + createReq := &v1pb.CreateShortcutRequest{ + Parent: fmt.Sprintf("users/%d", user.ID), + Shortcut: &v1pb.Shortcut{ + Title: "Original Title", + Filter: "tag in [\"original\"]", + }, + } + + created, err := ts.Service.CreateShortcut(userCtx, createReq) + require.NoError(t, err) + + // Update the shortcut + updateReq := &v1pb.UpdateShortcutRequest{ + Shortcut: &v1pb.Shortcut{ + Name: created.Name, + Title: "Updated Title", + Filter: "tag in [\"updated\"]", + }, + UpdateMask: &fieldmaskpb.FieldMask{ + Paths: []string{"title", "filter"}, + }, + } + + updated, err := ts.Service.UpdateShortcut(userCtx, updateReq) + require.NoError(t, err) + require.NotNil(t, updated) + require.Equal(t, "Updated Title", updated.Title) + require.Equal(t, "tag in [\"updated\"]", updated.Filter) + require.Equal(t, created.Name, updated.Name) + }) + + t.Run("UpdateShortcut permission denied for different user", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create two users + user1, err := ts.CreateRegularUser(ctx, "user1") + require.NoError(t, err) + user2, err := ts.CreateRegularUser(ctx, "user2") + require.NoError(t, err) + + // Create shortcut as user1 + user1Ctx := ts.CreateUserContext(ctx, user1.ID) + createReq := &v1pb.CreateShortcutRequest{ + Parent: fmt.Sprintf("users/%d", user1.ID), + Shortcut: &v1pb.Shortcut{ + Title: "User1 Shortcut", + Filter: "tag in [\"user1\"]", + }, + } + + created, err := ts.Service.CreateShortcut(user1Ctx, createReq) + require.NoError(t, err) + + // Try to update shortcut as user2 + user2Ctx := ts.CreateUserContext(ctx, user2.ID) + updateReq := &v1pb.UpdateShortcutRequest{ + Shortcut: &v1pb.Shortcut{ + Name: created.Name, + Title: "Hacked Title", + Filter: "tag in [\"hacked\"]", + }, + UpdateMask: &fieldmaskpb.FieldMask{ + Paths: []string{"title", "filter"}, + }, + } + + _, err = ts.Service.UpdateShortcut(user2Ctx, updateReq) + require.Error(t, err) + require.Contains(t, err.Error(), "permission denied") + }) + + t.Run("UpdateShortcut missing update mask", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create a user and context for authentication + user, err := ts.CreateRegularUser(ctx, "testuser") + require.NoError(t, err) + userCtx := ts.CreateUserContext(ctx, user.ID) + + req := &v1pb.UpdateShortcutRequest{ + Shortcut: &v1pb.Shortcut{ + Name: fmt.Sprintf("users/%d/shortcuts/test", user.ID), + Title: "Updated Title", + }, + } + + _, err = ts.Service.UpdateShortcut(userCtx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "update mask is required") + }) + + t.Run("UpdateShortcut invalid name format", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + req := &v1pb.UpdateShortcutRequest{ + Shortcut: &v1pb.Shortcut{ + Name: "invalid-shortcut-name", + Title: "Updated Title", + }, + UpdateMask: &fieldmaskpb.FieldMask{ + Paths: []string{"title"}, + }, + } + + _, err := ts.Service.UpdateShortcut(ctx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid shortcut name") + }) + + t.Run("UpdateShortcut invalid filter", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create user + user, err := ts.CreateRegularUser(ctx, "testuser") + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, user.ID) + + // Create a shortcut first + createReq := &v1pb.CreateShortcutRequest{ + Parent: fmt.Sprintf("users/%d", user.ID), + Shortcut: &v1pb.Shortcut{ + Title: "Test Shortcut", + Filter: "tag in [\"test\"]", + }, + } + + created, err := ts.Service.CreateShortcut(userCtx, createReq) + require.NoError(t, err) + + // Try to update with invalid filter + updateReq := &v1pb.UpdateShortcutRequest{ + Shortcut: &v1pb.Shortcut{ + Name: created.Name, + Filter: "invalid||filter))syntax", + }, + UpdateMask: &fieldmaskpb.FieldMask{ + Paths: []string{"filter"}, + }, + } + + _, err = ts.Service.UpdateShortcut(userCtx, updateReq) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid filter") + }) +} + +func TestDeleteShortcut(t *testing.T) { + ctx := context.Background() + + t.Run("DeleteShortcut success", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create a user + user, err := ts.CreateRegularUser(ctx, "testuser") + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, user.ID) + + // Create a shortcut first + createReq := &v1pb.CreateShortcutRequest{ + Parent: fmt.Sprintf("users/%d", user.ID), + Shortcut: &v1pb.Shortcut{ + Title: "Shortcut to Delete", + Filter: "tag in [\"delete\"]", + }, + } + + created, err := ts.Service.CreateShortcut(userCtx, createReq) + require.NoError(t, err) + + // Delete the shortcut + deleteReq := &v1pb.DeleteShortcutRequest{ + Name: created.Name, + } + + _, err = ts.Service.DeleteShortcut(userCtx, deleteReq) + require.NoError(t, err) + + // Verify deletion by listing shortcuts + listReq := &v1pb.ListShortcutsRequest{ + Parent: fmt.Sprintf("users/%d", user.ID), + } + + listResp, err := ts.Service.ListShortcuts(userCtx, listReq) + require.NoError(t, err) + require.Empty(t, listResp.Shortcuts) + + // Also verify by trying to get the deleted shortcut + getReq := &v1pb.GetShortcutRequest{ + Name: created.Name, + } + + _, err = ts.Service.GetShortcut(userCtx, getReq) + require.Error(t, err) + require.Contains(t, err.Error(), "not found") + }) + + t.Run("DeleteShortcut permission denied for different user", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create two users + user1, err := ts.CreateRegularUser(ctx, "user1") + require.NoError(t, err) + user2, err := ts.CreateRegularUser(ctx, "user2") + require.NoError(t, err) + + // Create shortcut as user1 + user1Ctx := ts.CreateUserContext(ctx, user1.ID) + createReq := &v1pb.CreateShortcutRequest{ + Parent: fmt.Sprintf("users/%d", user1.ID), + Shortcut: &v1pb.Shortcut{ + Title: "User1 Shortcut", + Filter: "tag in [\"user1\"]", + }, + } + + created, err := ts.Service.CreateShortcut(user1Ctx, createReq) + require.NoError(t, err) + + // Try to delete shortcut as user2 + user2Ctx := ts.CreateUserContext(ctx, user2.ID) + deleteReq := &v1pb.DeleteShortcutRequest{ + Name: created.Name, + } + + _, err = ts.Service.DeleteShortcut(user2Ctx, deleteReq) + require.Error(t, err) + require.Contains(t, err.Error(), "permission denied") + }) + + t.Run("DeleteShortcut invalid name format", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + req := &v1pb.DeleteShortcutRequest{ + Name: "invalid-shortcut-name", + } + + _, err := ts.Service.DeleteShortcut(ctx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "invalid shortcut name") + }) + + t.Run("DeleteShortcut not found", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create user + user, err := ts.CreateRegularUser(ctx, "testuser") + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, user.ID) + + req := &v1pb.DeleteShortcutRequest{ + Name: fmt.Sprintf("users/%d", user.ID) + "/shortcuts/nonexistent", + } + + _, err = ts.Service.DeleteShortcut(userCtx, req) + require.Error(t, err) + require.Contains(t, err.Error(), "not found") + }) +} + +func TestShortcutFiltering(t *testing.T) { + ctx := context.Background() + + t.Run("CreateShortcut with valid filters", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create user + user, err := ts.CreateRegularUser(ctx, "testuser") + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, user.ID) + + // Test various valid filter formats + validFilters := []string{ + "tag in [\"work\"]", + "content.contains(\"meeting\")", + "tag in [\"work\"] && content.contains(\"meeting\")", + "tag in [\"work\"] || tag in [\"personal\"]", + "creator_id == 1", + "visibility == \"PUBLIC\"", + "has_task_list == true", + "has_task_list == false", + } + + for i, filter := range validFilters { + req := &v1pb.CreateShortcutRequest{ + Parent: fmt.Sprintf("users/%d", user.ID), + Shortcut: &v1pb.Shortcut{ + Title: "Valid Filter " + string(rune(i)), + Filter: filter, + }, + } + + _, err = ts.Service.CreateShortcut(userCtx, req) + require.NoError(t, err, "Filter should be valid: %s", filter) + } + }) + + t.Run("CreateShortcut with invalid filters", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create user + user, err := ts.CreateRegularUser(ctx, "testuser") + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, user.ID) + + // Test various invalid filter formats + invalidFilters := []string{ + "tag in ", // incomplete expression + "invalid_field @in [\"value\"]", // unknown field + "tag in [\"work\"] &&", // incomplete expression + "tag in [\"work\"] || || tag in [\"test\"]", // double operator + "((tag in [\"work\"]", // unmatched parentheses + "tag in [\"work\"] && )", // mismatched parentheses + "tag == \"work\"", // wrong operator (== not supported for tags) + "tag in work", // missing brackets + } + + for _, filter := range invalidFilters { + req := &v1pb.CreateShortcutRequest{ + Parent: fmt.Sprintf("users/%d", user.ID), + Shortcut: &v1pb.Shortcut{ + Title: "Invalid Filter Test", + Filter: filter, + }, + } + + _, err = ts.Service.CreateShortcut(userCtx, req) + require.Error(t, err, "Filter should be invalid: %s", filter) + require.Contains(t, err.Error(), "invalid filter", "Error should mention invalid filter for: %s", filter) + } + }) +} + +func TestShortcutCRUDComplete(t *testing.T) { + ctx := context.Background() + + t.Run("Complete CRUD lifecycle", func(t *testing.T) { + ts := NewTestService(t) + defer ts.Cleanup() + + // Create user + user, err := ts.CreateRegularUser(ctx, "testuser") + require.NoError(t, err) + + // Set user context + userCtx := ts.CreateUserContext(ctx, user.ID) + + // 1. Create multiple shortcuts + shortcut1Req := &v1pb.CreateShortcutRequest{ + Parent: fmt.Sprintf("users/%d", user.ID), + Shortcut: &v1pb.Shortcut{ + Title: "Work Notes", + Filter: "tag in [\"work\"]", + }, + } + + shortcut2Req := &v1pb.CreateShortcutRequest{ + Parent: fmt.Sprintf("users/%d", user.ID), + Shortcut: &v1pb.Shortcut{ + Title: "Personal Notes", + Filter: "tag in [\"personal\"]", + }, + } + + created1, err := ts.Service.CreateShortcut(userCtx, shortcut1Req) + require.NoError(t, err) + require.Equal(t, "Work Notes", created1.Title) + + created2, err := ts.Service.CreateShortcut(userCtx, shortcut2Req) + require.NoError(t, err) + require.Equal(t, "Personal Notes", created2.Title) + + // 2. List shortcuts and verify both exist + listReq := &v1pb.ListShortcutsRequest{ + Parent: fmt.Sprintf("users/%d", user.ID), + } + + listResp, err := ts.Service.ListShortcuts(userCtx, listReq) + require.NoError(t, err) + require.Len(t, listResp.Shortcuts, 2) + + // 3. Get individual shortcuts + getReq1 := &v1pb.GetShortcutRequest{Name: created1.Name} + getResp1, err := ts.Service.GetShortcut(userCtx, getReq1) + require.NoError(t, err) + require.Equal(t, created1.Name, getResp1.Name) + require.Equal(t, "Work Notes", getResp1.Title) + + getReq2 := &v1pb.GetShortcutRequest{Name: created2.Name} + getResp2, err := ts.Service.GetShortcut(userCtx, getReq2) + require.NoError(t, err) + require.Equal(t, created2.Name, getResp2.Name) + require.Equal(t, "Personal Notes", getResp2.Title) + + // 4. Update one shortcut + updateReq := &v1pb.UpdateShortcutRequest{ + Shortcut: &v1pb.Shortcut{ + Name: created1.Name, + Title: "Work & Meeting Notes", + Filter: "tag in [\"work\"] || tag in [\"meeting\"]", + }, + UpdateMask: &fieldmaskpb.FieldMask{ + Paths: []string{"title", "filter"}, + }, + } + + updated, err := ts.Service.UpdateShortcut(userCtx, updateReq) + require.NoError(t, err) + require.Equal(t, "Work & Meeting Notes", updated.Title) + require.Equal(t, "tag in [\"work\"] || tag in [\"meeting\"]", updated.Filter) + + // 5. Verify update by getting it again + getUpdatedReq := &v1pb.GetShortcutRequest{Name: created1.Name} + getUpdatedResp, err := ts.Service.GetShortcut(userCtx, getUpdatedReq) + require.NoError(t, err) + require.Equal(t, "Work & Meeting Notes", getUpdatedResp.Title) + require.Equal(t, "tag in [\"work\"] || tag in [\"meeting\"]", getUpdatedResp.Filter) + + // 6. Delete one shortcut + deleteReq := &v1pb.DeleteShortcutRequest{ + Name: created2.Name, + } + + _, err = ts.Service.DeleteShortcut(userCtx, deleteReq) + require.NoError(t, err) + + // 7. Verify deletion by listing (should only have 1 left) + finalListResp, err := ts.Service.ListShortcuts(userCtx, listReq) + require.NoError(t, err) + require.Len(t, finalListResp.Shortcuts, 1) + require.Equal(t, "Work & Meeting Notes", finalListResp.Shortcuts[0].Title) + + // 8. Verify deleted shortcut can't be accessed + getDeletedReq := &v1pb.GetShortcutRequest{Name: created2.Name} + _, err = ts.Service.GetShortcut(userCtx, getDeletedReq) + require.Error(t, err) + require.Contains(t, err.Error(), "not found") + }) +} diff --git a/server/router/api/v1/test/test_helper.go b/server/router/api/v1/test/test_helper.go new file mode 100644 index 0000000..b40e9c7 --- /dev/null +++ b/server/router/api/v1/test/test_helper.go @@ -0,0 +1,81 @@ +package v1 + +import ( + "context" + "testing" + + "github.com/usememos/memos/internal/profile" + apiv1 "github.com/usememos/memos/server/router/api/v1" + "github.com/usememos/memos/store" + teststore "github.com/usememos/memos/store/test" +) + +// TestService holds the test service setup for API v1 services. +type TestService struct { + Service *apiv1.APIV1Service + Store *store.Store + Profile *profile.Profile + Secret string +} + +// NewTestService creates a new test service with SQLite database. +func NewTestService(t *testing.T) *TestService { + ctx := context.Background() + + // Create a test store with SQLite + testStore := teststore.NewTestingStore(ctx, t) + + // Create a test profile + testProfile := &profile.Profile{ + Mode: "dev", + Version: "test-1.0.0", + InstanceURL: "http://localhost:8080", + Driver: "sqlite", + DSN: ":memory:", + } + + // Create APIV1Service with nil grpcServer since we're testing direct calls + secret := "test-secret" + service := &apiv1.APIV1Service{ + Secret: secret, + Profile: testProfile, + Store: testStore, + } + + return &TestService{ + Service: service, + Store: testStore, + Profile: testProfile, + Secret: secret, + } +} + +// Cleanup clears caches and closes resources after test. +func (ts *TestService) Cleanup() { + ts.Store.Close() + // Note: Owner cache is package-level in parent package, cannot clear from test package +} + +// CreateHostUser creates a host user for testing. +func (ts *TestService) CreateHostUser(ctx context.Context, username string) (*store.User, error) { + return ts.Store.CreateUser(ctx, &store.User{ + Username: username, + Role: store.RoleHost, + Email: username + "@example.com", + }) +} + +// CreateRegularUser creates a regular user for testing. +func (ts *TestService) CreateRegularUser(ctx context.Context, username string) (*store.User, error) { + return ts.Store.CreateUser(ctx, &store.User{ + Username: username, + Role: store.RoleUser, + Email: username + "@example.com", + }) +} + +// CreateUserContext creates a context with the given user's ID for authentication. +func (*TestService) CreateUserContext(ctx context.Context, userID int32) context.Context { + // Use the real context key from the parent package + return apiv1.CreateTestUserContext(ctx, userID) +} diff --git a/server/router/api/v1/test/user_service_stats_test.go b/server/router/api/v1/test/user_service_stats_test.go new file mode 100644 index 0000000..4af148c --- /dev/null +++ b/server/router/api/v1/test/user_service_stats_test.go @@ -0,0 +1,105 @@ +package v1 + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + v1pb "github.com/usememos/memos/proto/gen/api/v1" + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func TestGetUserStats_TagCount(t *testing.T) { + ctx := context.Background() + + // Create test service + ts := NewTestService(t) + defer ts.Cleanup() + + // Create a test host user + user, err := ts.CreateHostUser(ctx, "test_user") + require.NoError(t, err) + + // Create user context for authentication + userCtx := ts.CreateUserContext(ctx, user.ID) + + // Create a memo with a single tag + memo, err := ts.Store.CreateMemo(ctx, &store.Memo{ + UID: "test-memo-1", + CreatorID: user.ID, + Content: "This is a test memo with #test tag", + Visibility: store.Public, + Payload: &storepb.MemoPayload{ + Tags: []string{"test"}, + }, + }) + require.NoError(t, err) + require.NotNil(t, memo) + + // Test GetUserStats + userName := fmt.Sprintf("users/%d", user.ID) + response, err := ts.Service.GetUserStats(userCtx, &v1pb.GetUserStatsRequest{ + Name: userName, + }) + require.NoError(t, err) + require.NotNil(t, response) + + // Check that the tag count is exactly 1, not 2 + require.Contains(t, response.TagCount, "test") + require.Equal(t, int32(1), response.TagCount["test"], "Tag count should be 1 for a single occurrence") + + // Create another memo with the same tag + memo2, err := ts.Store.CreateMemo(ctx, &store.Memo{ + UID: "test-memo-2", + CreatorID: user.ID, + Content: "Another memo with #test tag", + Visibility: store.Public, + Payload: &storepb.MemoPayload{ + Tags: []string{"test"}, + }, + }) + require.NoError(t, err) + require.NotNil(t, memo2) + + // Test GetUserStats again + response2, err := ts.Service.GetUserStats(userCtx, &v1pb.GetUserStatsRequest{ + Name: userName, + }) + require.NoError(t, err) + require.NotNil(t, response2) + + // Check that the tag count is exactly 2, not 3 + require.Contains(t, response2.TagCount, "test") + require.Equal(t, int32(2), response2.TagCount["test"], "Tag count should be 2 for two occurrences") + + // Test with a new unique tag + memo3, err := ts.Store.CreateMemo(ctx, &store.Memo{ + UID: "test-memo-3", + CreatorID: user.ID, + Content: "Memo with #unique tag", + Visibility: store.Public, + Payload: &storepb.MemoPayload{ + Tags: []string{"unique"}, + }, + }) + require.NoError(t, err) + require.NotNil(t, memo3) + + // Test GetUserStats for the new tag + response3, err := ts.Service.GetUserStats(userCtx, &v1pb.GetUserStatsRequest{ + Name: userName, + }) + require.NoError(t, err) + require.NotNil(t, response3) + + // Check that the unique tag count is exactly 1 + require.Contains(t, response3.TagCount, "unique") + require.Equal(t, int32(1), response3.TagCount["unique"], "New tag count should be 1 for first occurrence") + + // The original test tag should still be 2 + require.Contains(t, response3.TagCount, "test") + require.Equal(t, int32(2), response3.TagCount["test"], "Original tag count should remain 2") +} diff --git a/server/router/api/v1/test/webhook_service_test.go b/server/router/api/v1/test/webhook_service_test.go new file mode 100644 index 0000000..8960f94 --- /dev/null +++ b/server/router/api/v1/test/webhook_service_test.go @@ -0,0 +1,406 @@ +package v1 + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/types/known/fieldmaskpb" + + v1pb "github.com/usememos/memos/proto/gen/api/v1" +) + +func TestCreateWebhook(t *testing.T) { + ctx := context.Background() + t.Run("CreateWebhook with host user", func(t *testing.T) { + // Create test service for this specific test + ts := NewTestService(t) + defer ts.Cleanup() + + // Create and authenticate as host user + hostUser, err := ts.CreateHostUser(ctx, "admin") + require.NoError(t, err) + + userCtx := ts.CreateUserContext(ctx, hostUser.ID) + + // Create a webhook + req := &v1pb.CreateWebhookRequest{ + Parent: fmt.Sprintf("users/%d", hostUser.ID), + Webhook: &v1pb.Webhook{ + DisplayName: "Test Webhook", + Url: "https://example.com/webhook", + }, + } + + resp, err := ts.Service.CreateWebhook(userCtx, req) + + // Verify response + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, "Test Webhook", resp.DisplayName) + require.Equal(t, "https://example.com/webhook", resp.Url) + require.Contains(t, resp.Name, "webhooks/") + require.Contains(t, resp.Name, fmt.Sprintf("users/%d", hostUser.ID)) + }) + + t.Run("CreateWebhook fails without authentication", func(t *testing.T) { + // Create test service for this specific test + ts := NewTestService(t) + defer ts.Cleanup() + // Try to create webhook without authentication + req := &v1pb.CreateWebhookRequest{ + Parent: "users/1", // Dummy parent since we don't have a real user + Webhook: &v1pb.Webhook{ + DisplayName: "Test Webhook", + Url: "https://example.com/webhook", + }, + } + + _, err := ts.Service.CreateWebhook(ctx, req) + + // Should fail with permission denied or unauthenticated + require.Error(t, err) + }) + + t.Run("CreateWebhook fails with regular user", func(t *testing.T) { + // Create test service for this specific test + ts := NewTestService(t) + defer ts.Cleanup() + + // Create and authenticate as regular user + regularUser, err := ts.CreateRegularUser(ctx, "user1") + require.NoError(t, err) + + userCtx := ts.CreateUserContext(ctx, regularUser.ID) + // Try to create webhook as regular user + req := &v1pb.CreateWebhookRequest{ + Parent: fmt.Sprintf("users/%d", regularUser.ID), + Webhook: &v1pb.Webhook{ + DisplayName: "Test Webhook", + Url: "https://example.com/webhook", + }, + } + + _, err = ts.Service.CreateWebhook(userCtx, req) + + // Should fail with permission denied + require.Error(t, err) + require.Contains(t, err.Error(), "permission denied") + }) + + t.Run("CreateWebhook validates required fields", func(t *testing.T) { + // Create test service for this specific test + ts := NewTestService(t) + defer ts.Cleanup() + + // Create and authenticate as host user + hostUser, err := ts.CreateHostUser(ctx, "admin") + require.NoError(t, err) + + userCtx := ts.CreateUserContext(ctx, hostUser.ID) + // Try to create webhook with missing URL + req := &v1pb.CreateWebhookRequest{ + Parent: fmt.Sprintf("users/%d", hostUser.ID), + Webhook: &v1pb.Webhook{ + DisplayName: "Test Webhook", + // URL missing + }, + } + + _, err = ts.Service.CreateWebhook(userCtx, req) + + // Should fail with validation error + require.Error(t, err) + }) +} + +func TestListWebhooks(t *testing.T) { + ctx := context.Background() + + t.Run("ListWebhooks returns empty list initially", func(t *testing.T) { + // Create test service for this specific test + ts := NewTestService(t) + defer ts.Cleanup() + + // Create host user for authentication + hostUser, err := ts.CreateHostUser(ctx, "admin") + require.NoError(t, err) + + userCtx := ts.CreateUserContext(ctx, hostUser.ID) + // List webhooks + req := &v1pb.ListWebhooksRequest{ + Parent: fmt.Sprintf("users/%d", hostUser.ID), + } + resp, err := ts.Service.ListWebhooks(userCtx, req) + + // Verify response + require.NoError(t, err) + require.NotNil(t, resp) + require.Empty(t, resp.Webhooks) + }) + + t.Run("ListWebhooks returns created webhooks", func(t *testing.T) { + // Create test service for this specific test + ts := NewTestService(t) + defer ts.Cleanup() + + // Create host user and authenticate + hostUser, err := ts.CreateHostUser(ctx, "admin") + require.NoError(t, err) + userCtx := ts.CreateUserContext(ctx, hostUser.ID) + // Create a webhook + createReq := &v1pb.CreateWebhookRequest{ + Parent: fmt.Sprintf("users/%d", hostUser.ID), + Webhook: &v1pb.Webhook{ + DisplayName: "Test Webhook", + Url: "https://example.com/webhook", + }, + } + createdWebhook, err := ts.Service.CreateWebhook(userCtx, createReq) + require.NoError(t, err) + + // List webhooks + listReq := &v1pb.ListWebhooksRequest{ + Parent: fmt.Sprintf("users/%d", hostUser.ID), + } + resp, err := ts.Service.ListWebhooks(userCtx, listReq) + + // Verify response + require.NoError(t, err) + require.NotNil(t, resp) + require.Len(t, resp.Webhooks, 1) + require.Equal(t, createdWebhook.Name, resp.Webhooks[0].Name) + require.Equal(t, createdWebhook.Url, resp.Webhooks[0].Url) + }) + + t.Run("ListWebhooks fails without authentication", func(t *testing.T) { + // Create test service for this specific test + ts := NewTestService(t) + defer ts.Cleanup() + // Try to list webhooks without authentication + req := &v1pb.ListWebhooksRequest{ + Parent: "users/1", // Dummy parent since we don't have a real user + } + _, err := ts.Service.ListWebhooks(ctx, req) + + // Should fail with permission denied or unauthenticated + require.Error(t, err) + }) +} + +func TestGetWebhook(t *testing.T) { + ctx := context.Background() + + t.Run("GetWebhook returns webhook by name", func(t *testing.T) { + // Create test service for this specific test + ts := NewTestService(t) + defer ts.Cleanup() + + // Create host user and authenticate + hostUser, err := ts.CreateHostUser(ctx, "admin") + require.NoError(t, err) + userCtx := ts.CreateUserContext(ctx, hostUser.ID) + // Create a webhook + createReq := &v1pb.CreateWebhookRequest{ + Parent: fmt.Sprintf("users/%d", hostUser.ID), + Webhook: &v1pb.Webhook{ + DisplayName: "Test Webhook", + Url: "https://example.com/webhook", + }, + } + createdWebhook, err := ts.Service.CreateWebhook(userCtx, createReq) + require.NoError(t, err) + + // Get the webhook + getReq := &v1pb.GetWebhookRequest{ + Name: createdWebhook.Name, + } + resp, err := ts.Service.GetWebhook(userCtx, getReq) + // Verify response + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, createdWebhook.Name, resp.Name) + require.Equal(t, createdWebhook.Url, resp.Url) + }) + + t.Run("GetWebhook fails with invalid name", func(t *testing.T) { + // Create test service for this specific test + ts := NewTestService(t) + defer ts.Cleanup() + + // Create host user and authenticate + hostUser, err := ts.CreateHostUser(ctx, "admin") + require.NoError(t, err) + userCtx := ts.CreateUserContext(ctx, hostUser.ID) + + // Try to get webhook with invalid name + req := &v1pb.GetWebhookRequest{ + Name: "invalid/webhook/name", + } + _, err = ts.Service.GetWebhook(userCtx, req) + + // Should return an error + require.Error(t, err) + }) + + t.Run("GetWebhook fails with non-existent webhook", func(t *testing.T) { + // Create test service for this specific test + ts := NewTestService(t) + defer ts.Cleanup() + + // Create host user and authenticate + hostUser, err := ts.CreateHostUser(ctx, "admin") + require.NoError(t, err) + userCtx := ts.CreateUserContext(ctx, hostUser.ID) + // Try to get non-existent webhook + req := &v1pb.GetWebhookRequest{ + Name: fmt.Sprintf("users/%d/webhooks/999", hostUser.ID), + } + _, err = ts.Service.GetWebhook(userCtx, req) + + // Should return not found error + require.Error(t, err) + require.Contains(t, err.Error(), "not found") + }) +} + +func TestUpdateWebhook(t *testing.T) { + ctx := context.Background() + + t.Run("UpdateWebhook updates webhook properties", func(t *testing.T) { + // Create test service for this specific test + ts := NewTestService(t) + defer ts.Cleanup() + + // Create host user and authenticate + hostUser, err := ts.CreateHostUser(ctx, "admin") + require.NoError(t, err) + userCtx := ts.CreateUserContext(ctx, hostUser.ID) + // Create a webhook + createReq := &v1pb.CreateWebhookRequest{ + Parent: fmt.Sprintf("users/%d", hostUser.ID), + Webhook: &v1pb.Webhook{ + DisplayName: "Original Webhook", + Url: "https://example.com/webhook", + }, + } + createdWebhook, err := ts.Service.CreateWebhook(userCtx, createReq) + require.NoError(t, err) + + // Update the webhook + updateReq := &v1pb.UpdateWebhookRequest{ + Webhook: &v1pb.Webhook{ + Name: createdWebhook.Name, + Url: "https://updated.example.com/webhook", + }, + UpdateMask: &fieldmaskpb.FieldMask{ + Paths: []string{"url"}, + }, + } + resp, err := ts.Service.UpdateWebhook(userCtx, updateReq) + + // Verify response + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, createdWebhook.Name, resp.Name) + require.Equal(t, "https://updated.example.com/webhook", resp.Url) + }) + + t.Run("UpdateWebhook fails without authentication", func(t *testing.T) { + // Create test service for this specific test + ts := NewTestService(t) + defer ts.Cleanup() + // Try to update webhook without authentication + req := &v1pb.UpdateWebhookRequest{ + Webhook: &v1pb.Webhook{ + Name: "users/1/webhooks/1", + Url: "https://updated.example.com/webhook", + }, + } + + _, err := ts.Service.UpdateWebhook(ctx, req) + + // Should fail with permission denied or unauthenticated + require.Error(t, err) + }) +} + +func TestDeleteWebhook(t *testing.T) { + ctx := context.Background() + t.Run("DeleteWebhook removes webhook", func(t *testing.T) { + // Create test service for this specific test + ts := NewTestService(t) + defer ts.Cleanup() + + // Create host user and authenticate + hostUser, err := ts.CreateHostUser(ctx, "admin") + require.NoError(t, err) + userCtx := ts.CreateUserContext(ctx, hostUser.ID) + + // Create a webhook + createReq := &v1pb.CreateWebhookRequest{ + Parent: fmt.Sprintf("users/%d", hostUser.ID), + Webhook: &v1pb.Webhook{ + DisplayName: "Test Webhook", + Url: "https://example.com/webhook", + }, + } + createdWebhook, err := ts.Service.CreateWebhook(userCtx, createReq) + require.NoError(t, err) + + // Delete the webhook + deleteReq := &v1pb.DeleteWebhookRequest{ + Name: createdWebhook.Name, + } + _, err = ts.Service.DeleteWebhook(userCtx, deleteReq) + + // Verify deletion + require.NoError(t, err) + + // Try to get the deleted webhook + getReq := &v1pb.GetWebhookRequest{ + Name: createdWebhook.Name, + } + _, err = ts.Service.GetWebhook(userCtx, getReq) + + // Should return not found error + require.Error(t, err) + require.Contains(t, err.Error(), "not found") + }) + + t.Run("DeleteWebhook fails without authentication", func(t *testing.T) { + // Create test service for this specific test + ts := NewTestService(t) + defer ts.Cleanup() + // Try to delete webhook without authentication + req := &v1pb.DeleteWebhookRequest{ + Name: "users/1/webhooks/1", + } + + _, err := ts.Service.DeleteWebhook(ctx, req) + + // Should fail with permission denied or unauthenticated + require.Error(t, err) + }) + + t.Run("DeleteWebhook fails with non-existent webhook", func(t *testing.T) { + // Create test service for this specific test + ts := NewTestService(t) + defer ts.Cleanup() + + // Create host user and authenticate + hostUser, err := ts.CreateHostUser(ctx, "admin") + require.NoError(t, err) + userCtx := ts.CreateUserContext(ctx, hostUser.ID) + // Try to delete non-existent webhook + req := &v1pb.DeleteWebhookRequest{ + Name: fmt.Sprintf("users/%d/webhooks/999", hostUser.ID), + } + _, err = ts.Service.DeleteWebhook(userCtx, req) + + // Should return not found error + require.Error(t, err) + require.Contains(t, err.Error(), "not found") + }) +} diff --git a/server/router/api/v1/test/workspace_service_test.go b/server/router/api/v1/test/workspace_service_test.go new file mode 100644 index 0000000..95a93a0 --- /dev/null +++ b/server/router/api/v1/test/workspace_service_test.go @@ -0,0 +1,206 @@ +package v1 + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/require" + + v1pb "github.com/usememos/memos/proto/gen/api/v1" +) + +func TestGetWorkspaceProfile(t *testing.T) { + ctx := context.Background() + + t.Run("GetWorkspaceProfile returns workspace profile", func(t *testing.T) { + // Create test service for this specific test + ts := NewTestService(t) + defer ts.Cleanup() + + // Call GetWorkspaceProfile directly + req := &v1pb.GetWorkspaceProfileRequest{} + resp, err := ts.Service.GetWorkspaceProfile(ctx, req) + + // Verify response + require.NoError(t, err) + require.NotNil(t, resp) + + // Verify the response contains expected data + require.Equal(t, "test-1.0.0", resp.Version) + require.Equal(t, "dev", resp.Mode) + require.Equal(t, "http://localhost:8080", resp.InstanceUrl) + + // Owner should be empty since no users are created + require.Empty(t, resp.Owner) + }) + + t.Run("GetWorkspaceProfile with owner", func(t *testing.T) { + // Create test service for this specific test + ts := NewTestService(t) + defer ts.Cleanup() + + // Create a host user in the store + hostUser, err := ts.CreateHostUser(ctx, "admin") + require.NoError(t, err) + require.NotNil(t, hostUser) + + // Call GetWorkspaceProfile directly + req := &v1pb.GetWorkspaceProfileRequest{} + resp, err := ts.Service.GetWorkspaceProfile(ctx, req) + + // Verify response + require.NoError(t, err) + require.NotNil(t, resp) + + // Verify the response contains expected data including owner + require.Equal(t, "test-1.0.0", resp.Version) + require.Equal(t, "dev", resp.Mode) + require.Equal(t, "http://localhost:8080", resp.InstanceUrl) + + // User name should be "users/{id}" format where id is the user's ID + expectedOwnerName := fmt.Sprintf("users/%d", hostUser.ID) + require.Equal(t, expectedOwnerName, resp.Owner) + }) +} + +func TestGetWorkspaceProfile_Concurrency(t *testing.T) { + ctx := context.Background() + + t.Run("Concurrent access to service", func(t *testing.T) { + // Create test service for this specific test + ts := NewTestService(t) + defer ts.Cleanup() + + // Create a host user + hostUser, err := ts.CreateHostUser(ctx, "admin") + require.NoError(t, err) + expectedOwnerName := fmt.Sprintf("users/%d", hostUser.ID) + + // Make concurrent requests + numGoroutines := 10 + results := make(chan *v1pb.WorkspaceProfile, numGoroutines) + errors := make(chan error, numGoroutines) + + for i := 0; i < numGoroutines; i++ { + go func() { + req := &v1pb.GetWorkspaceProfileRequest{} + resp, err := ts.Service.GetWorkspaceProfile(ctx, req) + if err != nil { + errors <- err + return + } + results <- resp + }() + } + + // Collect all results + for i := 0; i < numGoroutines; i++ { + select { + case err := <-errors: + t.Fatalf("Goroutine returned error: %v", err) + case resp := <-results: + require.NotNil(t, resp) + require.Equal(t, "test-1.0.0", resp.Version) + require.Equal(t, "dev", resp.Mode) + require.Equal(t, "http://localhost:8080", resp.InstanceUrl) + require.Equal(t, expectedOwnerName, resp.Owner) + } + } + }) +} + +func TestGetWorkspaceSetting(t *testing.T) { + ctx := context.Background() + + t.Run("GetWorkspaceSetting - general setting", func(t *testing.T) { + // Create test service for this specific test + ts := NewTestService(t) + defer ts.Cleanup() + + // Call GetWorkspaceSetting for general setting + req := &v1pb.GetWorkspaceSettingRequest{ + Name: "workspace/settings/GENERAL", + } + resp, err := ts.Service.GetWorkspaceSetting(ctx, req) + + // Verify response + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, "workspace/settings/GENERAL", resp.Name) + + // The general setting should have a general_setting field + generalSetting := resp.GetGeneralSetting() + require.NotNil(t, generalSetting) + + // General setting should have default values + require.False(t, generalSetting.DisallowUserRegistration) + require.False(t, generalSetting.DisallowPasswordAuth) + require.Empty(t, generalSetting.AdditionalScript) + }) + + t.Run("GetWorkspaceSetting - storage setting", func(t *testing.T) { + // Create test service for this specific test + ts := NewTestService(t) + defer ts.Cleanup() + + // Create a host user for storage setting access + hostUser, err := ts.CreateHostUser(ctx, "testhost") + require.NoError(t, err) + + // Add user to context + userCtx := ts.CreateUserContext(ctx, hostUser.ID) + + // Call GetWorkspaceSetting for storage setting + req := &v1pb.GetWorkspaceSettingRequest{ + Name: "workspace/settings/STORAGE", + } + resp, err := ts.Service.GetWorkspaceSetting(userCtx, req) + + // Verify response + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, "workspace/settings/STORAGE", resp.Name) + + // The storage setting should have a storage_setting field + storageSetting := resp.GetStorageSetting() + require.NotNil(t, storageSetting) + }) + + t.Run("GetWorkspaceSetting - memo related setting", func(t *testing.T) { + // Create test service for this specific test + ts := NewTestService(t) + defer ts.Cleanup() + + // Call GetWorkspaceSetting for memo related setting + req := &v1pb.GetWorkspaceSettingRequest{ + Name: "workspace/settings/MEMO_RELATED", + } + resp, err := ts.Service.GetWorkspaceSetting(ctx, req) + + // Verify response + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, "workspace/settings/MEMO_RELATED", resp.Name) + + // The memo related setting should have a memo_related_setting field + memoRelatedSetting := resp.GetMemoRelatedSetting() + require.NotNil(t, memoRelatedSetting) + }) + + t.Run("GetWorkspaceSetting - invalid setting name", func(t *testing.T) { + // Create test service for this specific test + ts := NewTestService(t) + defer ts.Cleanup() + + // Call GetWorkspaceSetting with invalid name + req := &v1pb.GetWorkspaceSettingRequest{ + Name: "invalid/setting/name", + } + _, err := ts.Service.GetWorkspaceSetting(ctx, req) + + // Should return an error + require.Error(t, err) + require.Contains(t, err.Error(), "invalid workspace setting name") + }) +} diff --git a/server/router/api/v1/test_auth.go b/server/router/api/v1/test_auth.go new file mode 100644 index 0000000..f2f09bd --- /dev/null +++ b/server/router/api/v1/test_auth.go @@ -0,0 +1,19 @@ +package v1 + +import ( + "context" + + "github.com/usememos/memos/store" +) + +// CreateTestUserContext creates a context with user's ID for testing purposes. +// This function is only intended for use in tests. +func CreateTestUserContext(ctx context.Context, userID int32) context.Context { + return context.WithValue(ctx, userIDContextKey, userID) +} + +// CreateTestUserContextWithUser creates a context and ensures the user exists for testing. +// This function is only intended for use in tests. +func CreateTestUserContextWithUser(ctx context.Context, _ *APIV1Service, user *store.User) context.Context { + return context.WithValue(ctx, userIDContextKey, user.ID) +} diff --git a/server/router/api/v1/user_service.go b/server/router/api/v1/user_service.go new file mode 100644 index 0000000..74cacf6 --- /dev/null +++ b/server/router/api/v1/user_service.go @@ -0,0 +1,831 @@ +package v1 + +import ( + "context" + "encoding/base64" + "fmt" + "net/http" + "regexp" + "slices" + "strings" + "time" + + "github.com/golang-jwt/jwt/v5" + "github.com/labstack/echo/v4" + "github.com/pkg/errors" + "golang.org/x/crypto/bcrypt" + "google.golang.org/genproto/googleapis/api/httpbody" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/usememos/memos/internal/base" + v1pb "github.com/usememos/memos/proto/gen/api/v1" + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func (s *APIV1Service) ListUsers(ctx context.Context, _ *v1pb.ListUsersRequest) (*v1pb.ListUsersResponse, error) { + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) + } + if currentUser.Role != store.RoleHost && currentUser.Role != store.RoleAdmin { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + users, err := s.Store.ListUsers(ctx, &store.FindUser{}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list users: %v", err) + } + + // TODO: Implement proper filtering, ordering, and pagination + // For now, return all users with basic structure + response := &v1pb.ListUsersResponse{ + Users: []*v1pb.User{}, + TotalSize: int32(len(users)), + } + for _, user := range users { + response.Users = append(response.Users, convertUserFromStore(user)) + } + return response, nil +} + +func (s *APIV1Service) GetUser(ctx context.Context, request *v1pb.GetUserRequest) (*v1pb.User, error) { + userID, err := ExtractUserIDFromName(request.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err) + } + user, err := s.Store.GetUser(ctx, &store.FindUser{ + ID: &userID, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) + } + if user == nil { + return nil, status.Errorf(codes.NotFound, "user not found") + } + userPb := convertUserFromStore(user) + + // TODO: Implement read_mask field filtering + // For now, return all fields + + return userPb, nil +} + +func (s *APIV1Service) SearchUsers(ctx context.Context, request *v1pb.SearchUsersRequest) (*v1pb.SearchUsersResponse, error) { + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) + } + if currentUser.Role != store.RoleHost && currentUser.Role != store.RoleAdmin { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + // Search users by username, email, or display name + users, err := s.Store.ListUsers(ctx, &store.FindUser{}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list users: %v", err) + } + + var filteredUsers []*store.User + query := strings.ToLower(request.Query) + for _, user := range users { + if strings.Contains(strings.ToLower(user.Username), query) || + strings.Contains(strings.ToLower(user.Email), query) || + strings.Contains(strings.ToLower(user.Nickname), query) { + filteredUsers = append(filteredUsers, user) + } + } + + response := &v1pb.SearchUsersResponse{ + Users: []*v1pb.User{}, + TotalSize: int32(len(filteredUsers)), + } + for _, user := range filteredUsers { + response.Users = append(response.Users, convertUserFromStore(user)) + } + return response, nil +} + +func (s *APIV1Service) GetUserAvatar(ctx context.Context, request *v1pb.GetUserAvatarRequest) (*httpbody.HttpBody, error) { + userID, err := ExtractUserIDFromName(request.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err) + } + user, err := s.Store.GetUser(ctx, &store.FindUser{ + ID: &userID, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) + } + if user == nil { + return nil, status.Errorf(codes.NotFound, "user not found") + } + if user.AvatarURL == "" { + return nil, status.Errorf(codes.NotFound, "avatar not found") + } + + imageType, base64Data, err := extractImageInfo(user.AvatarURL) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to extract image info: %v", err) + } + imageData, err := base64.StdEncoding.DecodeString(base64Data) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to decode string: %v", err) + } + httpBody := &httpbody.HttpBody{ + ContentType: imageType, + Data: imageData, + } + return httpBody, nil +} + +func (s *APIV1Service) CreateUser(ctx context.Context, request *v1pb.CreateUserRequest) (*v1pb.User, error) { + // Check if there are any existing host users (for first-time setup detection) + hostUserType := store.RoleHost + existedHostUsers, err := s.Store.ListUsers(ctx, &store.FindUser{ + Role: &hostUserType, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list host users: %v", err) + } + + // Determine the role to assign and check permissions + var roleToAssign store.Role + if len(existedHostUsers) == 0 { + // First-time setup: create the first user as HOST (no authentication required) + roleToAssign = store.RoleHost + } else { + // Regular user creation: allow unauthenticated creation of normal users + // But if authenticated, check if user has HOST permission for any role + currentUser, err := s.GetCurrentUser(ctx) + if err == nil && currentUser != nil && currentUser.Role == store.RoleHost { + // Authenticated HOST user can create users with any role specified in request + if request.User.Role != v1pb.User_ROLE_UNSPECIFIED { + roleToAssign = convertUserRoleToStore(request.User.Role) + } else { + roleToAssign = store.RoleUser + } + } else { + // Unauthenticated or non-HOST users can only create normal users + roleToAssign = store.RoleUser + } + } + + if !base.UIDMatcher.MatchString(strings.ToLower(request.User.Username)) { + return nil, status.Errorf(codes.InvalidArgument, "invalid username: %s", request.User.Username) + } + + // If validate_only is true, just validate without creating + if request.ValidateOnly { + // Perform validation checks without actually creating the user + return &v1pb.User{ + Username: request.User.Username, + Email: request.User.Email, + DisplayName: request.User.DisplayName, + Role: convertUserRoleFromStore(roleToAssign), + }, nil + } + + passwordHash, err := bcrypt.GenerateFromPassword([]byte(request.User.Password), bcrypt.DefaultCost) + if err != nil { + return nil, echo.NewHTTPError(http.StatusInternalServerError, "failed to generate password hash").SetInternal(err) + } + + user, err := s.Store.CreateUser(ctx, &store.User{ + Username: request.User.Username, + Role: roleToAssign, + Email: request.User.Email, + Nickname: request.User.DisplayName, + PasswordHash: string(passwordHash), + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to create user: %v", err) + } + + return convertUserFromStore(user), nil +} + +func (s *APIV1Service) UpdateUser(ctx context.Context, request *v1pb.UpdateUserRequest) (*v1pb.User, error) { + if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 { + return nil, status.Errorf(codes.InvalidArgument, "update mask is empty") + } + userID, err := ExtractUserIDFromName(request.User.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err) + } + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) + } + // Check permission. + // Only allow admin or self to update user. + if currentUser.ID != userID && currentUser.Role != store.RoleAdmin && currentUser.Role != store.RoleHost { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + user, err := s.Store.GetUser(ctx, &store.FindUser{ID: &userID}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) + } + if user == nil { + // Handle allow_missing field + if request.AllowMissing { + // Could create user if missing, but for now return not found + return nil, status.Errorf(codes.NotFound, "user not found") + } + return nil, status.Errorf(codes.NotFound, "user not found") + } + + currentTs := time.Now().Unix() + update := &store.UpdateUser{ + ID: user.ID, + UpdatedTs: ¤tTs, + } + workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get workspace general setting: %v", err) + } + for _, field := range request.UpdateMask.Paths { + switch field { + case "username": + if workspaceGeneralSetting.DisallowChangeUsername { + return nil, status.Errorf(codes.PermissionDenied, "permission denied: disallow change username") + } + if !base.UIDMatcher.MatchString(strings.ToLower(request.User.Username)) { + return nil, status.Errorf(codes.InvalidArgument, "invalid username: %s", request.User.Username) + } + update.Username = &request.User.Username + case "display_name": + if workspaceGeneralSetting.DisallowChangeNickname { + return nil, status.Errorf(codes.PermissionDenied, "permission denied: disallow change nickname") + } + update.Nickname = &request.User.DisplayName + case "email": + update.Email = &request.User.Email + case "avatar_url": + update.AvatarURL = &request.User.AvatarUrl + case "description": + update.Description = &request.User.Description + case "role": + // Only allow admin to update role. + if currentUser.Role != store.RoleAdmin && currentUser.Role != store.RoleHost { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + role := convertUserRoleToStore(request.User.Role) + update.Role = &role + case "password": + passwordHash, err := bcrypt.GenerateFromPassword([]byte(request.User.Password), bcrypt.DefaultCost) + if err != nil { + return nil, echo.NewHTTPError(http.StatusInternalServerError, "failed to generate password hash").SetInternal(err) + } + passwordHashStr := string(passwordHash) + update.PasswordHash = &passwordHashStr + case "state": + rowStatus := convertStateToStore(request.User.State) + update.RowStatus = &rowStatus + default: + return nil, status.Errorf(codes.InvalidArgument, "invalid update path: %s", field) + } + } + + updatedUser, err := s.Store.UpdateUser(ctx, update) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to update user: %v", err) + } + + return convertUserFromStore(updatedUser), nil +} + +func (s *APIV1Service) DeleteUser(ctx context.Context, request *v1pb.DeleteUserRequest) (*emptypb.Empty, error) { + userID, err := ExtractUserIDFromName(request.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err) + } + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) + } + if currentUser.ID != userID && currentUser.Role != store.RoleAdmin && currentUser.Role != store.RoleHost { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + user, err := s.Store.GetUser(ctx, &store.FindUser{ID: &userID}) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) + } + if user == nil { + return nil, status.Errorf(codes.NotFound, "user not found") + } + + if err := s.Store.DeleteUser(ctx, &store.DeleteUser{ + ID: user.ID, + }); err != nil { + return nil, status.Errorf(codes.Internal, "failed to delete user: %v", err) + } + + return &emptypb.Empty{}, nil +} + +func getDefaultUserSetting() *v1pb.UserSetting { + return &v1pb.UserSetting{ + Name: "", // Will be set by caller + Locale: "en", + Appearance: "system", + MemoVisibility: "PRIVATE", + Theme: "", + } +} + +func (s *APIV1Service) GetUserSetting(ctx context.Context, request *v1pb.GetUserSettingRequest) (*v1pb.UserSetting, error) { + userID, err := ExtractUserIDFromName(request.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err) + } + + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) + } + + // Only allow user to get their own settings + if currentUser.ID != userID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + userSettings, err := s.Store.ListUserSettings(ctx, &store.FindUserSetting{ + UserID: &userID, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list user settings: %v", err) + } + + userSettingMessage := getDefaultUserSetting() + userSettingMessage.Name = fmt.Sprintf("users/%d", userID) + + for _, setting := range userSettings { + if setting.Key == storepb.UserSetting_GENERAL { + general := setting.GetGeneral() + if general != nil { + userSettingMessage.Locale = general.Locale + userSettingMessage.Appearance = general.Appearance + userSettingMessage.MemoVisibility = general.MemoVisibility + userSettingMessage.Theme = general.Theme + } + } + } + + // Backfill theme if empty: use workspace theme or default to "default" + if userSettingMessage.Theme == "" { + workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get workspace general setting: %v", err) + } + workspaceTheme := workspaceGeneralSetting.Theme + if workspaceTheme == "" { + workspaceTheme = "default" + } + userSettingMessage.Theme = workspaceTheme + } + + return userSettingMessage, nil +} + +func (s *APIV1Service) UpdateUserSetting(ctx context.Context, request *v1pb.UpdateUserSettingRequest) (*v1pb.UserSetting, error) { + // Extract user ID from the setting resource name + userID, err := ExtractUserIDFromName(request.Setting.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err) + } + + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) + } + + // Only allow user to update their own settings + if currentUser.ID != userID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 { + return nil, status.Errorf(codes.InvalidArgument, "update mask is empty") + } + + // Get the current general setting + existingGeneralSetting, err := s.Store.GetUserSetting(ctx, &store.FindUserSetting{ + UserID: &userID, + Key: storepb.UserSetting_GENERAL, + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get existing general setting: %v", err) + } + + // Create or update the general setting + generalSetting := &storepb.GeneralUserSetting{ + Locale: "en", + Appearance: "system", + MemoVisibility: "PRIVATE", + Theme: "", + } + + // If there's an existing setting, use its values as defaults + if existingGeneralSetting != nil && existingGeneralSetting.GetGeneral() != nil { + existing := existingGeneralSetting.GetGeneral() + generalSetting.Locale = existing.Locale + generalSetting.Appearance = existing.Appearance + generalSetting.MemoVisibility = existing.MemoVisibility + generalSetting.Theme = existing.Theme + } + + // Apply updates based on the update mask + for _, field := range request.UpdateMask.Paths { + switch field { + case "locale": + generalSetting.Locale = request.Setting.Locale + case "appearance": + generalSetting.Appearance = request.Setting.Appearance + case "memo_visibility": + generalSetting.MemoVisibility = request.Setting.MemoVisibility + case "theme": + generalSetting.Theme = request.Setting.Theme + default: + return nil, status.Errorf(codes.InvalidArgument, "invalid update path: %s", field) + } + } + + // Upsert the general setting + if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{ + UserId: userID, + Key: storepb.UserSetting_GENERAL, + Value: &storepb.UserSetting_General{ + General: generalSetting, + }, + }); err != nil { + return nil, status.Errorf(codes.Internal, "failed to upsert user setting: %v", err) + } + + return s.GetUserSetting(ctx, &v1pb.GetUserSettingRequest{Name: request.Setting.Name}) +} + +func (s *APIV1Service) ListUserAccessTokens(ctx context.Context, request *v1pb.ListUserAccessTokensRequest) (*v1pb.ListUserAccessTokensResponse, error) { + userID, err := ExtractUserIDFromName(request.Parent) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err) + } + + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) + } + if currentUser == nil { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + if currentUser.ID != userID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, userID) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list access tokens: %v", err) + } + + accessTokens := []*v1pb.UserAccessToken{} + for _, userAccessToken := range userAccessTokens { + claims := &ClaimsMessage{} + _, err := jwt.ParseWithClaims(userAccessToken.AccessToken, claims, func(t *jwt.Token) (any, error) { + if t.Method.Alg() != jwt.SigningMethodHS256.Name { + return nil, errors.Errorf("unexpected access token signing method=%v, expect %v", t.Header["alg"], jwt.SigningMethodHS256) + } + if kid, ok := t.Header["kid"].(string); ok { + if kid == "v1" { + return []byte(s.Secret), nil + } + } + return nil, errors.Errorf("unexpected access token kid=%v", t.Header["kid"]) + }) + if err != nil { + // If the access token is invalid or expired, just ignore it. + continue + } + + accessTokenResponse := &v1pb.UserAccessToken{ + Name: fmt.Sprintf("users/%d/accessTokens/%s", userID, userAccessToken.AccessToken), + AccessToken: userAccessToken.AccessToken, + Description: userAccessToken.Description, + IssuedAt: timestamppb.New(claims.IssuedAt.Time), + } + if claims.ExpiresAt != nil { + accessTokenResponse.ExpiresAt = timestamppb.New(claims.ExpiresAt.Time) + } + accessTokens = append(accessTokens, accessTokenResponse) + } + + // Sort by issued time in descending order. + slices.SortFunc(accessTokens, func(i, j *v1pb.UserAccessToken) int { + return int(i.IssuedAt.Seconds - j.IssuedAt.Seconds) + }) + response := &v1pb.ListUserAccessTokensResponse{ + AccessTokens: accessTokens, + } + return response, nil +} + +func (s *APIV1Service) CreateUserAccessToken(ctx context.Context, request *v1pb.CreateUserAccessTokenRequest) (*v1pb.UserAccessToken, error) { + userID, err := ExtractUserIDFromName(request.Parent) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err) + } + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) + } + if currentUser == nil { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + if currentUser.ID != userID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + expiresAt := time.Time{} + if request.AccessToken.ExpiresAt != nil { + expiresAt = request.AccessToken.ExpiresAt.AsTime() + } + + accessToken, err := GenerateAccessToken(currentUser.Username, currentUser.ID, expiresAt, []byte(s.Secret)) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to generate access token: %v", err) + } + + claims := &ClaimsMessage{} + _, err = jwt.ParseWithClaims(accessToken, claims, func(t *jwt.Token) (any, error) { + if t.Method.Alg() != jwt.SigningMethodHS256.Name { + return nil, errors.Errorf("unexpected access token signing method=%v, expect %v", t.Header["alg"], jwt.SigningMethodHS256) + } + if kid, ok := t.Header["kid"].(string); ok { + if kid == "v1" { + return []byte(s.Secret), nil + } + } + return nil, errors.Errorf("unexpected access token kid=%v", t.Header["kid"]) + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to parse access token: %v", err) + } + + // Upsert the access token to user setting store. + if err := s.UpsertAccessTokenToStore(ctx, currentUser, accessToken, request.AccessToken.Description); err != nil { + return nil, status.Errorf(codes.Internal, "failed to upsert access token to store: %v", err) + } + + userAccessToken := &v1pb.UserAccessToken{ + Name: fmt.Sprintf("users/%d/accessTokens/%s", userID, accessToken), + AccessToken: accessToken, + Description: request.AccessToken.Description, + IssuedAt: timestamppb.New(claims.IssuedAt.Time), + } + if claims.ExpiresAt != nil { + userAccessToken.ExpiresAt = timestamppb.New(claims.ExpiresAt.Time) + } + return userAccessToken, nil +} + +func (s *APIV1Service) DeleteUserAccessToken(ctx context.Context, request *v1pb.DeleteUserAccessTokenRequest) (*emptypb.Empty, error) { + // Extract user ID from the access token resource name + // Format: users/{user}/accessTokens/{access_token} + parts := strings.Split(request.Name, "/") + if len(parts) != 4 || parts[0] != "users" || parts[2] != "accessTokens" { + return nil, status.Errorf(codes.InvalidArgument, "invalid access token name format: %s", request.Name) + } + + userID, err := ExtractUserIDFromName(fmt.Sprintf("users/%s", parts[1])) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err) + } + accessTokenToDelete := parts[3] + + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) + } + if currentUser == nil { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + if currentUser.ID != userID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, currentUser.ID) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list access tokens: %v", err) + } + updatedUserAccessTokens := []*storepb.AccessTokensUserSetting_AccessToken{} + for _, userAccessToken := range userAccessTokens { + if userAccessToken.AccessToken == accessTokenToDelete { + continue + } + updatedUserAccessTokens = append(updatedUserAccessTokens, userAccessToken) + } + if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{ + UserId: currentUser.ID, + Key: storepb.UserSetting_ACCESS_TOKENS, + Value: &storepb.UserSetting_AccessTokens{ + AccessTokens: &storepb.AccessTokensUserSetting{ + AccessTokens: updatedUserAccessTokens, + }, + }, + }); err != nil { + return nil, status.Errorf(codes.Internal, "failed to upsert user setting: %v", err) + } + + return &emptypb.Empty{}, nil +} + +func (s *APIV1Service) ListUserSessions(ctx context.Context, request *v1pb.ListUserSessionsRequest) (*v1pb.ListUserSessionsResponse, error) { + userID, err := ExtractUserIDFromName(request.Parent) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err) + } + + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) + } + if currentUser == nil { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + if currentUser.ID != userID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + userSessions, err := s.Store.GetUserSessions(ctx, userID) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list sessions: %v", err) + } + + sessions := []*v1pb.UserSession{} + for _, userSession := range userSessions { + sessionResponse := &v1pb.UserSession{ + Name: fmt.Sprintf("users/%d/sessions/%s", userID, userSession.SessionId), + SessionId: userSession.SessionId, + CreateTime: userSession.CreateTime, + LastAccessedTime: userSession.LastAccessedTime, + } + + if userSession.ClientInfo != nil { + sessionResponse.ClientInfo = &v1pb.UserSession_ClientInfo{ + UserAgent: userSession.ClientInfo.UserAgent, + IpAddress: userSession.ClientInfo.IpAddress, + DeviceType: userSession.ClientInfo.DeviceType, + Os: userSession.ClientInfo.Os, + Browser: userSession.ClientInfo.Browser, + } + } + + sessions = append(sessions, sessionResponse) + } + + // Sort by last accessed time in descending order. + slices.SortFunc(sessions, func(i, j *v1pb.UserSession) int { + return int(j.LastAccessedTime.Seconds - i.LastAccessedTime.Seconds) + }) + + response := &v1pb.ListUserSessionsResponse{ + Sessions: sessions, + } + return response, nil +} + +func (s *APIV1Service) RevokeUserSession(ctx context.Context, request *v1pb.RevokeUserSessionRequest) (*emptypb.Empty, error) { + // Extract user ID and session ID from the session resource name + // Format: users/{user}/sessions/{session} + parts := strings.Split(request.Name, "/") + if len(parts) != 4 || parts[0] != "users" || parts[2] != "sessions" { + return nil, status.Errorf(codes.InvalidArgument, "invalid session name format: %s", request.Name) + } + + userID, err := ExtractUserIDFromName(fmt.Sprintf("users/%s", parts[1])) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err) + } + sessionIDToRevoke := parts[3] + + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) + } + if currentUser == nil { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + if currentUser.ID != userID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + if err := s.Store.RemoveUserSession(ctx, userID, sessionIDToRevoke); err != nil { + return nil, status.Errorf(codes.Internal, "failed to revoke session: %v", err) + } + + return &emptypb.Empty{}, nil +} + +// UpsertUserSession adds or updates a user session. +func (s *APIV1Service) UpsertUserSession(ctx context.Context, userID int32, sessionID string, clientInfo *storepb.SessionsUserSetting_ClientInfo) error { + session := &storepb.SessionsUserSetting_Session{ + SessionId: sessionID, + CreateTime: timestamppb.Now(), + LastAccessedTime: timestamppb.Now(), + ClientInfo: clientInfo, + } + + return s.Store.AddUserSession(ctx, userID, session) +} + +func (s *APIV1Service) UpsertAccessTokenToStore(ctx context.Context, user *store.User, accessToken, description string) error { + userAccessTokens, err := s.Store.GetUserAccessTokens(ctx, user.ID) + if err != nil { + return errors.Wrap(err, "failed to get user access tokens") + } + userAccessToken := storepb.AccessTokensUserSetting_AccessToken{ + AccessToken: accessToken, + Description: description, + } + userAccessTokens = append(userAccessTokens, &userAccessToken) + + if _, err := s.Store.UpsertUserSetting(ctx, &storepb.UserSetting{ + UserId: user.ID, + Key: storepb.UserSetting_ACCESS_TOKENS, + Value: &storepb.UserSetting_AccessTokens{ + AccessTokens: &storepb.AccessTokensUserSetting{ + AccessTokens: userAccessTokens, + }, + }, + }); err != nil { + return errors.Wrap(err, "failed to upsert user setting") + } + return nil +} + +func convertUserFromStore(user *store.User) *v1pb.User { + userpb := &v1pb.User{ + Name: fmt.Sprintf("%s%d", UserNamePrefix, user.ID), + State: convertStateFromStore(user.RowStatus), + CreateTime: timestamppb.New(time.Unix(user.CreatedTs, 0)), + UpdateTime: timestamppb.New(time.Unix(user.UpdatedTs, 0)), + Role: convertUserRoleFromStore(user.Role), + Username: user.Username, + Email: user.Email, + DisplayName: user.Nickname, + AvatarUrl: user.AvatarURL, + Description: user.Description, + } + // Use the avatar URL instead of raw base64 image data to reduce the response size. + if user.AvatarURL != "" { + // Check if avatar url is base64 format. + _, _, err := extractImageInfo(user.AvatarURL) + if err == nil { + userpb.AvatarUrl = fmt.Sprintf("/api/v1/%s/avatar", userpb.Name) + } else { + userpb.AvatarUrl = user.AvatarURL + } + } + return userpb +} + +func convertUserRoleFromStore(role store.Role) v1pb.User_Role { + switch role { + case store.RoleHost: + return v1pb.User_HOST + case store.RoleAdmin: + return v1pb.User_ADMIN + case store.RoleUser: + return v1pb.User_USER + default: + return v1pb.User_ROLE_UNSPECIFIED + } +} + +func convertUserRoleToStore(role v1pb.User_Role) store.Role { + switch role { + case v1pb.User_HOST: + return store.RoleHost + case v1pb.User_ADMIN: + return store.RoleAdmin + case v1pb.User_USER: + return store.RoleUser + default: + return store.RoleUser + } +} + +func extractImageInfo(dataURI string) (string, string, error) { + dataURIRegex := regexp.MustCompile(`^data:(?P.+);base64,(?P.+)`) + matches := dataURIRegex.FindStringSubmatch(dataURI) + if len(matches) != 3 { + return "", "", errors.New("Invalid data URI format") + } + imageType := matches[1] + base64Data := matches[2] + return imageType, base64Data, nil +} diff --git a/server/router/api/v1/user_service_stats.go b/server/router/api/v1/user_service_stats.go new file mode 100644 index 0000000..552e61e --- /dev/null +++ b/server/router/api/v1/user_service_stats.go @@ -0,0 +1,168 @@ +package v1 + +import ( + "context" + "fmt" + "time" + + "github.com/pkg/errors" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" + + v1pb "github.com/usememos/memos/proto/gen/api/v1" + "github.com/usememos/memos/store" +) + +func (s *APIV1Service) ListAllUserStats(ctx context.Context, _ *v1pb.ListAllUserStatsRequest) (*v1pb.ListAllUserStatsResponse, error) { + workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to get workspace memo related setting") + } + + normalStatus := store.Normal + memoFind := &store.FindMemo{ + // Exclude comments by default. + ExcludeComments: true, + ExcludeContent: true, + RowStatus: &normalStatus, + } + + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) + } + if currentUser == nil { + memoFind.VisibilityList = []store.Visibility{store.Public} + } else { + if memoFind.CreatorID == nil { + internalFilter := fmt.Sprintf(`creator_id == %d || visibility in ["PUBLIC", "PROTECTED"]`, currentUser.ID) + if memoFind.Filter != nil { + filter := fmt.Sprintf("(%s) && (%s)", *memoFind.Filter, internalFilter) + memoFind.Filter = &filter + } else { + memoFind.Filter = &internalFilter + } + } else if *memoFind.CreatorID != currentUser.ID { + memoFind.VisibilityList = []store.Visibility{store.Public, store.Protected} + } + } + memos, err := s.Store.ListMemos(ctx, memoFind) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list memos: %v", err) + } + + userMemoStatMap := make(map[int32]*v1pb.UserStats) + for _, memo := range memos { + displayTs := memo.CreatedTs + if workspaceMemoRelatedSetting.DisplayWithUpdateTime { + displayTs = memo.UpdatedTs + } + userMemoStatMap[memo.CreatorID] = &v1pb.UserStats{ + Name: fmt.Sprintf("users/%d/stats", memo.CreatorID), + } + userMemoStatMap[memo.CreatorID].MemoDisplayTimestamps = append(userMemoStatMap[memo.CreatorID].MemoDisplayTimestamps, timestamppb.New(time.Unix(displayTs, 0))) + } + + userMemoStats := []*v1pb.UserStats{} + for _, userMemoStat := range userMemoStatMap { + userMemoStats = append(userMemoStats, userMemoStat) + } + + response := &v1pb.ListAllUserStatsResponse{ + UserStats: userMemoStats, + } + return response, nil +} + +func (s *APIV1Service) GetUserStats(ctx context.Context, request *v1pb.GetUserStatsRequest) (*v1pb.UserStats, error) { + userID, err := ExtractUserIDFromName(request.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid user name: %v", err) + } + + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) + } + + normalStatus := store.Normal + memoFind := &store.FindMemo{ + CreatorID: &userID, + // Exclude comments by default. + ExcludeComments: true, + ExcludeContent: true, + RowStatus: &normalStatus, + } + + if currentUser == nil { + memoFind.VisibilityList = []store.Visibility{store.Public} + } else if currentUser.ID != userID { + memoFind.VisibilityList = []store.Visibility{store.Public, store.Protected} + } + + memos, err := s.Store.ListMemos(ctx, memoFind) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list memos: %v", err) + } + + workspaceMemoRelatedSetting, err := s.Store.GetWorkspaceMemoRelatedSetting(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to get workspace memo related setting") + } + + displayTimestamps := []*timestamppb.Timestamp{} + tagCount := make(map[string]int32) + linkCount := int32(0) + codeCount := int32(0) + todoCount := int32(0) + undoCount := int32(0) + pinnedMemos := []string{} + + for _, memo := range memos { + displayTs := memo.CreatedTs + if workspaceMemoRelatedSetting.DisplayWithUpdateTime { + displayTs = memo.UpdatedTs + } + displayTimestamps = append(displayTimestamps, timestamppb.New(time.Unix(displayTs, 0))) + // Count different memo types based on content. + if memo.Payload != nil { + for _, tag := range memo.Payload.Tags { + tagCount[tag]++ + } + if memo.Payload.Property != nil { + if memo.Payload.Property.HasLink { + linkCount++ + } + if memo.Payload.Property.HasCode { + codeCount++ + } + if memo.Payload.Property.HasTaskList { + todoCount++ + } + if memo.Payload.Property.HasIncompleteTasks { + undoCount++ + } + } + } + if memo.Pinned { + pinnedMemos = append(pinnedMemos, fmt.Sprintf("users/%d/memos/%d", userID, memo.ID)) + } + } + + userStats := &v1pb.UserStats{ + Name: fmt.Sprintf("users/%d/stats", userID), + MemoDisplayTimestamps: displayTimestamps, + TagCount: tagCount, + PinnedMemos: pinnedMemos, + TotalMemoCount: int32(len(memos)), + MemoTypeStats: &v1pb.UserStats_MemoTypeStats{ + LinkCount: linkCount, + CodeCount: codeCount, + TodoCount: todoCount, + UndoCount: undoCount, + }, + } + + return userStats, nil +} diff --git a/server/router/api/v1/v1.go b/server/router/api/v1/v1.go new file mode 100644 index 0000000..2cdfe49 --- /dev/null +++ b/server/router/api/v1/v1.go @@ -0,0 +1,137 @@ +package v1 + +import ( + "context" + "fmt" + "math" + + "github.com/grpc-ecosystem/grpc-gateway/v2/runtime" + "github.com/improbable-eng/grpc-web/go/grpcweb" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/reflection" + + "github.com/usememos/memos/internal/profile" + v1pb "github.com/usememos/memos/proto/gen/api/v1" + "github.com/usememos/memos/store" +) + +type APIV1Service struct { + grpc_health_v1.UnimplementedHealthServer + + v1pb.UnimplementedWorkspaceServiceServer + v1pb.UnimplementedAuthServiceServer + v1pb.UnimplementedUserServiceServer + v1pb.UnimplementedMemoServiceServer + v1pb.UnimplementedAttachmentServiceServer + v1pb.UnimplementedShortcutServiceServer + v1pb.UnimplementedInboxServiceServer + v1pb.UnimplementedActivityServiceServer + v1pb.UnimplementedWebhookServiceServer + v1pb.UnimplementedMarkdownServiceServer + v1pb.UnimplementedIdentityProviderServiceServer + + Secret string + Profile *profile.Profile + Store *store.Store + + grpcServer *grpc.Server +} + +func NewAPIV1Service(secret string, profile *profile.Profile, store *store.Store, grpcServer *grpc.Server) *APIV1Service { + grpc.EnableTracing = true + apiv1Service := &APIV1Service{ + Secret: secret, + Profile: profile, + Store: store, + grpcServer: grpcServer, + } + grpc_health_v1.RegisterHealthServer(grpcServer, apiv1Service) + v1pb.RegisterWorkspaceServiceServer(grpcServer, apiv1Service) + v1pb.RegisterAuthServiceServer(grpcServer, apiv1Service) + v1pb.RegisterUserServiceServer(grpcServer, apiv1Service) + v1pb.RegisterMemoServiceServer(grpcServer, apiv1Service) + v1pb.RegisterAttachmentServiceServer(grpcServer, apiv1Service) + v1pb.RegisterShortcutServiceServer(grpcServer, apiv1Service) + v1pb.RegisterInboxServiceServer(grpcServer, apiv1Service) + v1pb.RegisterActivityServiceServer(grpcServer, apiv1Service) + v1pb.RegisterWebhookServiceServer(grpcServer, apiv1Service) + v1pb.RegisterMarkdownServiceServer(grpcServer, apiv1Service) + v1pb.RegisterIdentityProviderServiceServer(grpcServer, apiv1Service) + reflection.Register(grpcServer) + return apiv1Service +} + +// RegisterGateway registers the gRPC-Gateway with the given Echo instance. +func (s *APIV1Service) RegisterGateway(ctx context.Context, echoServer *echo.Echo) error { + var target string + if len(s.Profile.UNIXSock) == 0 { + target = fmt.Sprintf("%s:%d", s.Profile.Addr, s.Profile.Port) + } else { + target = fmt.Sprintf("unix:%s", s.Profile.UNIXSock) + } + conn, err := grpc.NewClient( + target, + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(math.MaxInt32)), + ) + if err != nil { + return err + } + + gwMux := runtime.NewServeMux() + if err := v1pb.RegisterWorkspaceServiceHandler(ctx, gwMux, conn); err != nil { + return err + } + if err := v1pb.RegisterAuthServiceHandler(ctx, gwMux, conn); err != nil { + return err + } + if err := v1pb.RegisterUserServiceHandler(ctx, gwMux, conn); err != nil { + return err + } + if err := v1pb.RegisterMemoServiceHandler(ctx, gwMux, conn); err != nil { + return err + } + if err := v1pb.RegisterAttachmentServiceHandler(ctx, gwMux, conn); err != nil { + return err + } + if err := v1pb.RegisterShortcutServiceHandler(ctx, gwMux, conn); err != nil { + return err + } + if err := v1pb.RegisterInboxServiceHandler(ctx, gwMux, conn); err != nil { + return err + } + if err := v1pb.RegisterActivityServiceHandler(ctx, gwMux, conn); err != nil { + return err + } + if err := v1pb.RegisterWebhookServiceHandler(ctx, gwMux, conn); err != nil { + return err + } + if err := v1pb.RegisterMarkdownServiceHandler(ctx, gwMux, conn); err != nil { + return err + } + if err := v1pb.RegisterIdentityProviderServiceHandler(ctx, gwMux, conn); err != nil { + return err + } + gwGroup := echoServer.Group("") + gwGroup.Use(middleware.CORS()) + handler := echo.WrapHandler(gwMux) + + gwGroup.Any("/api/v1/*", handler) + gwGroup.Any("/file/*", handler) + + // GRPC web proxy. + options := []grpcweb.Option{ + grpcweb.WithCorsForRegisteredEndpointsOnly(false), + grpcweb.WithOriginFunc(func(_ string) bool { + return true + }), + } + wrappedGrpc := grpcweb.WrapServer(s.grpcServer, options...) + echoServer.Any("/memos.api.v1.*", echo.WrapHandler(wrappedGrpc)) + + return nil +} diff --git a/server/router/api/v1/webhook_service.go b/server/router/api/v1/webhook_service.go new file mode 100644 index 0000000..9b6768f --- /dev/null +++ b/server/router/api/v1/webhook_service.go @@ -0,0 +1,317 @@ +package v1 + +import ( + "context" + "crypto/rand" + "encoding/hex" + "fmt" + "strings" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/emptypb" + + "github.com/usememos/memos/internal/util" + v1pb "github.com/usememos/memos/proto/gen/api/v1" + storepb "github.com/usememos/memos/proto/gen/store" +) + +func (s *APIV1Service) CreateWebhook(ctx context.Context, request *v1pb.CreateWebhookRequest) (*v1pb.Webhook, error) { + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) + } + if currentUser == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } + + // Extract user ID from parent (format: users/{user}) + parentUserID, err := ExtractUserIDFromName(request.Parent) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid parent: %v", err) + } + + // Users can only create webhooks for themselves + if parentUserID != currentUser.ID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + // Only host users can create webhooks + if !isSuperUser(currentUser) { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + // Validate required fields + if request.Webhook == nil { + return nil, status.Errorf(codes.InvalidArgument, "webhook is required") + } + if strings.TrimSpace(request.Webhook.Url) == "" { + return nil, status.Errorf(codes.InvalidArgument, "webhook URL is required") + } + + // Handle validate_only field + if request.ValidateOnly { + // Perform validation checks without actually creating the webhook + return &v1pb.Webhook{ + Name: fmt.Sprintf("users/%d/webhooks/validate", currentUser.ID), + DisplayName: request.Webhook.DisplayName, + Url: request.Webhook.Url, + }, nil + } + + err = s.Store.AddUserWebhook(ctx, currentUser.ID, &storepb.WebhooksUserSetting_Webhook{ + Id: generateWebhookID(), + Title: request.Webhook.DisplayName, + Url: strings.TrimSpace(request.Webhook.Url), + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to create webhook, error: %+v", err) + } + + // Return the newly created webhook + webhooks, err := s.Store.GetUserWebhooks(ctx, currentUser.ID) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user webhooks, error: %+v", err) + } + + // Find the webhook we just created + for _, webhook := range webhooks { + if webhook.Title == request.Webhook.DisplayName && webhook.Url == strings.TrimSpace(request.Webhook.Url) { + return convertWebhookFromUserSetting(webhook, currentUser.ID), nil + } + } + + return nil, status.Errorf(codes.Internal, "failed to find created webhook") +} + +func (s *APIV1Service) ListWebhooks(ctx context.Context, request *v1pb.ListWebhooksRequest) (*v1pb.ListWebhooksResponse, error) { + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) + } + if currentUser == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } + + // Extract user ID from parent (format: users/{user}) + parentUserID, err := ExtractUserIDFromName(request.Parent) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid parent: %v", err) + } + + // Users can only list their own webhooks + if parentUserID != currentUser.ID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + webhooks, err := s.Store.GetUserWebhooks(ctx, currentUser.ID) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to list webhooks, error: %+v", err) + } + + response := &v1pb.ListWebhooksResponse{ + Webhooks: []*v1pb.Webhook{}, + } + for _, webhook := range webhooks { + response.Webhooks = append(response.Webhooks, convertWebhookFromUserSetting(webhook, currentUser.ID)) + } + return response, nil +} + +func (s *APIV1Service) GetWebhook(ctx context.Context, request *v1pb.GetWebhookRequest) (*v1pb.Webhook, error) { + // Extract user ID and webhook ID from name (format: users/{user}/webhooks/{webhook}) + tokens, err := GetNameParentTokens(request.Name, UserNamePrefix, WebhookNamePrefix) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name: %v", err) + } + if len(tokens) != 2 { + return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name format") + } + + userIDStr := tokens[0] + webhookID := tokens[1] + + requestedUserID, err := util.ConvertStringToInt32(userIDStr) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid user ID in webhook name: %v", err) + } + + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) + } + if currentUser == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } + + // Users can only access their own webhooks + if requestedUserID != currentUser.ID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + webhooks, err := s.Store.GetUserWebhooks(ctx, currentUser.ID) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get webhooks, error: %+v", err) + } + + // Find webhook by ID + for _, webhook := range webhooks { + if webhook.Id == webhookID { + return convertWebhookFromUserSetting(webhook, currentUser.ID), nil + } + } + return nil, status.Errorf(codes.NotFound, "webhook not found") +} + +func (s *APIV1Service) UpdateWebhook(ctx context.Context, request *v1pb.UpdateWebhookRequest) (*v1pb.Webhook, error) { + if request.UpdateMask == nil || len(request.UpdateMask.Paths) == 0 { + return nil, status.Errorf(codes.InvalidArgument, "update_mask is required") + } + + // Extract user ID and webhook ID from name (format: users/{user}/webhooks/{webhook}) + tokens, err := GetNameParentTokens(request.Webhook.Name, UserNamePrefix, WebhookNamePrefix) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name: %v", err) + } + if len(tokens) != 2 { + return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name format") + } + + userIDStr := tokens[0] + webhookID := tokens[1] + + requestedUserID, err := util.ConvertStringToInt32(userIDStr) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid user ID in webhook name: %v", err) + } + + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) + } + if currentUser == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } + + // Users can only update their own webhooks + if requestedUserID != currentUser.ID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + // Get existing webhooks from user settings + webhooks, err := s.Store.GetUserWebhooks(ctx, currentUser.ID) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get webhooks: %v", err) + } + + // Find the webhook to update + var existingWebhook *storepb.WebhooksUserSetting_Webhook + for _, webhook := range webhooks { + if webhook.Id == webhookID { + existingWebhook = webhook + break + } + } + + if existingWebhook == nil { + return nil, status.Errorf(codes.NotFound, "webhook not found") + } + + // Create updated webhook + updatedWebhook := &storepb.WebhooksUserSetting_Webhook{ + Id: existingWebhook.Id, + Title: existingWebhook.Title, + Url: existingWebhook.Url, + } + + // Apply updates based on update mask + for _, field := range request.UpdateMask.Paths { + switch field { + case "display_name": + updatedWebhook.Title = request.Webhook.DisplayName + case "url": + updatedWebhook.Url = request.Webhook.Url + default: + return nil, status.Errorf(codes.InvalidArgument, "invalid update path: %s", field) + } + } + + // Update the webhook in user settings + err = s.Store.UpdateUserWebhook(ctx, currentUser.ID, updatedWebhook) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to update webhook: %v", err) + } + + return convertWebhookFromUserSetting(updatedWebhook, currentUser.ID), nil +} + +func (s *APIV1Service) DeleteWebhook(ctx context.Context, request *v1pb.DeleteWebhookRequest) (*emptypb.Empty, error) { + // Extract user ID and webhook ID from name (format: users/{user}/webhooks/{webhook}) + tokens, err := GetNameParentTokens(request.Name, UserNamePrefix, WebhookNamePrefix) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name: %v", err) + } + if len(tokens) != 2 { + return nil, status.Errorf(codes.InvalidArgument, "invalid webhook name format") + } + + userIDStr := tokens[0] + webhookID := tokens[1] + + requestedUserID, err := util.ConvertStringToInt32(userIDStr) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid user ID in webhook name: %v", err) + } + + currentUser, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get user: %v", err) + } + if currentUser == nil { + return nil, status.Errorf(codes.Unauthenticated, "user not authenticated") + } + + // Users can only delete their own webhooks + if requestedUserID != currentUser.ID { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + // Get existing webhooks from user settings to verify it exists + webhooks, err := s.Store.GetUserWebhooks(ctx, currentUser.ID) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get webhooks: %v", err) + } + + // Check if webhook exists + webhookExists := false + for _, webhook := range webhooks { + if webhook.Id == webhookID { + webhookExists = true + break + } + } + + if !webhookExists { + return nil, status.Errorf(codes.NotFound, "webhook not found") + } + + err = s.Store.RemoveUserWebhook(ctx, currentUser.ID, webhookID) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to delete webhook: %v", err) + } + return &emptypb.Empty{}, nil +} + +func convertWebhookFromUserSetting(webhook *storepb.WebhooksUserSetting_Webhook, userID int32) *v1pb.Webhook { + return &v1pb.Webhook{ + Name: fmt.Sprintf("users/%d/webhooks/%s", userID, webhook.Id), + DisplayName: webhook.Title, + Url: webhook.Url, + } +} + +func generateWebhookID() string { + b := make([]byte, 8) + rand.Read(b) + return hex.EncodeToString(b) +} diff --git a/server/router/api/v1/workspace_service.go b/server/router/api/v1/workspace_service.go new file mode 100644 index 0000000..aed7f08 --- /dev/null +++ b/server/router/api/v1/workspace_service.go @@ -0,0 +1,306 @@ +package v1 + +import ( + "context" + "fmt" + + "github.com/pkg/errors" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + v1pb "github.com/usememos/memos/proto/gen/api/v1" + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +// GetWorkspaceProfile returns the workspace profile. +func (s *APIV1Service) GetWorkspaceProfile(ctx context.Context, _ *v1pb.GetWorkspaceProfileRequest) (*v1pb.WorkspaceProfile, error) { + workspaceProfile := &v1pb.WorkspaceProfile{ + Version: s.Profile.Version, + Mode: s.Profile.Mode, + InstanceUrl: s.Profile.InstanceURL, + } + owner, err := s.GetInstanceOwner(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get instance owner: %v", err) + } + if owner != nil { + workspaceProfile.Owner = owner.Name + } + return workspaceProfile, nil +} + +func (s *APIV1Service) GetWorkspaceSetting(ctx context.Context, request *v1pb.GetWorkspaceSettingRequest) (*v1pb.WorkspaceSetting, error) { + workspaceSettingKeyString, err := ExtractWorkspaceSettingKeyFromName(request.Name) + if err != nil { + return nil, status.Errorf(codes.InvalidArgument, "invalid workspace setting name: %v", err) + } + + workspaceSettingKey := storepb.WorkspaceSettingKey(storepb.WorkspaceSettingKey_value[workspaceSettingKeyString]) + // Get workspace setting from store with default value. + switch workspaceSettingKey { + case storepb.WorkspaceSettingKey_BASIC: + _, err = s.Store.GetWorkspaceBasicSetting(ctx) + case storepb.WorkspaceSettingKey_GENERAL: + _, err = s.Store.GetWorkspaceGeneralSetting(ctx) + case storepb.WorkspaceSettingKey_MEMO_RELATED: + _, err = s.Store.GetWorkspaceMemoRelatedSetting(ctx) + case storepb.WorkspaceSettingKey_STORAGE: + _, err = s.Store.GetWorkspaceStorageSetting(ctx) + default: + return nil, status.Errorf(codes.InvalidArgument, "unsupported workspace setting key: %v", workspaceSettingKey) + } + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get workspace setting: %v", err) + } + + workspaceSetting, err := s.Store.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{ + Name: workspaceSettingKey.String(), + }) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get workspace setting: %v", err) + } + if workspaceSetting == nil { + return nil, status.Errorf(codes.NotFound, "workspace setting not found") + } + + // For storage setting, only host can get it. + if workspaceSetting.Key == storepb.WorkspaceSettingKey_STORAGE { + user, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) + } + if user == nil || user.Role != store.RoleHost { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + } + + return convertWorkspaceSettingFromStore(workspaceSetting), nil +} + +func (s *APIV1Service) UpdateWorkspaceSetting(ctx context.Context, request *v1pb.UpdateWorkspaceSettingRequest) (*v1pb.WorkspaceSetting, error) { + user, err := s.GetCurrentUser(ctx) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to get current user: %v", err) + } + if user.Role != store.RoleHost { + return nil, status.Errorf(codes.PermissionDenied, "permission denied") + } + + // TODO: Apply update_mask if specified + _ = request.UpdateMask + + updateSetting := convertWorkspaceSettingToStore(request.Setting) + workspaceSetting, err := s.Store.UpsertWorkspaceSetting(ctx, updateSetting) + if err != nil { + return nil, status.Errorf(codes.Internal, "failed to upsert workspace setting: %v", err) + } + + return convertWorkspaceSettingFromStore(workspaceSetting), nil +} + +func convertWorkspaceSettingFromStore(setting *storepb.WorkspaceSetting) *v1pb.WorkspaceSetting { + workspaceSetting := &v1pb.WorkspaceSetting{ + Name: fmt.Sprintf("workspace/settings/%s", setting.Key.String()), + } + switch setting.Value.(type) { + case *storepb.WorkspaceSetting_GeneralSetting: + workspaceSetting.Value = &v1pb.WorkspaceSetting_GeneralSetting{ + GeneralSetting: convertWorkspaceGeneralSettingFromStore(setting.GetGeneralSetting()), + } + case *storepb.WorkspaceSetting_StorageSetting: + workspaceSetting.Value = &v1pb.WorkspaceSetting_StorageSetting{ + StorageSetting: convertWorkspaceStorageSettingFromStore(setting.GetStorageSetting()), + } + case *storepb.WorkspaceSetting_MemoRelatedSetting: + workspaceSetting.Value = &v1pb.WorkspaceSetting_MemoRelatedSetting{ + MemoRelatedSetting: convertWorkspaceMemoRelatedSettingFromStore(setting.GetMemoRelatedSetting()), + } + } + return workspaceSetting +} + +func convertWorkspaceSettingToStore(setting *v1pb.WorkspaceSetting) *storepb.WorkspaceSetting { + settingKeyString, _ := ExtractWorkspaceSettingKeyFromName(setting.Name) + workspaceSetting := &storepb.WorkspaceSetting{ + Key: storepb.WorkspaceSettingKey(storepb.WorkspaceSettingKey_value[settingKeyString]), + Value: &storepb.WorkspaceSetting_GeneralSetting{ + GeneralSetting: convertWorkspaceGeneralSettingToStore(setting.GetGeneralSetting()), + }, + } + switch workspaceSetting.Key { + case storepb.WorkspaceSettingKey_GENERAL: + workspaceSetting.Value = &storepb.WorkspaceSetting_GeneralSetting{ + GeneralSetting: convertWorkspaceGeneralSettingToStore(setting.GetGeneralSetting()), + } + case storepb.WorkspaceSettingKey_STORAGE: + workspaceSetting.Value = &storepb.WorkspaceSetting_StorageSetting{ + StorageSetting: convertWorkspaceStorageSettingToStore(setting.GetStorageSetting()), + } + case storepb.WorkspaceSettingKey_MEMO_RELATED: + workspaceSetting.Value = &storepb.WorkspaceSetting_MemoRelatedSetting{ + MemoRelatedSetting: convertWorkspaceMemoRelatedSettingToStore(setting.GetMemoRelatedSetting()), + } + } + return workspaceSetting +} + +func convertWorkspaceGeneralSettingFromStore(setting *storepb.WorkspaceGeneralSetting) *v1pb.WorkspaceGeneralSetting { + if setting == nil { + return nil + } + // Backfill theme if empty + theme := setting.Theme + if theme == "" { + theme = "default" + } + + generalSetting := &v1pb.WorkspaceGeneralSetting{ + Theme: theme, + DisallowUserRegistration: setting.DisallowUserRegistration, + DisallowPasswordAuth: setting.DisallowPasswordAuth, + AdditionalScript: setting.AdditionalScript, + AdditionalStyle: setting.AdditionalStyle, + WeekStartDayOffset: setting.WeekStartDayOffset, + DisallowChangeUsername: setting.DisallowChangeUsername, + DisallowChangeNickname: setting.DisallowChangeNickname, + } + if setting.CustomProfile != nil { + generalSetting.CustomProfile = &v1pb.WorkspaceCustomProfile{ + Title: setting.CustomProfile.Title, + Description: setting.CustomProfile.Description, + LogoUrl: setting.CustomProfile.LogoUrl, + Locale: setting.CustomProfile.Locale, + Appearance: setting.CustomProfile.Appearance, + } + } + return generalSetting +} + +func convertWorkspaceGeneralSettingToStore(setting *v1pb.WorkspaceGeneralSetting) *storepb.WorkspaceGeneralSetting { + if setting == nil { + return nil + } + generalSetting := &storepb.WorkspaceGeneralSetting{ + Theme: setting.Theme, + DisallowUserRegistration: setting.DisallowUserRegistration, + DisallowPasswordAuth: setting.DisallowPasswordAuth, + AdditionalScript: setting.AdditionalScript, + AdditionalStyle: setting.AdditionalStyle, + WeekStartDayOffset: setting.WeekStartDayOffset, + DisallowChangeUsername: setting.DisallowChangeUsername, + DisallowChangeNickname: setting.DisallowChangeNickname, + } + if setting.CustomProfile != nil { + generalSetting.CustomProfile = &storepb.WorkspaceCustomProfile{ + Title: setting.CustomProfile.Title, + Description: setting.CustomProfile.Description, + LogoUrl: setting.CustomProfile.LogoUrl, + Locale: setting.CustomProfile.Locale, + Appearance: setting.CustomProfile.Appearance, + } + } + return generalSetting +} + +func convertWorkspaceStorageSettingFromStore(settingpb *storepb.WorkspaceStorageSetting) *v1pb.WorkspaceStorageSetting { + if settingpb == nil { + return nil + } + setting := &v1pb.WorkspaceStorageSetting{ + StorageType: v1pb.WorkspaceStorageSetting_StorageType(settingpb.StorageType), + FilepathTemplate: settingpb.FilepathTemplate, + UploadSizeLimitMb: settingpb.UploadSizeLimitMb, + } + if settingpb.S3Config != nil { + setting.S3Config = &v1pb.WorkspaceStorageSetting_S3Config{ + AccessKeyId: settingpb.S3Config.AccessKeyId, + AccessKeySecret: settingpb.S3Config.AccessKeySecret, + Endpoint: settingpb.S3Config.Endpoint, + Region: settingpb.S3Config.Region, + Bucket: settingpb.S3Config.Bucket, + UsePathStyle: settingpb.S3Config.UsePathStyle, + } + } + return setting +} + +func convertWorkspaceStorageSettingToStore(setting *v1pb.WorkspaceStorageSetting) *storepb.WorkspaceStorageSetting { + if setting == nil { + return nil + } + settingpb := &storepb.WorkspaceStorageSetting{ + StorageType: storepb.WorkspaceStorageSetting_StorageType(setting.StorageType), + FilepathTemplate: setting.FilepathTemplate, + UploadSizeLimitMb: setting.UploadSizeLimitMb, + } + if setting.S3Config != nil { + settingpb.S3Config = &storepb.StorageS3Config{ + AccessKeyId: setting.S3Config.AccessKeyId, + AccessKeySecret: setting.S3Config.AccessKeySecret, + Endpoint: setting.S3Config.Endpoint, + Region: setting.S3Config.Region, + Bucket: setting.S3Config.Bucket, + UsePathStyle: setting.S3Config.UsePathStyle, + } + } + return settingpb +} + +func convertWorkspaceMemoRelatedSettingFromStore(setting *storepb.WorkspaceMemoRelatedSetting) *v1pb.WorkspaceMemoRelatedSetting { + if setting == nil { + return nil + } + return &v1pb.WorkspaceMemoRelatedSetting{ + DisallowPublicVisibility: setting.DisallowPublicVisibility, + DisplayWithUpdateTime: setting.DisplayWithUpdateTime, + ContentLengthLimit: setting.ContentLengthLimit, + EnableDoubleClickEdit: setting.EnableDoubleClickEdit, + EnableLinkPreview: setting.EnableLinkPreview, + EnableComment: setting.EnableComment, + Reactions: setting.Reactions, + DisableMarkdownShortcuts: setting.DisableMarkdownShortcuts, + EnableBlurNsfwContent: setting.EnableBlurNsfwContent, + NsfwTags: setting.NsfwTags, + } +} + +func convertWorkspaceMemoRelatedSettingToStore(setting *v1pb.WorkspaceMemoRelatedSetting) *storepb.WorkspaceMemoRelatedSetting { + if setting == nil { + return nil + } + return &storepb.WorkspaceMemoRelatedSetting{ + DisallowPublicVisibility: setting.DisallowPublicVisibility, + DisplayWithUpdateTime: setting.DisplayWithUpdateTime, + ContentLengthLimit: setting.ContentLengthLimit, + EnableDoubleClickEdit: setting.EnableDoubleClickEdit, + EnableLinkPreview: setting.EnableLinkPreview, + EnableComment: setting.EnableComment, + Reactions: setting.Reactions, + DisableMarkdownShortcuts: setting.DisableMarkdownShortcuts, + EnableBlurNsfwContent: setting.EnableBlurNsfwContent, + NsfwTags: setting.NsfwTags, + } +} + +var ownerCache *v1pb.User + +func (s *APIV1Service) GetInstanceOwner(ctx context.Context) (*v1pb.User, error) { + if ownerCache != nil { + return ownerCache, nil + } + + hostUserType := store.RoleHost + user, err := s.Store.GetUser(ctx, &store.FindUser{ + Role: &hostUserType, + }) + if err != nil { + return nil, errors.Wrapf(err, "failed to find owner") + } + if user == nil { + return nil, nil + } + + ownerCache = convertUserFromStore(user) + return ownerCache, nil +} diff --git a/server/router/frontend/frontend.go b/server/router/frontend/frontend.go new file mode 100644 index 0000000..f3eccd9 --- /dev/null +++ b/server/router/frontend/frontend.go @@ -0,0 +1,61 @@ +package frontend + +import ( + "context" + "embed" + "io/fs" + "net/http" + + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + + "github.com/usememos/memos/internal/profile" + "github.com/usememos/memos/internal/util" + "github.com/usememos/memos/store" +) + +//go:embed dist/* +var embeddedFiles embed.FS + +type FrontendService struct { + Profile *profile.Profile + Store *store.Store +} + +func NewFrontendService(profile *profile.Profile, store *store.Store) *FrontendService { + return &FrontendService{ + Profile: profile, + Store: store, + } +} + +func (*FrontendService) Serve(_ context.Context, e *echo.Echo) { + skipper := func(c echo.Context) bool { + // Skip API routes. + if util.HasPrefixes(c.Path(), "/api", "/memos.api.v1") { + return true + } + // Skip setting cache headers for index.html + if c.Path() == "/" || c.Path() == "/index.html" { + return false + } + // Set Cache-Control header to allow public caching with a max-age of 7 days. + c.Response().Header().Set(echo.HeaderCacheControl, "public, max-age=604800") // 7 days + return false + } + + // Route to serve the main app with HTML5 fallback for SPA behavior. + e.Use(middleware.StaticWithConfig(middleware.StaticConfig{ + Filesystem: getFileSystem("dist"), + HTML5: true, // Enable fallback to index.html + Skipper: skipper, + })) +} + +func getFileSystem(path string) http.FileSystem { + fs, err := fs.Sub(embeddedFiles, path) + if err != nil { + panic(err) + } + return http.FS(fs) +} diff --git a/server/router/rss/rss.go b/server/router/rss/rss.go new file mode 100644 index 0000000..42b42aa --- /dev/null +++ b/server/router/rss/rss.go @@ -0,0 +1,179 @@ +package rss + +import ( + "context" + "fmt" + "net/http" + "strconv" + "time" + + "github.com/gorilla/feeds" + "github.com/labstack/echo/v4" + "github.com/usememos/gomark" + "github.com/usememos/gomark/renderer" + + "github.com/usememos/memos/internal/profile" + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +const ( + maxRSSItemCount = 100 +) + +type RSSService struct { + Profile *profile.Profile + Store *store.Store +} + +type RSSHeading struct { + Title string + Description string +} + +func NewRSSService(profile *profile.Profile, store *store.Store) *RSSService { + return &RSSService{ + Profile: profile, + Store: store, + } +} + +func (s *RSSService) RegisterRoutes(g *echo.Group) { + g.GET("/explore/rss.xml", s.GetExploreRSS) + g.GET("/u/:username/rss.xml", s.GetUserRSS) +} + +func (s *RSSService) GetExploreRSS(c echo.Context) error { + ctx := c.Request().Context() + normalStatus := store.Normal + memoFind := store.FindMemo{ + RowStatus: &normalStatus, + VisibilityList: []store.Visibility{store.Public}, + } + memoList, err := s.Store.ListMemos(ctx, &memoFind) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err) + } + + baseURL := c.Scheme() + "://" + c.Request().Host + rss, err := s.generateRSSFromMemoList(ctx, memoList, baseURL) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate rss").SetInternal(err) + } + c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationXMLCharsetUTF8) + return c.String(http.StatusOK, rss) +} + +func (s *RSSService) GetUserRSS(c echo.Context) error { + ctx := c.Request().Context() + username := c.Param("username") + user, err := s.Store.GetUser(ctx, &store.FindUser{ + Username: &username, + }) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find user").SetInternal(err) + } + if user == nil { + return echo.NewHTTPError(http.StatusNotFound, "User not found") + } + + normalStatus := store.Normal + memoFind := store.FindMemo{ + CreatorID: &user.ID, + RowStatus: &normalStatus, + VisibilityList: []store.Visibility{store.Public}, + } + memoList, err := s.Store.ListMemos(ctx, &memoFind) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find memo list").SetInternal(err) + } + + baseURL := c.Scheme() + "://" + c.Request().Host + rss, err := s.generateRSSFromMemoList(ctx, memoList, baseURL) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to generate rss").SetInternal(err) + } + c.Response().Header().Set(echo.HeaderContentType, echo.MIMEApplicationXMLCharsetUTF8) + return c.String(http.StatusOK, rss) +} + +func (s *RSSService) generateRSSFromMemoList(ctx context.Context, memoList []*store.Memo, baseURL string) (string, error) { + rssHeading, err := getRSSHeading(ctx, s.Store) + if err != nil { + return "", err + } + feed := &feeds.Feed{ + Title: rssHeading.Title, + Link: &feeds.Link{Href: baseURL}, + Description: rssHeading.Description, + Created: time.Now(), + } + + var itemCountLimit = min(len(memoList), maxRSSItemCount) + feed.Items = make([]*feeds.Item, itemCountLimit) + for i := 0; i < itemCountLimit; i++ { + memo := memoList[i] + description, err := getRSSItemDescription(memo.Content) + if err != nil { + return "", err + } + link := &feeds.Link{Href: baseURL + "/memos/" + memo.UID} + feed.Items[i] = &feeds.Item{ + Link: link, + Description: description, + Created: time.Unix(memo.CreatedTs, 0), + Id: link.Href, + } + attachments, err := s.Store.ListAttachments(ctx, &store.FindAttachment{ + MemoID: &memo.ID, + }) + if err != nil { + return "", err + } + if len(attachments) > 0 { + attachment := attachments[0] + enclosure := feeds.Enclosure{} + if attachment.StorageType == storepb.AttachmentStorageType_EXTERNAL || attachment.StorageType == storepb.AttachmentStorageType_S3 { + enclosure.Url = attachment.Reference + } else { + enclosure.Url = fmt.Sprintf("%s/file/attachments/%s/%s", baseURL, attachment.UID, attachment.Filename) + } + enclosure.Length = strconv.Itoa(int(attachment.Size)) + enclosure.Type = attachment.Type + feed.Items[i].Enclosure = &enclosure + } + } + + rss, err := feed.ToRss() + if err != nil { + return "", err + } + return rss, nil +} + +func getRSSItemDescription(content string) (string, error) { + nodes, err := gomark.Parse(content) + if err != nil { + return "", err + } + result := renderer.NewHTMLRenderer().Render(nodes) + return result, nil +} + +func getRSSHeading(ctx context.Context, stores *store.Store) (RSSHeading, error) { + settings, err := stores.GetWorkspaceGeneralSetting(ctx) + if err != nil { + return RSSHeading{}, err + } + if settings == nil || settings.CustomProfile == nil { + return RSSHeading{ + Title: "Memos", + Description: "An open source, lightweight note-taking service. Easily capture and share your great thoughts.", + }, nil + } + customProfile := settings.CustomProfile + return RSSHeading{ + Title: customProfile.Title, + Description: customProfile.Description, + }, nil +} diff --git a/server/runner/memopayload/runner.go b/server/runner/memopayload/runner.go new file mode 100644 index 0000000..141110d --- /dev/null +++ b/server/runner/memopayload/runner.go @@ -0,0 +1,134 @@ +package memopayload + +import ( + "context" + "log/slog" + "slices" + + "github.com/pkg/errors" + "github.com/usememos/gomark/ast" + "github.com/usememos/gomark/parser" + "github.com/usememos/gomark/parser/tokenizer" + + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +type Runner struct { + Store *store.Store +} + +func NewRunner(store *store.Store) *Runner { + return &Runner{ + Store: store, + } +} + +// RunOnce rebuilds the payload of all memos. +func (r *Runner) RunOnce(ctx context.Context) { + // Process memos in batches to avoid loading all memos into memory at once + const batchSize = 100 + offset := 0 + processed := 0 + + for { + limit := batchSize + memos, err := r.Store.ListMemos(ctx, &store.FindMemo{ + Limit: &limit, + Offset: &offset, + }) + if err != nil { + slog.Error("failed to list memos", "err", err) + return + } + + // Break if no more memos + if len(memos) == 0 { + break + } + + // Process batch + batchSuccessCount := 0 + for _, memo := range memos { + if err := RebuildMemoPayload(memo); err != nil { + slog.Error("failed to rebuild memo payload", "err", err, "memoID", memo.ID) + continue + } + if err := r.Store.UpdateMemo(ctx, &store.UpdateMemo{ + ID: memo.ID, + Payload: memo.Payload, + }); err != nil { + slog.Error("failed to update memo", "err", err, "memoID", memo.ID) + continue + } + batchSuccessCount++ + } + + processed += len(memos) + slog.Info("Processed memo batch", "batchSize", len(memos), "successCount", batchSuccessCount, "totalProcessed", processed) + + // Move to next batch + offset += len(memos) + } +} + +func RebuildMemoPayload(memo *store.Memo) error { + nodes, err := parser.Parse(tokenizer.Tokenize(memo.Content)) + if err != nil { + return errors.Wrap(err, "failed to parse content") + } + + if memo.Payload == nil { + memo.Payload = &storepb.MemoPayload{} + } + tags := []string{} + property := &storepb.MemoPayload_Property{} + TraverseASTNodes(nodes, func(node ast.Node) { + switch n := node.(type) { + case *ast.Tag: + tag := n.Content + if !slices.Contains(tags, tag) { + tags = append(tags, tag) + } + case *ast.Link, *ast.AutoLink: + property.HasLink = true + case *ast.TaskListItem: + property.HasTaskList = true + if !n.Complete { + property.HasIncompleteTasks = true + } + case *ast.CodeBlock: + property.HasCode = true + case *ast.EmbeddedContent: + // TODO: validate references. + property.References = append(property.References, n.ResourceName) + } + }) + memo.Payload.Tags = tags + memo.Payload.Property = property + return nil +} + +func TraverseASTNodes(nodes []ast.Node, fn func(ast.Node)) { + for _, node := range nodes { + fn(node) + switch n := node.(type) { + case *ast.Paragraph: + TraverseASTNodes(n.Children, fn) + case *ast.Heading: + TraverseASTNodes(n.Children, fn) + case *ast.Blockquote: + TraverseASTNodes(n.Children, fn) + case *ast.List: + TraverseASTNodes(n.Children, fn) + case *ast.OrderedListItem: + TraverseASTNodes(n.Children, fn) + case *ast.UnorderedListItem: + TraverseASTNodes(n.Children, fn) + case *ast.TaskListItem: + TraverseASTNodes(n.Children, fn) + case *ast.Bold: + TraverseASTNodes(n.Children, fn) + } + } +} diff --git a/server/runner/s3presign/runner.go b/server/runner/s3presign/runner.go new file mode 100644 index 0000000..582aec1 --- /dev/null +++ b/server/runner/s3presign/runner.go @@ -0,0 +1,134 @@ +package s3presign + +import ( + "context" + "log/slog" + "time" + + "google.golang.org/protobuf/types/known/timestamppb" + + "github.com/usememos/memos/plugin/storage/s3" + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +type Runner struct { + Store *store.Store +} + +func NewRunner(store *store.Store) *Runner { + return &Runner{ + Store: store, + } +} + +// Schedule runner every 12 hours. +const runnerInterval = time.Hour * 12 + +func (r *Runner) Run(ctx context.Context) { + ticker := time.NewTicker(runnerInterval) + defer ticker.Stop() + + for { + select { + case <-ticker.C: + r.RunOnce(ctx) + case <-ctx.Done(): + return + } + } +} + +func (r *Runner) RunOnce(ctx context.Context) { + r.CheckAndPresign(ctx) +} + +func (r *Runner) CheckAndPresign(ctx context.Context) { + workspaceStorageSetting, err := r.Store.GetWorkspaceStorageSetting(ctx) + if err != nil { + return + } + + s3StorageType := storepb.AttachmentStorageType_S3 + // Limit attachments to a reasonable batch size + const batchSize = 100 + offset := 0 + + for { + limit := batchSize + attachments, err := r.Store.ListAttachments(ctx, &store.FindAttachment{ + GetBlob: false, + StorageType: &s3StorageType, + Limit: &limit, + Offset: &offset, + }) + if err != nil { + slog.Error("Failed to list attachments for presigning", "error", err) + return + } + + // Break if no more attachments + if len(attachments) == 0 { + break + } + + // Process batch of attachments + presignCount := 0 + for _, attachment := range attachments { + s3ObjectPayload := attachment.Payload.GetS3Object() + if s3ObjectPayload == nil { + continue + } + + if s3ObjectPayload.LastPresignedTime != nil { + // Skip if the presigned URL is still valid for the next 4 days. + // The expiration time is set to 5 days. + if time.Now().Before(s3ObjectPayload.LastPresignedTime.AsTime().Add(4 * 24 * time.Hour)) { + continue + } + } + + s3Config := workspaceStorageSetting.GetS3Config() + if s3ObjectPayload.S3Config != nil { + s3Config = s3ObjectPayload.S3Config + } + if s3Config == nil { + slog.Error("S3 config is not found") + continue + } + + s3Client, err := s3.NewClient(ctx, s3Config) + if err != nil { + slog.Error("Failed to create S3 client", "error", err) + continue + } + + presignURL, err := s3Client.PresignGetObject(ctx, s3ObjectPayload.Key) + if err != nil { + slog.Error("Failed to presign URL", "error", err, "attachmentID", attachment.ID) + continue + } + + s3ObjectPayload.S3Config = s3Config + s3ObjectPayload.LastPresignedTime = timestamppb.New(time.Now()) + if err := r.Store.UpdateAttachment(ctx, &store.UpdateAttachment{ + ID: attachment.ID, + Reference: &presignURL, + Payload: &storepb.AttachmentPayload{ + Payload: &storepb.AttachmentPayload_S3Object_{ + S3Object: s3ObjectPayload, + }, + }, + }); err != nil { + slog.Error("Failed to update attachment", "error", err, "attachmentID", attachment.ID) + continue + } + presignCount++ + } + + slog.Info("Presigned batch of S3 attachments", "batchSize", len(attachments), "presigned", presignCount) + + // Move to next batch + offset += len(attachments) + } +} diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..f5e7e83 --- /dev/null +++ b/server/server.go @@ -0,0 +1,227 @@ +package server + +import ( + "context" + "fmt" + "log/slog" + "math" + "net" + "net/http" + "runtime" + "time" + + "github.com/google/uuid" + grpcrecovery "github.com/grpc-ecosystem/go-grpc-middleware/recovery" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" + "github.com/pkg/errors" + "github.com/soheilhy/cmux" + "google.golang.org/grpc" + + "github.com/usememos/memos/internal/profile" + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/server/profiler" + apiv1 "github.com/usememos/memos/server/router/api/v1" + "github.com/usememos/memos/server/router/frontend" + "github.com/usememos/memos/server/router/rss" + "github.com/usememos/memos/server/runner/s3presign" + "github.com/usememos/memos/store" +) + +type Server struct { + Secret string + Profile *profile.Profile + Store *store.Store + + echoServer *echo.Echo + grpcServer *grpc.Server + profiler *profiler.Profiler + runnerCancelFuncs []context.CancelFunc +} + +func NewServer(ctx context.Context, profile *profile.Profile, store *store.Store) (*Server, error) { + s := &Server{ + Store: store, + Profile: profile, + } + + echoServer := echo.New() + echoServer.Debug = true + echoServer.HideBanner = true + echoServer.HidePort = true + echoServer.Use(middleware.Recover()) + s.echoServer = echoServer + + // Initialize profiler + s.profiler = profiler.NewProfiler() + s.profiler.RegisterRoutes(echoServer) + s.profiler.StartMemoryMonitor(ctx) + + workspaceBasicSetting, err := s.getOrUpsertWorkspaceBasicSetting(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to get workspace basic setting") + } + + secret := "usememos" + if profile.Mode == "prod" { + secret = workspaceBasicSetting.SecretKey + } + s.Secret = secret + + // Register healthz endpoint. + echoServer.GET("/healthz", func(c echo.Context) error { + return c.String(http.StatusOK, "Service ready.") + }) + + // Serve frontend static files. + frontend.NewFrontendService(profile, store).Serve(ctx, echoServer) + + rootGroup := echoServer.Group("") + + // Create and register RSS routes. + rss.NewRSSService(s.Profile, s.Store).RegisterRoutes(rootGroup) + + grpcServer := grpc.NewServer( + // Override the maximum receiving message size to math.MaxInt32 for uploading large attachments. + grpc.MaxRecvMsgSize(math.MaxInt32), + grpc.ChainUnaryInterceptor( + apiv1.NewLoggerInterceptor().LoggerInterceptor, + grpcrecovery.UnaryServerInterceptor(), + apiv1.NewGRPCAuthInterceptor(store, secret).AuthenticationInterceptor, + )) + s.grpcServer = grpcServer + + apiV1Service := apiv1.NewAPIV1Service(s.Secret, profile, store, grpcServer) + // Register gRPC gateway as api v1. + if err := apiV1Service.RegisterGateway(ctx, echoServer); err != nil { + return nil, errors.Wrap(err, "failed to register gRPC gateway") + } + + return s, nil +} + +func (s *Server) Start(ctx context.Context) error { + var address, network string + if len(s.Profile.UNIXSock) == 0 { + address = fmt.Sprintf("%s:%d", s.Profile.Addr, s.Profile.Port) + network = "tcp" + } else { + address = s.Profile.UNIXSock + network = "unix" + } + listener, err := net.Listen(network, address) + if err != nil { + return errors.Wrap(err, "failed to listen") + } + + muxServer := cmux.New(listener) + go func() { + grpcListener := muxServer.MatchWithWriters(cmux.HTTP2MatchHeaderFieldSendSettings("content-type", "application/grpc")) + if err := s.grpcServer.Serve(grpcListener); err != nil { + slog.Error("failed to serve gRPC", "error", err) + } + }() + go func() { + httpListener := muxServer.Match(cmux.HTTP1Fast(http.MethodPatch)) + s.echoServer.Listener = httpListener + if err := s.echoServer.Start(address); err != nil { + slog.Error("failed to start echo server", "error", err) + } + }() + go func() { + if err := muxServer.Serve(); err != nil { + slog.Error("mux server listen error", "error", err) + } + }() + s.StartBackgroundRunners(ctx) + + return nil +} + +func (s *Server) Shutdown(ctx context.Context) { + ctx, cancel := context.WithTimeout(ctx, 10*time.Second) + defer cancel() + + slog.Info("server shutting down") + + // Cancel all background runners + for _, cancelFunc := range s.runnerCancelFuncs { + if cancelFunc != nil { + cancelFunc() + } + } + + // Shutdown echo server. + if err := s.echoServer.Shutdown(ctx); err != nil { + slog.Error("failed to shutdown server", slog.String("error", err.Error())) + } + + // Shutdown gRPC server. + s.grpcServer.GracefulStop() + + // Stop the profiler + if s.profiler != nil { + slog.Info("stopping profiler") + // Log final memory stats + var m runtime.MemStats + runtime.ReadMemStats(&m) + slog.Info("final memory stats before exit", + "heapAlloc", m.Alloc, + "heapSys", m.Sys, + "heapObjects", m.HeapObjects, + "numGoroutine", runtime.NumGoroutine(), + ) + } + + // Close database connection. + if err := s.Store.Close(); err != nil { + slog.Error("failed to close database", slog.String("error", err.Error())) + } + + slog.Info("memos stopped properly") +} + +func (s *Server) StartBackgroundRunners(ctx context.Context) { + // Create a separate context for each background runner + // This allows us to control cancellation for each runner independently + s3Context, s3Cancel := context.WithCancel(ctx) + + // Store the cancel function so we can properly shut down runners + s.runnerCancelFuncs = append(s.runnerCancelFuncs, s3Cancel) + + // Create and start S3 presign runner + s3presignRunner := s3presign.NewRunner(s.Store) + s3presignRunner.RunOnce(ctx) + + // Start continuous S3 presign runner + go func() { + s3presignRunner.Run(s3Context) + slog.Info("s3presign runner stopped") + }() + + // Log the number of goroutines running + slog.Info("background runners started", "goroutines", runtime.NumGoroutine()) +} + +func (s *Server) getOrUpsertWorkspaceBasicSetting(ctx context.Context) (*storepb.WorkspaceBasicSetting, error) { + workspaceBasicSetting, err := s.Store.GetWorkspaceBasicSetting(ctx) + if err != nil { + return nil, errors.Wrap(err, "failed to get workspace basic setting") + } + modified := false + if workspaceBasicSetting.SecretKey == "" { + workspaceBasicSetting.SecretKey = uuid.NewString() + modified = true + } + if modified { + workspaceSetting, err := s.Store.UpsertWorkspaceSetting(ctx, &storepb.WorkspaceSetting{ + Key: storepb.WorkspaceSettingKey_BASIC, + Value: &storepb.WorkspaceSetting_BasicSetting{BasicSetting: workspaceBasicSetting}, + }) + if err != nil { + return nil, errors.Wrap(err, "failed to upsert workspace setting") + } + workspaceBasicSetting = workspaceSetting.GetBasicSetting() + } + return workspaceBasicSetting, nil +} diff --git a/store/activity.go b/store/activity.go new file mode 100644 index 0000000..eb5b7c9 --- /dev/null +++ b/store/activity.go @@ -0,0 +1,64 @@ +package store + +import ( + "context" + + storepb "github.com/usememos/memos/proto/gen/store" +) + +type ActivityType string + +const ( + ActivityTypeMemoComment ActivityType = "MEMO_COMMENT" +) + +func (t ActivityType) String() string { + return string(t) +} + +type ActivityLevel string + +const ( + ActivityLevelInfo ActivityLevel = "INFO" +) + +func (l ActivityLevel) String() string { + return string(l) +} + +type Activity struct { + ID int32 + + // Standard fields + CreatorID int32 + CreatedTs int64 + + // Domain specific fields + Type ActivityType + Level ActivityLevel + Payload *storepb.ActivityPayload +} + +type FindActivity struct { + ID *int32 + Type *ActivityType +} + +func (s *Store) CreateActivity(ctx context.Context, create *Activity) (*Activity, error) { + return s.driver.CreateActivity(ctx, create) +} + +func (s *Store) ListActivities(ctx context.Context, find *FindActivity) ([]*Activity, error) { + return s.driver.ListActivities(ctx, find) +} + +func (s *Store) GetActivity(ctx context.Context, find *FindActivity) (*Activity, error) { + list, err := s.ListActivities(ctx, find) + if err != nil { + return nil, err + } + if len(list) == 0 { + return nil, nil + } + return list[0], nil +} diff --git a/store/attachment.go b/store/attachment.go new file mode 100644 index 0000000..acbc177 --- /dev/null +++ b/store/attachment.go @@ -0,0 +1,166 @@ +package store + +import ( + "context" + "log/slog" + "os" + "path/filepath" + + "github.com/pkg/errors" + + "github.com/usememos/memos/internal/base" + "github.com/usememos/memos/plugin/storage/s3" + storepb "github.com/usememos/memos/proto/gen/store" +) + +type Attachment struct { + // ID is the system generated unique identifier for the attachment. + ID int32 + // UID is the user defined unique identifier for the attachment. + UID string + + // Standard fields + CreatorID int32 + CreatedTs int64 + UpdatedTs int64 + + // Domain specific fields + Filename string + Blob []byte + Type string + Size int64 + StorageType storepb.AttachmentStorageType + Reference string + Payload *storepb.AttachmentPayload + + // The related memo ID. + MemoID *int32 +} + +type FindAttachment struct { + GetBlob bool + ID *int32 + UID *string + CreatorID *int32 + Filename *string + FilenameSearch *string + MemoID *int32 + HasRelatedMemo bool + StorageType *storepb.AttachmentStorageType + Limit *int + Offset *int +} + +type UpdateAttachment struct { + ID int32 + UID *string + UpdatedTs *int64 + Filename *string + MemoID *int32 + Reference *string + Payload *storepb.AttachmentPayload +} + +type DeleteAttachment struct { + ID int32 + MemoID *int32 +} + +func (s *Store) CreateAttachment(ctx context.Context, create *Attachment) (*Attachment, error) { + if !base.UIDMatcher.MatchString(create.UID) { + return nil, errors.New("invalid uid") + } + return s.driver.CreateAttachment(ctx, create) +} + +func (s *Store) ListAttachments(ctx context.Context, find *FindAttachment) ([]*Attachment, error) { + // Set default limits to prevent loading too many attachments at once + if find.Limit == nil && find.GetBlob { + // When fetching blobs, we should be especially careful with limits + defaultLimit := 10 + find.Limit = &defaultLimit + } else if find.Limit == nil { + // Even without blobs, let's default to a reasonable limit + defaultLimit := 100 + find.Limit = &defaultLimit + } + + return s.driver.ListAttachments(ctx, find) +} + +func (s *Store) GetAttachment(ctx context.Context, find *FindAttachment) (*Attachment, error) { + attachments, err := s.ListAttachments(ctx, find) + if err != nil { + return nil, err + } + + if len(attachments) == 0 { + return nil, nil + } + + return attachments[0], nil +} + +func (s *Store) UpdateAttachment(ctx context.Context, update *UpdateAttachment) error { + if update.UID != nil && !base.UIDMatcher.MatchString(*update.UID) { + return errors.New("invalid uid") + } + return s.driver.UpdateAttachment(ctx, update) +} + +func (s *Store) DeleteAttachment(ctx context.Context, delete *DeleteAttachment) error { + attachment, err := s.GetAttachment(ctx, &FindAttachment{ID: &delete.ID}) + if err != nil { + return errors.Wrap(err, "failed to get attachment") + } + if attachment == nil { + return errors.New("attachment not found") + } + + if attachment.StorageType == storepb.AttachmentStorageType_LOCAL { + if err := func() error { + p := filepath.FromSlash(attachment.Reference) + if !filepath.IsAbs(p) { + p = filepath.Join(s.profile.Data, p) + } + err := os.Remove(p) + if err != nil { + return errors.Wrap(err, "failed to delete local file") + } + return nil + }(); err != nil { + return errors.Wrap(err, "failed to delete local file") + } + } else if attachment.StorageType == storepb.AttachmentStorageType_S3 { + if err := func() error { + s3ObjectPayload := attachment.Payload.GetS3Object() + if s3ObjectPayload == nil { + return errors.Errorf("No s3 object found") + } + workspaceStorageSetting, err := s.GetWorkspaceStorageSetting(ctx) + if err != nil { + return errors.Wrap(err, "failed to get workspace storage setting") + } + s3Config := s3ObjectPayload.S3Config + if s3Config == nil { + if workspaceStorageSetting.S3Config == nil { + return errors.Errorf("S3 config is not found") + } + s3Config = workspaceStorageSetting.S3Config + } + + s3Client, err := s3.NewClient(ctx, s3Config) + if err != nil { + return errors.Wrap(err, "Failed to create s3 client") + } + if err := s3Client.DeleteObject(ctx, s3ObjectPayload.Key); err != nil { + return errors.Wrap(err, "Failed to delete s3 object") + } + return nil + }(); err != nil { + slog.Warn("Failed to delete s3 object", slog.Any("err", err)) + } + } + + return s.driver.DeleteAttachment(ctx, delete) +} diff --git a/store/cache.go b/store/cache.go new file mode 100644 index 0000000..2a060f9 --- /dev/null +++ b/store/cache.go @@ -0,0 +1,9 @@ +package store + +import ( + "fmt" +) + +func getUserSettingCacheKey(userID int32, key string) string { + return fmt.Sprintf("%d-%s", userID, key) +} diff --git a/store/cache/cache.go b/store/cache/cache.go new file mode 100644 index 0000000..06e9a3d --- /dev/null +++ b/store/cache/cache.go @@ -0,0 +1,327 @@ +package cache + +import ( + "context" + "sync" + "sync/atomic" + "time" +) + +// Interface defines the operations a cache must support. +type Interface interface { + // Set adds a value to the cache with the default TTL. + Set(ctx context.Context, key string, value any) + + // SetWithTTL adds a value to the cache with a custom TTL. + SetWithTTL(ctx context.Context, key string, value any, ttl time.Duration) + + // Get retrieves a value from the cache. + Get(ctx context.Context, key string) (any, bool) + + // Delete removes a value from the cache. + Delete(ctx context.Context, key string) + + // Clear removes all values from the cache. + Clear(ctx context.Context) + + // Size returns the number of items in the cache. + Size() int64 + + // Close stops all background tasks and releases resources. + Close() error +} + +// item represents a cached value with metadata. +type item struct { + value any + expiration time.Time + size int // Approximate size in bytes +} + +// Config contains options for configuring a cache. +type Config struct { + // DefaultTTL is the default time-to-live for cache entries. + DefaultTTL time.Duration + + // CleanupInterval is how often the cache runs cleanup. + CleanupInterval time.Duration + + // MaxItems is the maximum number of items allowed in the cache. + MaxItems int + + // OnEviction is called when an item is evicted from the cache. + OnEviction func(key string, value any) +} + +// DefaultConfig returns a default configuration for the cache. +func DefaultConfig() Config { + return Config{ + DefaultTTL: 10 * time.Minute, + CleanupInterval: 5 * time.Minute, + MaxItems: 1000, + OnEviction: nil, + } +} + +// Cache is a thread-safe in-memory cache with TTL and memory management. +type Cache struct { + data sync.Map + config Config + itemCount int64 // Use atomic operations to track item count + stopChan chan struct{} + closedChan chan struct{} +} + +// New creates a new memory cache with the given configuration. +func New(config Config) *Cache { + c := &Cache{ + config: config, + stopChan: make(chan struct{}), + closedChan: make(chan struct{}), + } + + go c.cleanupLoop() + return c +} + +// NewDefault creates a new memory cache with default configuration. +func NewDefault() *Cache { + return New(DefaultConfig()) +} + +// Set adds a value to the cache with the default TTL. +func (c *Cache) Set(ctx context.Context, key string, value any) { + c.SetWithTTL(ctx, key, value, c.config.DefaultTTL) +} + +// SetWithTTL adds a value to the cache with a custom TTL. +func (c *Cache) SetWithTTL(_ context.Context, key string, value any, ttl time.Duration) { + // Estimate size of the item (very rough approximation). + size := estimateSize(value) + + // Check if item already exists to avoid double counting. + if _, exists := c.data.Load(key); exists { + c.data.Delete(key) + } else { + // Only increment if this is a new key. + atomic.AddInt64(&c.itemCount, 1) + } + + c.data.Store(key, item{ + value: value, + expiration: time.Now().Add(ttl), + size: size, + }) + + // If we're over the max items, clean up old items. + if c.config.MaxItems > 0 && atomic.LoadInt64(&c.itemCount) > int64(c.config.MaxItems) { + c.cleanupOldest() + } +} + +// Get retrieves a value from the cache. +func (c *Cache) Get(_ context.Context, key string) (any, bool) { + value, ok := c.data.Load(key) + if !ok { + return nil, false + } + + itm, ok := value.(item) + if !ok { + // If the value is not of type item, it means it was corrupted or not set correctly. + c.data.Delete(key) + return nil, false + } + if time.Now().After(itm.expiration) { + c.data.Delete(key) + atomic.AddInt64(&c.itemCount, -1) + + if c.config.OnEviction != nil { + c.config.OnEviction(key, itm.value) + } + + return nil, false + } + + return itm.value, true +} + +// Delete removes a value from the cache. +func (c *Cache) Delete(_ context.Context, key string) { + if value, loaded := c.data.LoadAndDelete(key); loaded { + atomic.AddInt64(&c.itemCount, -1) + + if c.config.OnEviction != nil { + if itm, ok := value.(item); ok { + c.config.OnEviction(key, itm.value) + } + } + } +} + +// Clear removes all values from the cache. +func (c *Cache) Clear(_ context.Context) { + if c.config.OnEviction != nil { + c.data.Range(func(key, value any) bool { + itm, ok := value.(item) + if !ok { + return true + } + if keyStr, ok := key.(string); ok { + c.config.OnEviction(keyStr, itm.value) + } + return true + }) + } + + c.data = sync.Map{} + atomic.StoreInt64(&c.itemCount, 0) +} + +// Size returns the number of items in the cache. +func (c *Cache) Size() int64 { + return atomic.LoadInt64(&c.itemCount) +} + +// Close stops the cache cleanup goroutine. +func (c *Cache) Close() error { + select { + case <-c.stopChan: + // Already closed + return nil + default: + close(c.stopChan) + <-c.closedChan // Wait for cleanup goroutine to exit + return nil + } +} + +// cleanupLoop periodically cleans up expired items. +func (c *Cache) cleanupLoop() { + ticker := time.NewTicker(c.config.CleanupInterval) + defer func() { + ticker.Stop() + close(c.closedChan) + }() + + for { + select { + case <-ticker.C: + c.cleanup() + case <-c.stopChan: + return + } + } +} + +// cleanup removes expired items. +func (c *Cache) cleanup() { + evicted := make(map[string]any) + count := 0 + + c.data.Range(func(key, value any) bool { + itm, ok := value.(item) + if !ok { + return true + } + if time.Now().After(itm.expiration) { + c.data.Delete(key) + count++ + + if c.config.OnEviction != nil { + if keyStr, ok := key.(string); ok { + evicted[keyStr] = itm.value + } + } + } + return true + }) + + if count > 0 { + atomic.AddInt64(&c.itemCount, -int64(count)) + + // Call eviction callbacks outside the loop to avoid blocking the range + if c.config.OnEviction != nil { + for k, v := range evicted { + c.config.OnEviction(k, v) + } + } + } +} + +// cleanupOldest removes the oldest items if we're over the max items. +func (c *Cache) cleanupOldest() { + // Remove 20% of max items at once + threshold := max(c.config.MaxItems/5, 1) + + currentCount := atomic.LoadInt64(&c.itemCount) + + // If we're not over the threshold, don't do anything + if currentCount <= int64(c.config.MaxItems) { + return + } + + // Find the oldest items + type keyExpPair struct { + key string + value any + expiration time.Time + } + candidates := make([]keyExpPair, 0, threshold) + + c.data.Range(func(key, value any) bool { + itm, ok := value.(item) + if !ok { + return true + } + if keyStr, ok := key.(string); ok && len(candidates) < threshold { + candidates = append(candidates, keyExpPair{keyStr, itm.value, itm.expiration}) + return true + } + + // Find the newest item in candidates + newestIdx := 0 + for i := 1; i < len(candidates); i++ { + if candidates[i].expiration.After(candidates[newestIdx].expiration) { + newestIdx = i + } + } + + // Replace it if this item is older + if itm.expiration.Before(candidates[newestIdx].expiration) { + candidates[newestIdx] = keyExpPair{key.(string), itm.value, itm.expiration} + } + + return true + }) + + // Delete the oldest items + deletedCount := 0 + for _, candidate := range candidates { + c.data.Delete(candidate.key) + deletedCount++ + + if c.config.OnEviction != nil { + c.config.OnEviction(candidate.key, candidate.value) + } + } + + // Update count + if deletedCount > 0 { + atomic.AddInt64(&c.itemCount, -int64(deletedCount)) + } +} + +// estimateSize attempts to estimate the memory footprint of a value. +func estimateSize(value any) int { + switch v := value.(type) { + case string: + return len(v) + 24 // base size + string overhead + case []byte: + return len(v) + 24 // base size + slice overhead + case map[string]any: + return len(v) * 64 // rough estimate + default: + return 64 // default conservative estimate + } +} diff --git a/store/cache/cache_test.go b/store/cache/cache_test.go new file mode 100644 index 0000000..3ba2330 --- /dev/null +++ b/store/cache/cache_test.go @@ -0,0 +1,209 @@ +package cache + +import ( + "context" + "fmt" + "sync" + "testing" + "time" +) + +func TestCacheBasicOperations(t *testing.T) { + ctx := context.Background() + config := DefaultConfig() + config.DefaultTTL = 100 * time.Millisecond + config.CleanupInterval = 50 * time.Millisecond + cache := New(config) + defer cache.Close() + + // Test Set and Get + cache.Set(ctx, "key1", "value1") + if val, ok := cache.Get(ctx, "key1"); !ok || val != "value1" { + t.Errorf("Expected 'value1', got %v, exists: %v", val, ok) + } + + // Test SetWithTTL + cache.SetWithTTL(ctx, "key2", "value2", 200*time.Millisecond) + if val, ok := cache.Get(ctx, "key2"); !ok || val != "value2" { + t.Errorf("Expected 'value2', got %v, exists: %v", val, ok) + } + + // Test Delete + cache.Delete(ctx, "key1") + if _, ok := cache.Get(ctx, "key1"); ok { + t.Errorf("Key 'key1' should have been deleted") + } + + // Test automatic expiration + time.Sleep(150 * time.Millisecond) + if _, ok := cache.Get(ctx, "key1"); ok { + t.Errorf("Key 'key1' should have expired") + } + // key2 should still be valid (200ms TTL) + if _, ok := cache.Get(ctx, "key2"); !ok { + t.Errorf("Key 'key2' should still be valid") + } + + // Wait for key2 to expire + time.Sleep(100 * time.Millisecond) + if _, ok := cache.Get(ctx, "key2"); ok { + t.Errorf("Key 'key2' should have expired") + } + + // Test Clear + cache.Set(ctx, "key3", "value3") + cache.Clear(ctx) + if _, ok := cache.Get(ctx, "key3"); ok { + t.Errorf("Cache should be empty after Clear()") + } +} + +func TestCacheEviction(t *testing.T) { + ctx := context.Background() + config := DefaultConfig() + config.MaxItems = 5 + cache := New(config) + defer cache.Close() + + // Add 5 items (max capacity) + for i := 0; i < 5; i++ { + key := fmt.Sprintf("key%d", i) + cache.Set(ctx, key, i) + } + + // Verify all 5 items are in the cache + for i := 0; i < 5; i++ { + key := fmt.Sprintf("key%d", i) + if _, ok := cache.Get(ctx, key); !ok { + t.Errorf("Key '%s' should be in the cache", key) + } + } + + // Add 2 more items to trigger eviction + cache.Set(ctx, "keyA", "valueA") + cache.Set(ctx, "keyB", "valueB") + + // Verify size is still within limits + if cache.Size() > int64(config.MaxItems) { + t.Errorf("Cache size %d exceeds limit %d", cache.Size(), config.MaxItems) + } + + // Some of the original keys should have been evicted + evictedCount := 0 + for i := 0; i < 5; i++ { + key := fmt.Sprintf("key%d", i) + if _, ok := cache.Get(ctx, key); !ok { + evictedCount++ + } + } + + if evictedCount == 0 { + t.Errorf("No keys were evicted despite exceeding max items") + } + + // The newer keys should still be present + if _, ok := cache.Get(ctx, "keyA"); !ok { + t.Errorf("Key 'keyA' should be in the cache") + } + if _, ok := cache.Get(ctx, "keyB"); !ok { + t.Errorf("Key 'keyB' should be in the cache") + } +} + +func TestCacheConcurrency(t *testing.T) { + ctx := context.Background() + cache := NewDefault() + defer cache.Close() + + const goroutines = 10 + const operationsPerGoroutine = 100 + + var wg sync.WaitGroup + wg.Add(goroutines) + + for i := 0; i < goroutines; i++ { + go func(id int) { + defer wg.Done() + + baseKey := fmt.Sprintf("worker%d-", id) + + // Set operations + for j := 0; j < operationsPerGoroutine; j++ { + key := fmt.Sprintf("%skey%d", baseKey, j) + value := fmt.Sprintf("value%d-%d", id, j) + cache.Set(ctx, key, value) + } + + // Get operations + for j := 0; j < operationsPerGoroutine; j++ { + key := fmt.Sprintf("%skey%d", baseKey, j) + val, ok := cache.Get(ctx, key) + if !ok { + t.Errorf("Key '%s' should exist in cache", key) + continue + } + expected := fmt.Sprintf("value%d-%d", id, j) + if val != expected { + t.Errorf("For key '%s', expected '%s', got '%s'", key, expected, val) + } + } + + // Delete half the keys + for j := 0; j < operationsPerGoroutine/2; j++ { + key := fmt.Sprintf("%skey%d", baseKey, j) + cache.Delete(ctx, key) + } + }(i) + } + + wg.Wait() + + // Verify size and deletion + var totalKeysExpected int64 = goroutines * operationsPerGoroutine / 2 + if cache.Size() != totalKeysExpected { + t.Errorf("Expected cache size to be %d, got %d", totalKeysExpected, cache.Size()) + } +} + +func TestEvictionCallback(t *testing.T) { + ctx := context.Background() + evicted := make(map[string]interface{}) + evictedMu := sync.Mutex{} + + config := DefaultConfig() + config.DefaultTTL = 50 * time.Millisecond + config.CleanupInterval = 25 * time.Millisecond + config.OnEviction = func(key string, value interface{}) { + evictedMu.Lock() + evicted[key] = value + evictedMu.Unlock() + } + + cache := New(config) + defer cache.Close() + + // Add items + cache.Set(ctx, "key1", "value1") + cache.Set(ctx, "key2", "value2") + + // Manually delete + cache.Delete(ctx, "key1") + + // Verify manual deletion triggered callback + time.Sleep(10 * time.Millisecond) // Small delay to ensure callback processed + evictedMu.Lock() + if evicted["key1"] != "value1" { + t.Errorf("Eviction callback not triggered for manual deletion") + } + evictedMu.Unlock() + + // Wait for automatic expiration + time.Sleep(60 * time.Millisecond) + + // Verify TTL expiration triggered callback + evictedMu.Lock() + if evicted["key2"] != "value2" { + t.Errorf("Eviction callback not triggered for TTL expiration") + } + evictedMu.Unlock() +} diff --git a/store/common.go b/store/common.go new file mode 100644 index 0000000..f0b3c6e --- /dev/null +++ b/store/common.go @@ -0,0 +1,24 @@ +package store + +import "google.golang.org/protobuf/encoding/protojson" + +var ( + protojsonUnmarshaler = protojson.UnmarshalOptions{ + AllowPartial: true, + DiscardUnknown: true, + } +) + +// RowStatus is the status for a row. +type RowStatus string + +const ( + // Normal is the status for a normal row. + Normal RowStatus = "NORMAL" + // Archived is the status for an archived row. + Archived RowStatus = "ARCHIVED" +) + +func (r RowStatus) String() string { + return string(r) +} diff --git a/store/db/db.go b/store/db/db.go new file mode 100644 index 0000000..586a434 --- /dev/null +++ b/store/db/db.go @@ -0,0 +1,32 @@ +package db + +import ( + "github.com/pkg/errors" + + "github.com/usememos/memos/internal/profile" + "github.com/usememos/memos/store" + "github.com/usememos/memos/store/db/mysql" + "github.com/usememos/memos/store/db/postgres" + "github.com/usememos/memos/store/db/sqlite" +) + +// NewDBDriver creates new db driver based on profile. +func NewDBDriver(profile *profile.Profile) (store.Driver, error) { + var driver store.Driver + var err error + + switch profile.Driver { + case "sqlite": + driver, err = sqlite.NewDB(profile) + case "mysql": + driver, err = mysql.NewDB(profile) + case "postgres": + driver, err = postgres.NewDB(profile) + default: + return nil, errors.New("unknown db driver") + } + if err != nil { + return nil, errors.Wrap(err, "failed to create db driver") + } + return driver, nil +} diff --git a/store/db/mysql/activity.go b/store/db/mysql/activity.go new file mode 100644 index 0000000..fd36be4 --- /dev/null +++ b/store/db/mysql/activity.go @@ -0,0 +1,93 @@ +package mysql + +import ( + "context" + "strings" + + "github.com/pkg/errors" + "google.golang.org/protobuf/encoding/protojson" + + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func (d *DB) CreateActivity(ctx context.Context, create *store.Activity) (*store.Activity, error) { + payloadString := "{}" + if create.Payload != nil { + bytes, err := protojson.Marshal(create.Payload) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal activity payload") + } + payloadString = string(bytes) + } + fields := []string{"`creator_id`", "`type`", "`level`", "`payload`"} + placeholder := []string{"?", "?", "?", "?"} + args := []any{create.CreatorID, create.Type.String(), create.Level.String(), payloadString} + + stmt := "INSERT INTO `activity` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ")" + result, err := d.db.ExecContext(ctx, stmt, args...) + if err != nil { + return nil, errors.Wrap(err, "failed to execute statement") + } + + id, err := result.LastInsertId() + if err != nil { + return nil, errors.Wrap(err, "failed to get last insert id") + } + + id32 := int32(id) + + list, err := d.ListActivities(ctx, &store.FindActivity{ID: &id32}) + if err != nil || len(list) == 0 { + return nil, errors.Wrap(err, "failed to find activity") + } + + return list[0], nil +} + +func (d *DB) ListActivities(ctx context.Context, find *store.FindActivity) ([]*store.Activity, error) { + where, args := []string{"1 = 1"}, []any{} + + if find.ID != nil { + where, args = append(where, "`id` = ?"), append(args, *find.ID) + } + if find.Type != nil { + where, args = append(where, "`type` = ?"), append(args, find.Type.String()) + } + + query := "SELECT `id`, `creator_id`, `type`, `level`, `payload`, UNIX_TIMESTAMP(`created_ts`) FROM `activity` WHERE " + strings.Join(where, " AND ") + " ORDER BY `created_ts` DESC" + rows, err := d.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + list := []*store.Activity{} + for rows.Next() { + activity := &store.Activity{} + var payloadBytes []byte + if err := rows.Scan( + &activity.ID, + &activity.CreatorID, + &activity.Type, + &activity.Level, + &payloadBytes, + &activity.CreatedTs, + ); err != nil { + return nil, err + } + + payload := &storepb.ActivityPayload{} + if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil { + return nil, err + } + activity.Payload = payload + list = append(list, activity) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} diff --git a/store/db/mysql/attachment.go b/store/db/mysql/attachment.go new file mode 100644 index 0000000..468e903 --- /dev/null +++ b/store/db/mysql/attachment.go @@ -0,0 +1,202 @@ +package mysql + +import ( + "context" + "database/sql" + "fmt" + "strings" + + "github.com/pkg/errors" + "google.golang.org/protobuf/encoding/protojson" + + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func (d *DB) CreateAttachment(ctx context.Context, create *store.Attachment) (*store.Attachment, error) { + fields := []string{"`uid`", "`filename`", "`blob`", "`type`", "`size`", "`creator_id`", "`memo_id`", "`storage_type`", "`reference`", "`payload`"} + placeholder := []string{"?", "?", "?", "?", "?", "?", "?", "?", "?", "?"} + storageType := "" + if create.StorageType != storepb.AttachmentStorageType_ATTACHMENT_STORAGE_TYPE_UNSPECIFIED { + storageType = create.StorageType.String() + } + payloadString := "{}" + if create.Payload != nil { + bytes, err := protojson.Marshal(create.Payload) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal attachment payload") + } + payloadString = string(bytes) + } + args := []any{create.UID, create.Filename, create.Blob, create.Type, create.Size, create.CreatorID, create.MemoID, storageType, create.Reference, payloadString} + + stmt := "INSERT INTO `resource` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ")" + result, err := d.db.ExecContext(ctx, stmt, args...) + if err != nil { + return nil, err + } + + id, err := result.LastInsertId() + if err != nil { + return nil, err + } + + id32 := int32(id) + return d.GetAttachment(ctx, &store.FindAttachment{ID: &id32}) +} + +func (d *DB) ListAttachments(ctx context.Context, find *store.FindAttachment) ([]*store.Attachment, error) { + where, args := []string{"1 = 1"}, []any{} + + if v := find.ID; v != nil { + where, args = append(where, "`id` = ?"), append(args, *v) + } + if v := find.UID; v != nil { + where, args = append(where, "`uid` = ?"), append(args, *v) + } + if v := find.CreatorID; v != nil { + where, args = append(where, "`creator_id` = ?"), append(args, *v) + } + if v := find.Filename; v != nil { + where, args = append(where, "`filename` = ?"), append(args, *v) + } + if v := find.FilenameSearch; v != nil { + where, args = append(where, "`filename` LIKE ?"), append(args, "%"+*v+"%") + } + if v := find.MemoID; v != nil { + where, args = append(where, "`memo_id` = ?"), append(args, *v) + } + if find.HasRelatedMemo { + where = append(where, "`memo_id` IS NOT NULL") + } + if find.StorageType != nil { + where, args = append(where, "`storage_type` = ?"), append(args, find.StorageType.String()) + } + + fields := []string{"`id`", "`uid`", "`filename`", "`type`", "`size`", "`creator_id`", "UNIX_TIMESTAMP(`created_ts`)", "UNIX_TIMESTAMP(`updated_ts`)", "`memo_id`", "`storage_type`", "`reference`", "`payload`"} + if find.GetBlob { + fields = append(fields, "`blob`") + } + + query := fmt.Sprintf("SELECT %s FROM `resource` WHERE %s ORDER BY `updated_ts` DESC", strings.Join(fields, ", "), strings.Join(where, " AND ")) + if find.Limit != nil { + query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit) + if find.Offset != nil { + query = fmt.Sprintf("%s OFFSET %d", query, *find.Offset) + } + } + + rows, err := d.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + list := make([]*store.Attachment, 0) + for rows.Next() { + attachment := store.Attachment{} + var memoID sql.NullInt32 + var storageType string + var payloadBytes []byte + dests := []any{ + &attachment.ID, + &attachment.UID, + &attachment.Filename, + &attachment.Type, + &attachment.Size, + &attachment.CreatorID, + &attachment.CreatedTs, + &attachment.UpdatedTs, + &memoID, + &storageType, + &attachment.Reference, + &payloadBytes, + } + if find.GetBlob { + dests = append(dests, &attachment.Blob) + } + if err := rows.Scan(dests...); err != nil { + return nil, err + } + + if memoID.Valid { + attachment.MemoID = &memoID.Int32 + } + attachment.StorageType = storepb.AttachmentStorageType(storepb.AttachmentStorageType_value[storageType]) + payload := &storepb.AttachmentPayload{} + if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil { + return nil, err + } + attachment.Payload = payload + list = append(list, &attachment) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} + +func (d *DB) GetAttachment(ctx context.Context, find *store.FindAttachment) (*store.Attachment, error) { + list, err := d.ListAttachments(ctx, find) + if err != nil { + return nil, err + } + if len(list) == 0 { + return nil, nil + } + + return list[0], nil +} + +func (d *DB) UpdateAttachment(ctx context.Context, update *store.UpdateAttachment) error { + set, args := []string{}, []any{} + + if v := update.UID; v != nil { + set, args = append(set, "`uid` = ?"), append(args, *v) + } + if v := update.UpdatedTs; v != nil { + set, args = append(set, "`updated_ts` = FROM_UNIXTIME(?)"), append(args, *v) + } + if v := update.Filename; v != nil { + set, args = append(set, "`filename` = ?"), append(args, *v) + } + if v := update.MemoID; v != nil { + set, args = append(set, "`memo_id` = ?"), append(args, *v) + } + if v := update.Reference; v != nil { + set, args = append(set, "`reference` = ?"), append(args, *v) + } + if v := update.Payload; v != nil { + bytes, err := protojson.Marshal(v) + if err != nil { + return errors.Wrap(err, "failed to marshal attachment payload") + } + set, args = append(set, "`payload` = ?"), append(args, string(bytes)) + } + + args = append(args, update.ID) + stmt := "UPDATE `resource` SET " + strings.Join(set, ", ") + " WHERE `id` = ?" + result, err := d.db.ExecContext(ctx, stmt, args...) + if err != nil { + return err + } + if _, err := result.RowsAffected(); err != nil { + return err + } + return nil +} + +func (d *DB) DeleteAttachment(ctx context.Context, delete *store.DeleteAttachment) error { + stmt := "DELETE FROM `resource` WHERE `id` = ?" + result, err := d.db.ExecContext(ctx, stmt, delete.ID) + if err != nil { + return err + } + if _, err := result.RowsAffected(); err != nil { + return err + } + + return nil +} diff --git a/store/db/mysql/common.go b/store/db/mysql/common.go new file mode 100644 index 0000000..7159874 --- /dev/null +++ b/store/db/mysql/common.go @@ -0,0 +1,10 @@ +package mysql + +import "google.golang.org/protobuf/encoding/protojson" + +var ( + protojsonUnmarshaler = protojson.UnmarshalOptions{ + AllowPartial: true, + DiscardUnknown: true, + } +) diff --git a/store/db/mysql/idp.go b/store/db/mysql/idp.go new file mode 100644 index 0000000..4f0ba49 --- /dev/null +++ b/store/db/mysql/idp.go @@ -0,0 +1,126 @@ +package mysql + +import ( + "context" + "strings" + + "github.com/pkg/errors" + + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func (d *DB) CreateIdentityProvider(ctx context.Context, create *store.IdentityProvider) (*store.IdentityProvider, error) { + placeholders := []string{"?", "?", "?", "?"} + fields := []string{"`name`", "`type`", "`identifier_filter`", "`config`"} + args := []any{create.Name, create.Type.String(), create.IdentifierFilter, create.Config} + + stmt := "INSERT INTO `idp` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholders, ", ") + ")" + result, err := d.db.ExecContext(ctx, stmt, args...) + if err != nil { + return nil, err + } + + id, err := result.LastInsertId() + if err != nil { + return nil, err + } + + create.ID = int32(id) + return create, nil +} + +func (d *DB) ListIdentityProviders(ctx context.Context, find *store.FindIdentityProvider) ([]*store.IdentityProvider, error) { + where, args := []string{"1 = 1"}, []any{} + if v := find.ID; v != nil { + where, args = append(where, "`id` = ?"), append(args, *v) + } + + rows, err := d.db.QueryContext(ctx, "SELECT `id`, `name`, `type`, `identifier_filter`, `config` FROM `idp` WHERE "+strings.Join(where, " AND ")+" ORDER BY `id` ASC", + args..., + ) + if err != nil { + return nil, err + } + defer rows.Close() + + var identityProviders []*store.IdentityProvider + for rows.Next() { + var identityProvider store.IdentityProvider + var typeString string + if err := rows.Scan( + &identityProvider.ID, + &identityProvider.Name, + &typeString, + &identityProvider.IdentifierFilter, + &identityProvider.Config, + ); err != nil { + return nil, err + } + identityProvider.Type = storepb.IdentityProvider_Type(storepb.IdentityProvider_Type_value[typeString]) + identityProviders = append(identityProviders, &identityProvider) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return identityProviders, nil +} + +func (d *DB) GetIdentityProvider(ctx context.Context, find *store.FindIdentityProvider) (*store.IdentityProvider, error) { + list, err := d.ListIdentityProviders(ctx, find) + if err != nil { + return nil, err + } + if len(list) == 0 { + return nil, nil + } + + identityProvider := list[0] + return identityProvider, nil +} + +func (d *DB) UpdateIdentityProvider(ctx context.Context, update *store.UpdateIdentityProvider) (*store.IdentityProvider, error) { + set, args := []string{}, []any{} + if v := update.Name; v != nil { + set, args = append(set, "`name` = ?"), append(args, *v) + } + if v := update.IdentifierFilter; v != nil { + set, args = append(set, "`identifier_filter` = ?"), append(args, *v) + } + if v := update.Config; v != nil { + set, args = append(set, "`config` = ?"), append(args, *v) + } + args = append(args, update.ID) + + stmt := "UPDATE `idp` SET " + strings.Join(set, ", ") + " WHERE `id` = ?" + _, err := d.db.ExecContext(ctx, stmt, args...) + if err != nil { + return nil, err + } + + identityProvider, err := d.GetIdentityProvider(ctx, &store.FindIdentityProvider{ + ID: &update.ID, + }) + if err != nil { + return nil, err + } + if identityProvider == nil { + return nil, errors.Errorf("idp %d not found", update.ID) + } + return identityProvider, nil +} + +func (d *DB) DeleteIdentityProvider(ctx context.Context, delete *store.DeleteIdentityProvider) error { + where, args := []string{"`id` = ?"}, []any{delete.ID} + stmt := "DELETE FROM `idp` WHERE " + strings.Join(where, " AND ") + result, err := d.db.ExecContext(ctx, stmt, args...) + if err != nil { + return err + } + if _, err = result.RowsAffected(); err != nil { + return err + } + return nil +} diff --git a/store/db/mysql/inbox.go b/store/db/mysql/inbox.go new file mode 100644 index 0000000..a18fb51 --- /dev/null +++ b/store/db/mysql/inbox.go @@ -0,0 +1,141 @@ +package mysql + +import ( + "context" + "fmt" + "strings" + + "github.com/pkg/errors" + "google.golang.org/protobuf/encoding/protojson" + + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func (d *DB) CreateInbox(ctx context.Context, create *store.Inbox) (*store.Inbox, error) { + messageString := "{}" + if create.Message != nil { + bytes, err := protojson.Marshal(create.Message) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal inbox message") + } + messageString = string(bytes) + } + + fields := []string{"`sender_id`", "`receiver_id`", "`status`", "`message`"} + placeholder := []string{"?", "?", "?", "?"} + args := []any{create.SenderID, create.ReceiverID, create.Status, messageString} + + stmt := "INSERT INTO `inbox` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ")" + result, err := d.db.ExecContext(ctx, stmt, args...) + if err != nil { + return nil, err + } + + id, err := result.LastInsertId() + if err != nil { + return nil, err + } + + id32 := int32(id) + inbox, err := d.GetInbox(ctx, &store.FindInbox{ID: &id32}) + if err != nil { + return nil, err + } + return inbox, nil +} + +func (d *DB) ListInboxes(ctx context.Context, find *store.FindInbox) ([]*store.Inbox, error) { + where, args := []string{"1 = 1"}, []any{} + + if find.ID != nil { + where, args = append(where, "`id` = ?"), append(args, *find.ID) + } + if find.SenderID != nil { + where, args = append(where, "`sender_id` = ?"), append(args, *find.SenderID) + } + if find.ReceiverID != nil { + where, args = append(where, "`receiver_id` = ?"), append(args, *find.ReceiverID) + } + if find.Status != nil { + where, args = append(where, "`status` = ?"), append(args, *find.Status) + } + + query := "SELECT `id`, UNIX_TIMESTAMP(`created_ts`), `sender_id`, `receiver_id`, `status`, `message` FROM `inbox` WHERE " + strings.Join(where, " AND ") + " ORDER BY `created_ts` DESC" + if find.Limit != nil { + query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit) + if find.Offset != nil { + query = fmt.Sprintf("%s OFFSET %d", query, *find.Offset) + } + } + rows, err := d.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + list := []*store.Inbox{} + for rows.Next() { + inbox := &store.Inbox{} + var messageBytes []byte + if err := rows.Scan( + &inbox.ID, + &inbox.CreatedTs, + &inbox.SenderID, + &inbox.ReceiverID, + &inbox.Status, + &messageBytes, + ); err != nil { + return nil, err + } + + message := &storepb.InboxMessage{} + if err := protojsonUnmarshaler.Unmarshal(messageBytes, message); err != nil { + return nil, err + } + inbox.Message = message + list = append(list, inbox) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} + +func (d *DB) GetInbox(ctx context.Context, find *store.FindInbox) (*store.Inbox, error) { + list, err := d.ListInboxes(ctx, find) + if err != nil { + return nil, errors.Wrap(err, "failed to get inbox") + } + if len(list) != 1 { + return nil, errors.Errorf("unexpected inbox count: %d", len(list)) + } + return list[0], nil +} + +func (d *DB) UpdateInbox(ctx context.Context, update *store.UpdateInbox) (*store.Inbox, error) { + set, args := []string{"`status` = ?"}, []any{update.Status.String()} + args = append(args, update.ID) + query := "UPDATE `inbox` SET " + strings.Join(set, ", ") + " WHERE `id` = ?" + if _, err := d.db.ExecContext(ctx, query, args...); err != nil { + return nil, errors.Wrap(err, "failed to update inbox") + } + inbox, err := d.GetInbox(ctx, &store.FindInbox{ID: &update.ID}) + if err != nil { + return nil, err + } + return inbox, nil +} + +func (d *DB) DeleteInbox(ctx context.Context, delete *store.DeleteInbox) error { + result, err := d.db.ExecContext(ctx, "DELETE FROM `inbox` WHERE `id` = ?", delete.ID) + if err != nil { + return errors.Wrap(err, "failed to delete inbox") + } + if _, err := result.RowsAffected(); err != nil { + return err + } + return nil +} diff --git a/store/db/mysql/memo.go b/store/db/mysql/memo.go new file mode 100644 index 0000000..4dd87d3 --- /dev/null +++ b/store/db/mysql/memo.go @@ -0,0 +1,287 @@ +package mysql + +import ( + "context" + "fmt" + "strings" + + "github.com/pkg/errors" + "google.golang.org/protobuf/encoding/protojson" + + "github.com/usememos/memos/plugin/filter" + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func (d *DB) CreateMemo(ctx context.Context, create *store.Memo) (*store.Memo, error) { + fields := []string{"`uid`", "`creator_id`", "`content`", "`visibility`", "`payload`"} + placeholder := []string{"?", "?", "?", "?", "?"} + payload := "{}" + if create.Payload != nil { + payloadBytes, err := protojson.Marshal(create.Payload) + if err != nil { + return nil, err + } + payload = string(payloadBytes) + } + args := []any{create.UID, create.CreatorID, create.Content, create.Visibility, payload} + + stmt := "INSERT INTO `memo` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ")" + result, err := d.db.ExecContext(ctx, stmt, args...) + if err != nil { + return nil, err + } + + rawID, err := result.LastInsertId() + if err != nil { + return nil, err + } + id := int32(rawID) + memo, err := d.GetMemo(ctx, &store.FindMemo{ID: &id}) + if err != nil { + return nil, err + } + if memo == nil { + return nil, errors.Errorf("failed to create memo") + } + return memo, nil +} + +func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo, error) { + where, having, args := []string{"1 = 1"}, []string{"1 = 1"}, []any{} + + if v := find.ID; v != nil { + where, args = append(where, "`memo`.`id` = ?"), append(args, *v) + } + if v := find.UID; v != nil { + where, args = append(where, "`memo`.`uid` = ?"), append(args, *v) + } + if v := find.CreatorID; v != nil { + where, args = append(where, "`memo`.`creator_id` = ?"), append(args, *v) + } + if v := find.RowStatus; v != nil { + where, args = append(where, "`memo`.`row_status` = ?"), append(args, *v) + } + if v := find.CreatedTsBefore; v != nil { + where, args = append(where, "UNIX_TIMESTAMP(`memo`.`created_ts`) < ?"), append(args, *v) + } + if v := find.CreatedTsAfter; v != nil { + where, args = append(where, "UNIX_TIMESTAMP(`memo`.`created_ts`) > ?"), append(args, *v) + } + if v := find.UpdatedTsBefore; v != nil { + where, args = append(where, "UNIX_TIMESTAMP(`memo`.`updated_ts`) < ?"), append(args, *v) + } + if v := find.UpdatedTsAfter; v != nil { + where, args = append(where, "UNIX_TIMESTAMP(`memo`.`updated_ts`) > ?"), append(args, *v) + } + if v := find.ContentSearch; len(v) != 0 { + for _, s := range v { + where, args = append(where, "`memo`.`content` LIKE ?"), append(args, "%"+s+"%") + } + } + if v := find.VisibilityList; len(v) != 0 { + placeholder := []string{} + for _, visibility := range v { + placeholder = append(placeholder, "?") + args = append(args, visibility.String()) + } + where = append(where, fmt.Sprintf("`memo`.`visibility` in (%s)", strings.Join(placeholder, ","))) + } + if v := find.Pinned; v != nil { + where, args = append(where, "`memo`.`pinned` = ?"), append(args, *v) + } + if v := find.PayloadFind; v != nil { + if v.Raw != nil { + where, args = append(where, "`memo`.`payload` = ?"), append(args, *v.Raw) + } + if len(v.TagSearch) != 0 { + for _, tag := range v.TagSearch { + where, args = append(where, "(JSON_CONTAINS(JSON_EXTRACT(`memo`.`payload`, '$.tags'), ?) OR JSON_CONTAINS(JSON_EXTRACT(`memo`.`payload`, '$.tags'), ?))"), append(args, fmt.Sprintf(`"%s"`, tag), fmt.Sprintf(`"%s/"`, tag)) + } + } + if v.HasLink { + where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasLink') IS TRUE") + } + if v.HasTaskList { + where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') IS TRUE") + } + if v.HasCode { + where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasCode') IS TRUE") + } + if v.HasIncompleteTasks { + where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasIncompleteTasks') IS TRUE") + } + } + if v := find.Filter; v != nil { + // Parse filter string and return the parsed expression. + // The filter string should be a CEL expression. + parsedExpr, err := filter.Parse(*v, filter.MemoFilterCELAttributes...) + if err != nil { + return nil, err + } + convertCtx := filter.NewConvertContext() + // ConvertExprToSQL converts the parsed expression to a SQL condition string. + if err := d.ConvertExprToSQL(convertCtx, parsedExpr.GetExpr()); err != nil { + return nil, err + } + condition := convertCtx.Buffer.String() + if condition != "" { + where = append(where, fmt.Sprintf("(%s)", condition)) + args = append(args, convertCtx.Args...) + } + } + if find.ExcludeComments { + having = append(having, "`parent_id` IS NULL") + } + + order := "DESC" + if find.OrderByTimeAsc { + order = "ASC" + } + orderBy := []string{} + if find.OrderByPinned { + orderBy = append(orderBy, "`pinned` DESC") + } + if find.OrderByUpdatedTs { + orderBy = append(orderBy, "`updated_ts` "+order) + } else { + orderBy = append(orderBy, "`created_ts` "+order) + } + fields := []string{ + "`memo`.`id` AS `id`", + "`memo`.`uid` AS `uid`", + "`memo`.`creator_id` AS `creator_id`", + "UNIX_TIMESTAMP(`memo`.`created_ts`) AS `created_ts`", + "UNIX_TIMESTAMP(`memo`.`updated_ts`) AS `updated_ts`", + "`memo`.`row_status` AS `row_status`", + "`memo`.`visibility` AS `visibility`", + "`memo`.`pinned` AS `pinned`", + "`memo`.`payload` AS `payload`", + "`memo_relation`.`related_memo_id` AS `parent_id`", + } + if !find.ExcludeContent { + fields = append(fields, "`memo`.`content` AS `content`") + } + + query := "SELECT " + strings.Join(fields, ", ") + " FROM `memo`" + " " + + "LEFT JOIN `memo_relation` ON `memo`.`id` = `memo_relation`.`memo_id` AND `memo_relation`.`type` = 'COMMENT'" + " " + + "WHERE " + strings.Join(where, " AND ") + " " + + "HAVING " + strings.Join(having, " AND ") + " " + + "ORDER BY " + strings.Join(orderBy, ", ") + if find.Limit != nil { + query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit) + if find.Offset != nil { + query = fmt.Sprintf("%s OFFSET %d", query, *find.Offset) + } + } + + rows, err := d.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + list := make([]*store.Memo, 0) + for rows.Next() { + var memo store.Memo + var payloadBytes []byte + dests := []any{ + &memo.ID, + &memo.UID, + &memo.CreatorID, + &memo.CreatedTs, + &memo.UpdatedTs, + &memo.RowStatus, + &memo.Visibility, + &memo.Pinned, + &payloadBytes, + &memo.ParentID, + } + if !find.ExcludeContent { + dests = append(dests, &memo.Content) + } + if err := rows.Scan(dests...); err != nil { + return nil, err + } + payload := &storepb.MemoPayload{} + if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal payload") + } + memo.Payload = payload + list = append(list, &memo) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} + +func (d *DB) GetMemo(ctx context.Context, find *store.FindMemo) (*store.Memo, error) { + list, err := d.ListMemos(ctx, find) + if err != nil { + return nil, err + } + if len(list) == 0 { + return nil, nil + } + + memo := list[0] + return memo, nil +} + +func (d *DB) UpdateMemo(ctx context.Context, update *store.UpdateMemo) error { + set, args := []string{}, []any{} + if v := update.UID; v != nil { + set, args = append(set, "`uid` = ?"), append(args, *v) + } + if v := update.CreatedTs; v != nil { + set, args = append(set, "`created_ts` = FROM_UNIXTIME(?)"), append(args, *v) + } + if v := update.UpdatedTs; v != nil { + set, args = append(set, "`updated_ts` = FROM_UNIXTIME(?)"), append(args, *v) + } + if v := update.RowStatus; v != nil { + set, args = append(set, "`row_status` = ?"), append(args, *v) + } + if v := update.Content; v != nil { + set, args = append(set, "`content` = ?"), append(args, *v) + } + if v := update.Visibility; v != nil { + set, args = append(set, "`visibility` = ?"), append(args, *v) + } + if v := update.Pinned; v != nil { + set, args = append(set, "`pinned` = ?"), append(args, *v) + } + if v := update.Payload; v != nil { + payloadBytes, err := protojson.Marshal(v) + if err != nil { + return err + } + set, args = append(set, "`payload` = ?"), append(args, string(payloadBytes)) + } + if len(set) == 0 { + return nil + } + args = append(args, update.ID) + + stmt := "UPDATE `memo` SET " + strings.Join(set, ", ") + " WHERE `id` = ?" + if _, err := d.db.ExecContext(ctx, stmt, args...); err != nil { + return err + } + return nil +} + +func (d *DB) DeleteMemo(ctx context.Context, delete *store.DeleteMemo) error { + where, args := []string{"`id` = ?"}, []any{delete.ID} + stmt := "DELETE FROM `memo` WHERE " + strings.Join(where, " AND ") + result, err := d.db.ExecContext(ctx, stmt, args...) + if err != nil { + return err + } + if _, err := result.RowsAffected(); err != nil { + return err + } + return nil +} diff --git a/store/db/mysql/memo_filter.go b/store/db/mysql/memo_filter.go new file mode 100644 index 0000000..8ae7034 --- /dev/null +++ b/store/db/mysql/memo_filter.go @@ -0,0 +1,304 @@ +package mysql + +import ( + "fmt" + "slices" + "strings" + + "github.com/pkg/errors" + exprv1 "google.golang.org/genproto/googleapis/api/expr/v1alpha1" + + "github.com/usememos/memos/plugin/filter" +) + +func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) error { + return d.convertWithTemplates(ctx, expr) +} + +func (d *DB) convertWithTemplates(ctx *filter.ConvertContext, expr *exprv1.Expr) error { + const dbType = filter.MySQLTemplate + + if v, ok := expr.ExprKind.(*exprv1.Expr_CallExpr); ok { + switch v.CallExpr.Function { + case "_||_", "_&&_": + if len(v.CallExpr.Args) != 2 { + return errors.Errorf("invalid number of arguments for %s", v.CallExpr.Function) + } + if _, err := ctx.Buffer.WriteString("("); err != nil { + return err + } + if err := d.convertWithTemplates(ctx, v.CallExpr.Args[0]); err != nil { + return err + } + operator := "AND" + if v.CallExpr.Function == "_||_" { + operator = "OR" + } + if _, err := ctx.Buffer.WriteString(fmt.Sprintf(" %s ", operator)); err != nil { + return err + } + if err := d.convertWithTemplates(ctx, v.CallExpr.Args[1]); err != nil { + return err + } + if _, err := ctx.Buffer.WriteString(")"); err != nil { + return err + } + case "!_": + if len(v.CallExpr.Args) != 1 { + return errors.Errorf("invalid number of arguments for %s", v.CallExpr.Function) + } + if _, err := ctx.Buffer.WriteString("NOT ("); err != nil { + return err + } + if err := d.convertWithTemplates(ctx, v.CallExpr.Args[0]); err != nil { + return err + } + if _, err := ctx.Buffer.WriteString(")"); err != nil { + return err + } + case "_==_", "_!=_", "_<_", "_>_", "_<=_", "_>=_": + if len(v.CallExpr.Args) != 2 { + return errors.Errorf("invalid number of arguments for %s", v.CallExpr.Function) + } + // Check if the left side is a function call like size(tags) + if leftCallExpr, ok := v.CallExpr.Args[0].ExprKind.(*exprv1.Expr_CallExpr); ok { + if leftCallExpr.CallExpr.Function == "size" { + // Handle size(tags) comparison + if len(leftCallExpr.CallExpr.Args) != 1 { + return errors.New("size function requires exactly one argument") + } + identifier, err := filter.GetIdentExprName(leftCallExpr.CallExpr.Args[0]) + if err != nil { + return err + } + if identifier != "tags" { + return errors.Errorf("size function only supports 'tags' identifier, got: %s", identifier) + } + value, err := filter.GetExprValue(v.CallExpr.Args[1]) + if err != nil { + return err + } + valueInt, ok := value.(int64) + if !ok { + return errors.New("size comparison value must be an integer") + } + operator := d.getComparisonOperator(v.CallExpr.Function) + + if _, err := ctx.Buffer.WriteString(fmt.Sprintf("%s %s ?", + filter.GetSQL("json_array_length", dbType), operator)); err != nil { + return err + } + ctx.Args = append(ctx.Args, valueInt) + return nil + } + } + + identifier, err := filter.GetIdentExprName(v.CallExpr.Args[0]) + if err != nil { + return err + } + if !slices.Contains([]string{"creator_id", "created_ts", "updated_ts", "visibility", "content", "has_task_list"}, identifier) { + return errors.Errorf("invalid identifier for %s", v.CallExpr.Function) + } + value, err := filter.GetExprValue(v.CallExpr.Args[1]) + if err != nil { + return err + } + operator := d.getComparisonOperator(v.CallExpr.Function) + + if identifier == "created_ts" || identifier == "updated_ts" { + valueInt, ok := value.(int64) + if !ok { + return errors.New("invalid integer timestamp value") + } + + timestampSQL := fmt.Sprintf(filter.GetSQL("timestamp_field", dbType), identifier) + if _, err := ctx.Buffer.WriteString(fmt.Sprintf("%s %s ?", timestampSQL, operator)); err != nil { + return err + } + ctx.Args = append(ctx.Args, valueInt) + } else if identifier == "visibility" || identifier == "content" { + if operator != "=" && operator != "!=" { + return errors.Errorf("invalid operator for %s", v.CallExpr.Function) + } + valueStr, ok := value.(string) + if !ok { + return errors.New("invalid string value") + } + + var sqlTemplate string + if identifier == "visibility" { + sqlTemplate = filter.GetSQL("table_prefix", dbType) + ".`visibility`" + } else if identifier == "content" { + sqlTemplate = filter.GetSQL("table_prefix", dbType) + ".`content`" + } + if _, err := ctx.Buffer.WriteString(fmt.Sprintf("%s %s ?", sqlTemplate, operator)); err != nil { + return err + } + ctx.Args = append(ctx.Args, valueStr) + } else if identifier == "creator_id" { + if operator != "=" && operator != "!=" { + return errors.Errorf("invalid operator for %s", v.CallExpr.Function) + } + valueInt, ok := value.(int64) + if !ok { + return errors.New("invalid int value") + } + + sqlTemplate := filter.GetSQL("table_prefix", dbType) + ".`creator_id`" + if _, err := ctx.Buffer.WriteString(fmt.Sprintf("%s %s ?", sqlTemplate, operator)); err != nil { + return err + } + ctx.Args = append(ctx.Args, valueInt) + } else if identifier == "has_task_list" { + if operator != "=" && operator != "!=" { + return errors.Errorf("invalid operator for %s", v.CallExpr.Function) + } + valueBool, ok := value.(bool) + if !ok { + return errors.New("invalid boolean value for has_task_list") + } + // Use template for boolean comparison + var sqlTemplate string + if operator == "=" { + if valueBool { + sqlTemplate = filter.GetSQL("boolean_true", dbType) + } else { + sqlTemplate = filter.GetSQL("boolean_false", dbType) + } + } else { // operator == "!=" + if valueBool { + sqlTemplate = filter.GetSQL("boolean_not_true", dbType) + } else { + sqlTemplate = filter.GetSQL("boolean_not_false", dbType) + } + } + if _, err := ctx.Buffer.WriteString(sqlTemplate); err != nil { + return err + } + } + case "@in": + if len(v.CallExpr.Args) != 2 { + return errors.Errorf("invalid number of arguments for %s", v.CallExpr.Function) + } + + // Check if this is "element in collection" syntax + if identifier, err := filter.GetIdentExprName(v.CallExpr.Args[1]); err == nil { + // This is "element in collection" - the second argument is the collection + if !slices.Contains([]string{"tags"}, identifier) { + return errors.Errorf("invalid collection identifier for %s: %s", v.CallExpr.Function, identifier) + } + + if identifier == "tags" { + // Handle "element" in tags + element, err := filter.GetConstValue(v.CallExpr.Args[0]) + if err != nil { + return errors.Errorf("first argument must be a constant value for 'element in tags': %v", err) + } + if _, err := ctx.Buffer.WriteString(filter.GetSQL("json_contains_element", dbType)); err != nil { + return err + } + ctx.Args = append(ctx.Args, filter.GetParameterValue(dbType, "json_contains_element", element)) + } + return nil + } + + // Original logic for "identifier in [list]" syntax + identifier, err := filter.GetIdentExprName(v.CallExpr.Args[0]) + if err != nil { + return err + } + if !slices.Contains([]string{"tag", "visibility"}, identifier) { + return errors.Errorf("invalid identifier for %s", v.CallExpr.Function) + } + + values := []any{} + for _, element := range v.CallExpr.Args[1].GetListExpr().Elements { + value, err := filter.GetConstValue(element) + if err != nil { + return err + } + values = append(values, value) + } + if identifier == "tag" { + subconditions := []string{} + args := []any{} + for _, v := range values { + subconditions = append(subconditions, filter.GetSQL("json_contains_tag", dbType)) + args = append(args, filter.GetParameterValue(dbType, "json_contains_tag", v)) + } + if len(subconditions) == 1 { + if _, err := ctx.Buffer.WriteString(subconditions[0]); err != nil { + return err + } + } else { + if _, err := ctx.Buffer.WriteString(fmt.Sprintf("(%s)", strings.Join(subconditions, " OR "))); err != nil { + return err + } + } + ctx.Args = append(ctx.Args, args...) + } else if identifier == "visibility" { + placeholders := filter.FormatPlaceholders(dbType, len(values), 1) + visibilitySQL := fmt.Sprintf(filter.GetSQL("visibility_in", dbType), strings.Join(placeholders, ",")) + if _, err := ctx.Buffer.WriteString(visibilitySQL); err != nil { + return err + } + ctx.Args = append(ctx.Args, values...) + } + case "contains": + if len(v.CallExpr.Args) != 1 { + return errors.Errorf("invalid number of arguments for %s", v.CallExpr.Function) + } + identifier, err := filter.GetIdentExprName(v.CallExpr.Target) + if err != nil { + return err + } + if identifier != "content" { + return errors.Errorf("invalid identifier for %s", v.CallExpr.Function) + } + arg, err := filter.GetConstValue(v.CallExpr.Args[0]) + if err != nil { + return err + } + if _, err := ctx.Buffer.WriteString(filter.GetSQL("content_like", dbType)); err != nil { + return err + } + ctx.Args = append(ctx.Args, fmt.Sprintf("%%%s%%", arg)) + } + } else if v, ok := expr.ExprKind.(*exprv1.Expr_IdentExpr); ok { + identifier := v.IdentExpr.GetName() + if !slices.Contains([]string{"pinned", "has_task_list"}, identifier) { + return errors.Errorf("invalid identifier %s", identifier) + } + if identifier == "pinned" { + if _, err := ctx.Buffer.WriteString(filter.GetSQL("table_prefix", dbType) + ".`pinned` IS TRUE"); err != nil { + return err + } + } else if identifier == "has_task_list" { + // Handle has_task_list as a standalone boolean identifier + if _, err := ctx.Buffer.WriteString(filter.GetSQL("boolean_check", dbType)); err != nil { + return err + } + } + } + return nil +} + +func (*DB) getComparisonOperator(function string) string { + switch function { + case "_==_": + return "=" + case "_!=_": + return "!=" + case "_<_": + return "<" + case "_>_": + return ">" + case "_<=_": + return "<=" + case "_>=_": + return ">=" + default: + return "=" + } +} diff --git a/store/db/mysql/memo_filter_test.go b/store/db/mysql/memo_filter_test.go new file mode 100644 index 0000000..b5dd090 --- /dev/null +++ b/store/db/mysql/memo_filter_test.go @@ -0,0 +1,130 @@ +package mysql + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/usememos/memos/plugin/filter" +) + +func TestConvertExprToSQL(t *testing.T) { + tests := []struct { + filter string + want string + args []any + }{ + { + filter: `tag in ["tag1", "tag2"]`, + want: "(JSON_CONTAINS(JSON_EXTRACT(`memo`.`payload`, '$.tags'), ?) OR JSON_CONTAINS(JSON_EXTRACT(`memo`.`payload`, '$.tags'), ?))", + args: []any{"tag1", "tag2"}, + }, + { + filter: `!(tag in ["tag1", "tag2"])`, + want: "NOT ((JSON_CONTAINS(JSON_EXTRACT(`memo`.`payload`, '$.tags'), ?) OR JSON_CONTAINS(JSON_EXTRACT(`memo`.`payload`, '$.tags'), ?)))", + args: []any{"tag1", "tag2"}, + }, + { + filter: `content.contains("memos")`, + want: "`memo`.`content` LIKE ?", + args: []any{"%memos%"}, + }, + { + filter: `visibility in ["PUBLIC"]`, + want: "`memo`.`visibility` IN (?)", + args: []any{"PUBLIC"}, + }, + { + filter: `visibility in ["PUBLIC", "PRIVATE"]`, + want: "`memo`.`visibility` IN (?,?)", + args: []any{"PUBLIC", "PRIVATE"}, + }, + { + filter: `tag in ['tag1'] || content.contains('hello')`, + want: "(JSON_CONTAINS(JSON_EXTRACT(`memo`.`payload`, '$.tags'), ?) OR `memo`.`content` LIKE ?)", + args: []any{"tag1", "%hello%"}, + }, + { + filter: `1`, + want: "", + args: []any{}, + }, + { + filter: `pinned`, + want: "`memo`.`pinned` IS TRUE", + args: []any{}, + }, + { + filter: `has_task_list`, + want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = CAST('true' AS JSON)", + args: []any{}, + }, + { + filter: `has_task_list == true`, + want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = CAST('true' AS JSON)", + args: []any{}, + }, + { + filter: `has_task_list != false`, + want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') != CAST('false' AS JSON)", + args: []any{}, + }, + { + filter: `has_task_list == false`, + want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = CAST('false' AS JSON)", + args: []any{}, + }, + { + filter: `!has_task_list`, + want: "NOT (JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = CAST('true' AS JSON))", + args: []any{}, + }, + { + filter: `has_task_list && pinned`, + want: "(JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = CAST('true' AS JSON) AND `memo`.`pinned` IS TRUE)", + args: []any{}, + }, + { + filter: `has_task_list && content.contains("todo")`, + want: "(JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = CAST('true' AS JSON) AND `memo`.`content` LIKE ?)", + args: []any{"%todo%"}, + }, + { + filter: `created_ts > now() - 60 * 60 * 24`, + want: "UNIX_TIMESTAMP(`memo`.`created_ts`) > ?", + args: []any{time.Now().Unix() - 60*60*24}, + }, + { + filter: `size(tags) == 0`, + want: "JSON_LENGTH(COALESCE(JSON_EXTRACT(`memo`.`payload`, '$.tags'), JSON_ARRAY())) = ?", + args: []any{int64(0)}, + }, + { + filter: `size(tags) > 0`, + want: "JSON_LENGTH(COALESCE(JSON_EXTRACT(`memo`.`payload`, '$.tags'), JSON_ARRAY())) > ?", + args: []any{int64(0)}, + }, + { + filter: `"work" in tags`, + want: "JSON_CONTAINS(JSON_EXTRACT(`memo`.`payload`, '$.tags'), ?)", + args: []any{"work"}, + }, + { + filter: `size(tags) == 2`, + want: "JSON_LENGTH(COALESCE(JSON_EXTRACT(`memo`.`payload`, '$.tags'), JSON_ARRAY())) = ?", + args: []any{int64(2)}, + }, + } + + for _, tt := range tests { + db := &DB{} + parsedExpr, err := filter.Parse(tt.filter, filter.MemoFilterCELAttributes...) + require.NoError(t, err) + convertCtx := filter.NewConvertContext() + err = db.ConvertExprToSQL(convertCtx, parsedExpr.GetExpr()) + require.NoError(t, err) + require.Equal(t, tt.want, convertCtx.Buffer.String()) + require.Equal(t, tt.args, convertCtx.Args) + } +} diff --git a/store/db/mysql/memo_relation.go b/store/db/mysql/memo_relation.go new file mode 100644 index 0000000..8952488 --- /dev/null +++ b/store/db/mysql/memo_relation.go @@ -0,0 +1,111 @@ +package mysql + +import ( + "context" + "fmt" + "strings" + + "github.com/usememos/memos/plugin/filter" + "github.com/usememos/memos/store" +) + +func (d *DB) UpsertMemoRelation(ctx context.Context, create *store.MemoRelation) (*store.MemoRelation, error) { + stmt := "INSERT INTO `memo_relation` (`memo_id`, `related_memo_id`, `type`) VALUES (?, ?, ?)" + _, err := d.db.ExecContext( + ctx, + stmt, + create.MemoID, + create.RelatedMemoID, + create.Type, + ) + if err != nil { + return nil, err + } + + memoRelation := store.MemoRelation{ + MemoID: create.MemoID, + RelatedMemoID: create.RelatedMemoID, + Type: create.Type, + } + + return &memoRelation, nil +} + +func (d *DB) ListMemoRelations(ctx context.Context, find *store.FindMemoRelation) ([]*store.MemoRelation, error) { + where, args := []string{"TRUE"}, []any{} + if find.MemoID != nil { + where, args = append(where, "`memo_id` = ?"), append(args, find.MemoID) + } + if find.RelatedMemoID != nil { + where, args = append(where, "`related_memo_id` = ?"), append(args, find.RelatedMemoID) + } + if find.Type != nil { + where, args = append(where, "`type` = ?"), append(args, find.Type) + } + if find.MemoFilter != nil { + // Parse filter string and return the parsed expression. + // The filter string should be a CEL expression. + parsedExpr, err := filter.Parse(*find.MemoFilter, filter.MemoFilterCELAttributes...) + if err != nil { + return nil, err + } + convertCtx := filter.NewConvertContext() + // ConvertExprToSQL converts the parsed expression to a SQL condition string. + if err := d.ConvertExprToSQL(convertCtx, parsedExpr.GetExpr()); err != nil { + return nil, err + } + condition := convertCtx.Buffer.String() + if condition != "" { + where = append(where, fmt.Sprintf("memo_id IN (SELECT id FROM memo WHERE %s)", condition)) + where = append(where, fmt.Sprintf("related_memo_id IN (SELECT id FROM memo WHERE %s)", condition)) + args = append(args, append(convertCtx.Args, convertCtx.Args...)...) + } + } + + rows, err := d.db.QueryContext(ctx, "SELECT `memo_id`, `related_memo_id`, `type` FROM `memo_relation` WHERE "+strings.Join(where, " AND "), args...) + if err != nil { + return nil, err + } + defer rows.Close() + + list := []*store.MemoRelation{} + for rows.Next() { + memoRelation := &store.MemoRelation{} + if err := rows.Scan( + &memoRelation.MemoID, + &memoRelation.RelatedMemoID, + &memoRelation.Type, + ); err != nil { + return nil, err + } + list = append(list, memoRelation) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} + +func (d *DB) DeleteMemoRelation(ctx context.Context, delete *store.DeleteMemoRelation) error { + where, args := []string{"TRUE"}, []any{} + if delete.MemoID != nil { + where, args = append(where, "`memo_id` = ?"), append(args, delete.MemoID) + } + if delete.RelatedMemoID != nil { + where, args = append(where, "`related_memo_id` = ?"), append(args, delete.RelatedMemoID) + } + if delete.Type != nil { + where, args = append(where, "`type` = ?"), append(args, delete.Type) + } + stmt := "DELETE FROM `memo_relation` WHERE " + strings.Join(where, " AND ") + result, err := d.db.ExecContext(ctx, stmt, args...) + if err != nil { + return err + } + if _, err = result.RowsAffected(); err != nil { + return err + } + return nil +} diff --git a/store/db/mysql/migration_history.go b/store/db/mysql/migration_history.go new file mode 100644 index 0000000..bc6c89f --- /dev/null +++ b/store/db/mysql/migration_history.go @@ -0,0 +1,53 @@ +package mysql + +import ( + "context" + + "github.com/usememos/memos/store" +) + +func (d *DB) FindMigrationHistoryList(ctx context.Context, _ *store.FindMigrationHistory) ([]*store.MigrationHistory, error) { + query := "SELECT `version`, UNIX_TIMESTAMP(`created_ts`) FROM `migration_history` ORDER BY `created_ts` DESC" + rows, err := d.db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + list := make([]*store.MigrationHistory, 0) + for rows.Next() { + var migrationHistory store.MigrationHistory + if err := rows.Scan( + &migrationHistory.Version, + &migrationHistory.CreatedTs, + ); err != nil { + return nil, err + } + + list = append(list, &migrationHistory) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} + +func (d *DB) UpsertMigrationHistory(ctx context.Context, upsert *store.UpsertMigrationHistory) (*store.MigrationHistory, error) { + stmt := "INSERT INTO `migration_history` (`version`) VALUES (?) ON DUPLICATE KEY UPDATE `version` = ?" + _, err := d.db.ExecContext(ctx, stmt, upsert.Version, upsert.Version) + if err != nil { + return nil, err + } + + var migrationHistory store.MigrationHistory + stmt = "SELECT `version`, UNIX_TIMESTAMP(`created_ts`) FROM `migration_history` WHERE `version` = ?" + if err := d.db.QueryRowContext(ctx, stmt, upsert.Version).Scan( + &migrationHistory.Version, + &migrationHistory.CreatedTs, + ); err != nil { + return nil, err + } + return &migrationHistory, nil +} diff --git a/store/db/mysql/mysql.go b/store/db/mysql/mysql.go new file mode 100644 index 0000000..2862d87 --- /dev/null +++ b/store/db/mysql/mysql.go @@ -0,0 +1,68 @@ +package mysql + +import ( + "context" + "database/sql" + + "github.com/go-sql-driver/mysql" + "github.com/pkg/errors" + + "github.com/usememos/memos/internal/profile" + "github.com/usememos/memos/store" +) + +type DB struct { + db *sql.DB + profile *profile.Profile + config *mysql.Config +} + +func NewDB(profile *profile.Profile) (store.Driver, error) { + // Open MySQL connection with parameter. + // multiStatements=true is required for migration. + // See more in: https://github.com/go-sql-driver/mysql#multistatements + dsn, err := mergeDSN(profile.DSN) + if err != nil { + return nil, err + } + + driver := DB{profile: profile} + driver.config, err = mysql.ParseDSN(dsn) + if err != nil { + return nil, errors.New("Parse DSN eroor") + } + + driver.db, err = sql.Open("mysql", dsn) + if err != nil { + return nil, errors.Wrapf(err, "failed to open db: %s", profile.DSN) + } + + return &driver, nil +} + +func (d *DB) GetDB() *sql.DB { + return d.db +} + +func (d *DB) Close() error { + return d.db.Close() +} + +func (d *DB) IsInitialized(ctx context.Context) (bool, error) { + var exists bool + err := d.db.QueryRowContext(ctx, "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE TABLE_NAME = 'memo' AND TABLE_TYPE = 'BASE TABLE')").Scan(&exists) + if err != nil { + return false, errors.Wrap(err, "failed to check if database is initialized") + } + return exists, nil +} + +func mergeDSN(baseDSN string) (string, error) { + config, err := mysql.ParseDSN(baseDSN) + if err != nil { + return "", errors.Wrapf(err, "failed to parse DSN: %s", baseDSN) + } + + config.MultiStatements = true + return config.FormatDSN(), nil +} diff --git a/store/db/mysql/reaction.go b/store/db/mysql/reaction.go new file mode 100644 index 0000000..d59937e --- /dev/null +++ b/store/db/mysql/reaction.go @@ -0,0 +1,104 @@ +package mysql + +import ( + "context" + "strings" + + "github.com/pkg/errors" + + "github.com/usememos/memos/store" +) + +func (d *DB) UpsertReaction(ctx context.Context, upsert *store.Reaction) (*store.Reaction, error) { + fields := []string{"`creator_id`", "`content_id`", "`reaction_type`"} + placeholder := []string{"?", "?", "?"} + args := []interface{}{upsert.CreatorID, upsert.ContentID, upsert.ReactionType} + stmt := "INSERT INTO `reaction` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ")" + result, err := d.db.ExecContext(ctx, stmt, args...) + if err != nil { + return nil, err + } + + rawID, err := result.LastInsertId() + if err != nil { + return nil, err + } + id := int32(rawID) + reaction, err := d.GetReaction(ctx, &store.FindReaction{ID: &id}) + if err != nil { + return nil, err + } + if reaction == nil { + return nil, errors.Errorf("failed to create reaction") + } + return reaction, nil +} + +func (d *DB) ListReactions(ctx context.Context, find *store.FindReaction) ([]*store.Reaction, error) { + where, args := []string{"1 = 1"}, []interface{}{} + if find.ID != nil { + where, args = append(where, "`id` = ?"), append(args, *find.ID) + } + if find.CreatorID != nil { + where, args = append(where, "`creator_id` = ?"), append(args, *find.CreatorID) + } + if find.ContentID != nil { + where, args = append(where, "`content_id` = ?"), append(args, *find.ContentID) + } + + rows, err := d.db.QueryContext(ctx, ` + SELECT + id, + UNIX_TIMESTAMP(created_ts) AS created_ts, + creator_id, + content_id, + reaction_type + FROM reaction + WHERE `+strings.Join(where, " AND ")+` + ORDER BY id ASC`, + args..., + ) + if err != nil { + return nil, err + } + defer rows.Close() + + list := []*store.Reaction{} + for rows.Next() { + reaction := &store.Reaction{} + if err := rows.Scan( + &reaction.ID, + &reaction.CreatedTs, + &reaction.CreatorID, + &reaction.ContentID, + &reaction.ReactionType, + ); err != nil { + return nil, err + } + list = append(list, reaction) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} + +func (d *DB) GetReaction(ctx context.Context, find *store.FindReaction) (*store.Reaction, error) { + list, err := d.ListReactions(ctx, find) + if err != nil { + return nil, err + } + if len(list) == 0 { + return nil, nil + } + + reaction := list[0] + return reaction, nil +} + +func (d *DB) DeleteReaction(ctx context.Context, delete *store.DeleteReaction) error { + _, err := d.db.ExecContext(ctx, "DELETE FROM `reaction` WHERE `id` = ?", delete.ID) + return err +} diff --git a/store/db/mysql/user.go b/store/db/mysql/user.go new file mode 100644 index 0000000..0db9dda --- /dev/null +++ b/store/db/mysql/user.go @@ -0,0 +1,162 @@ +package mysql + +import ( + "context" + "fmt" + "strings" + + "github.com/pkg/errors" + + "github.com/usememos/memos/store" +) + +func (d *DB) CreateUser(ctx context.Context, create *store.User) (*store.User, error) { + fields := []string{"`username`", "`role`", "`email`", "`nickname`", "`password_hash`", "`avatar_url`"} + placeholder := []string{"?", "?", "?", "?", "?", "?"} + args := []any{create.Username, create.Role, create.Email, create.Nickname, create.PasswordHash, create.AvatarURL} + + stmt := "INSERT INTO user (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ")" + result, err := d.db.ExecContext(ctx, stmt, args...) + if err != nil { + return nil, err + } + + id, err := result.LastInsertId() + if err != nil { + return nil, err + } + + id32 := int32(id) + list, err := d.ListUsers(ctx, &store.FindUser{ID: &id32}) + if err != nil { + return nil, err + } + if len(list) != 1 { + return nil, errors.Errorf("unexpected user count: %d", len(list)) + } + + return list[0], nil +} + +func (d *DB) UpdateUser(ctx context.Context, update *store.UpdateUser) (*store.User, error) { + set, args := []string{}, []any{} + if v := update.UpdatedTs; v != nil { + set, args = append(set, "`updated_ts` = FROM_UNIXTIME(?)"), append(args, *v) + } + if v := update.RowStatus; v != nil { + set, args = append(set, "`row_status` = ?"), append(args, *v) + } + if v := update.Username; v != nil { + set, args = append(set, "`username` = ?"), append(args, *v) + } + if v := update.Email; v != nil { + set, args = append(set, "`email` = ?"), append(args, *v) + } + if v := update.Nickname; v != nil { + set, args = append(set, "`nickname` = ?"), append(args, *v) + } + if v := update.AvatarURL; v != nil { + set, args = append(set, "`avatar_url` = ?"), append(args, *v) + } + if v := update.PasswordHash; v != nil { + set, args = append(set, "`password_hash` = ?"), append(args, *v) + } + if v := update.Description; v != nil { + set, args = append(set, "`description` = ?"), append(args, *v) + } + if v := update.Role; v != nil { + set, args = append(set, "`role` = ?"), append(args, *v) + } + args = append(args, update.ID) + + query := "UPDATE `user` SET " + strings.Join(set, ", ") + " WHERE `id` = ?" + if _, err := d.db.ExecContext(ctx, query, args...); err != nil { + return nil, err + } + + user, err := d.GetUser(ctx, &store.FindUser{ID: &update.ID}) + if err != nil { + return nil, err + } + return user, nil +} + +func (d *DB) ListUsers(ctx context.Context, find *store.FindUser) ([]*store.User, error) { + where, args := []string{"1 = 1"}, []any{} + + if v := find.ID; v != nil { + where, args = append(where, "`id` = ?"), append(args, *v) + } + if v := find.Username; v != nil { + where, args = append(where, "`username` = ?"), append(args, *v) + } + if v := find.Role; v != nil { + where, args = append(where, "`role` = ?"), append(args, *v) + } + if v := find.Email; v != nil { + where, args = append(where, "`email` = ?"), append(args, *v) + } + if v := find.Nickname; v != nil { + where, args = append(where, "`nickname` = ?"), append(args, *v) + } + + orderBy := []string{"`created_ts` DESC", "`row_status` DESC"} + query := "SELECT `id`, `username`, `role`, `email`, `nickname`, `password_hash`, `avatar_url`, `description`, UNIX_TIMESTAMP(`created_ts`), UNIX_TIMESTAMP(`updated_ts`), `row_status` FROM `user` WHERE " + strings.Join(where, " AND ") + " ORDER BY " + strings.Join(orderBy, ", ") + if v := find.Limit; v != nil { + query += fmt.Sprintf(" LIMIT %d", *v) + } + rows, err := d.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + list := make([]*store.User, 0) + for rows.Next() { + var user store.User + if err := rows.Scan( + &user.ID, + &user.Username, + &user.Role, + &user.Email, + &user.Nickname, + &user.PasswordHash, + &user.AvatarURL, + &user.Description, + &user.CreatedTs, + &user.UpdatedTs, + &user.RowStatus, + ); err != nil { + return nil, err + } + list = append(list, &user) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} + +func (d *DB) GetUser(ctx context.Context, find *store.FindUser) (*store.User, error) { + list, err := d.ListUsers(ctx, find) + if err != nil { + return nil, err + } + if len(list) != 1 { + return nil, errors.Errorf("unexpected user count: %d", len(list)) + } + return list[0], nil +} + +func (d *DB) DeleteUser(ctx context.Context, delete *store.DeleteUser) error { + result, err := d.db.ExecContext(ctx, "DELETE FROM `user` WHERE `id` = ?", delete.ID) + if err != nil { + return err + } + if _, err := result.RowsAffected(); err != nil { + return err + } + return nil +} diff --git a/store/db/mysql/user_setting.go b/store/db/mysql/user_setting.go new file mode 100644 index 0000000..d51228b --- /dev/null +++ b/store/db/mysql/user_setting.go @@ -0,0 +1,56 @@ +package mysql + +import ( + "context" + "strings" + + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func (d *DB) UpsertUserSetting(ctx context.Context, upsert *store.UserSetting) (*store.UserSetting, error) { + stmt := "INSERT INTO `user_setting` (`user_id`, `key`, `value`) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE `value` = ?" + if _, err := d.db.ExecContext(ctx, stmt, upsert.UserID, upsert.Key.String(), upsert.Value, upsert.Value); err != nil { + return nil, err + } + return upsert, nil +} + +func (d *DB) ListUserSettings(ctx context.Context, find *store.FindUserSetting) ([]*store.UserSetting, error) { + where, args := []string{"1 = 1"}, []any{} + + if v := find.Key; v != storepb.UserSetting_KEY_UNSPECIFIED { + where, args = append(where, "`key` = ?"), append(args, v.String()) + } + if v := find.UserID; v != nil { + where, args = append(where, "`user_id` = ?"), append(args, *find.UserID) + } + + query := "SELECT `user_id`, `key`, `value` FROM `user_setting` WHERE " + strings.Join(where, " AND ") + rows, err := d.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + userSettingList := make([]*store.UserSetting, 0) + for rows.Next() { + userSetting := &store.UserSetting{} + var keyString string + if err := rows.Scan( + &userSetting.UserID, + &keyString, + &userSetting.Value, + ); err != nil { + return nil, err + } + userSetting.Key = storepb.UserSetting_Key(storepb.UserSetting_Key_value[keyString]) + userSettingList = append(userSettingList, userSetting) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return userSettingList, nil +} diff --git a/store/db/mysql/workspace_setting.go b/store/db/mysql/workspace_setting.go new file mode 100644 index 0000000..0f779d6 --- /dev/null +++ b/store/db/mysql/workspace_setting.go @@ -0,0 +1,65 @@ +package mysql + +import ( + "context" + "strings" + + "github.com/usememos/memos/store" +) + +func (d *DB) UpsertWorkspaceSetting(ctx context.Context, upsert *store.WorkspaceSetting) (*store.WorkspaceSetting, error) { + stmt := "INSERT INTO `system_setting` (`name`, `value`, `description`) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE `value` = ?, `description` = ?" + _, err := d.db.ExecContext( + ctx, + stmt, + upsert.Name, + upsert.Value, + upsert.Description, + upsert.Value, + upsert.Description, + ) + if err != nil { + return nil, err + } + + return upsert, nil +} + +func (d *DB) ListWorkspaceSettings(ctx context.Context, find *store.FindWorkspaceSetting) ([]*store.WorkspaceSetting, error) { + where, args := []string{"1 = 1"}, []any{} + if find.Name != "" { + where, args = append(where, "`name` = ?"), append(args, find.Name) + } + + query := "SELECT `name`, `value`, `description` FROM `system_setting` WHERE " + strings.Join(where, " AND ") + rows, err := d.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + list := []*store.WorkspaceSetting{} + for rows.Next() { + systemSettingMessage := &store.WorkspaceSetting{} + if err := rows.Scan( + &systemSettingMessage.Name, + &systemSettingMessage.Value, + &systemSettingMessage.Description, + ); err != nil { + return nil, err + } + list = append(list, systemSettingMessage) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} + +func (d *DB) DeleteWorkspaceSetting(ctx context.Context, delete *store.DeleteWorkspaceSetting) error { + stmt := "DELETE FROM `system_setting` WHERE `name` = ?" + _, err := d.db.ExecContext(ctx, stmt, delete.Name) + return err +} diff --git a/store/db/postgres/activity.go b/store/db/postgres/activity.go new file mode 100644 index 0000000..84e1532 --- /dev/null +++ b/store/db/postgres/activity.go @@ -0,0 +1,81 @@ +package postgres + +import ( + "context" + "strings" + + "github.com/pkg/errors" + "google.golang.org/protobuf/encoding/protojson" + + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func (d *DB) CreateActivity(ctx context.Context, create *store.Activity) (*store.Activity, error) { + payloadString := "{}" + if create.Payload != nil { + bytes, err := protojson.Marshal(create.Payload) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal activity payload") + } + payloadString = string(bytes) + } + + fields := []string{"creator_id", "type", "level", "payload"} + args := []any{create.CreatorID, create.Type.String(), create.Level.String(), payloadString} + stmt := "INSERT INTO activity (" + strings.Join(fields, ", ") + ") VALUES (" + placeholders(len(args)) + ") RETURNING id, created_ts" + if err := d.db.QueryRowContext(ctx, stmt, args...).Scan( + &create.ID, + &create.CreatedTs, + ); err != nil { + return nil, err + } + + return create, nil +} + +func (d *DB) ListActivities(ctx context.Context, find *store.FindActivity) ([]*store.Activity, error) { + where, args := []string{"1 = 1"}, []any{} + if find.ID != nil { + where, args = append(where, "id = "+placeholder(len(args)+1)), append(args, *find.ID) + } + if find.Type != nil { + where, args = append(where, "type = "+placeholder(len(args)+1)), append(args, find.Type.String()) + } + + query := "SELECT id, creator_id, type, level, payload, created_ts FROM activity WHERE " + strings.Join(where, " AND ") + " ORDER BY created_ts DESC" + rows, err := d.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + list := []*store.Activity{} + for rows.Next() { + activity := &store.Activity{} + var payloadBytes []byte + if err := rows.Scan( + &activity.ID, + &activity.CreatorID, + &activity.Type, + &activity.Level, + &payloadBytes, + &activity.CreatedTs, + ); err != nil { + return nil, err + } + + payload := &storepb.ActivityPayload{} + if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil { + return nil, err + } + activity.Payload = payload + list = append(list, activity) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} diff --git a/store/db/postgres/attachment.go b/store/db/postgres/attachment.go new file mode 100644 index 0000000..da24c37 --- /dev/null +++ b/store/db/postgres/attachment.go @@ -0,0 +1,186 @@ +package postgres + +import ( + "context" + "database/sql" + "fmt" + "strings" + + "github.com/pkg/errors" + "google.golang.org/protobuf/encoding/protojson" + + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func (d *DB) CreateAttachment(ctx context.Context, create *store.Attachment) (*store.Attachment, error) { + fields := []string{"uid", "filename", "blob", "type", "size", "creator_id", "memo_id", "storage_type", "reference", "payload"} + storageType := "" + if create.StorageType != storepb.AttachmentStorageType_ATTACHMENT_STORAGE_TYPE_UNSPECIFIED { + storageType = create.StorageType.String() + } + payloadString := "{}" + if create.Payload != nil { + bytes, err := protojson.Marshal(create.Payload) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal attachment payload") + } + payloadString = string(bytes) + } + args := []any{create.UID, create.Filename, create.Blob, create.Type, create.Size, create.CreatorID, create.MemoID, storageType, create.Reference, payloadString} + + stmt := "INSERT INTO resource (" + strings.Join(fields, ", ") + ") VALUES (" + placeholders(len(args)) + ") RETURNING id, created_ts, updated_ts" + if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(&create.ID, &create.CreatedTs, &create.UpdatedTs); err != nil { + return nil, err + } + return create, nil +} + +func (d *DB) ListAttachments(ctx context.Context, find *store.FindAttachment) ([]*store.Attachment, error) { + where, args := []string{"1 = 1"}, []any{} + + if v := find.ID; v != nil { + where, args = append(where, "id = "+placeholder(len(args)+1)), append(args, *v) + } + if v := find.UID; v != nil { + where, args = append(where, "uid = "+placeholder(len(args)+1)), append(args, *v) + } + if v := find.CreatorID; v != nil { + where, args = append(where, "creator_id = "+placeholder(len(args)+1)), append(args, *v) + } + if v := find.Filename; v != nil { + where, args = append(where, "filename = "+placeholder(len(args)+1)), append(args, *v) + } + if v := find.FilenameSearch; v != nil { + where, args = append(where, "filename LIKE "+placeholder(len(args)+1)), append(args, fmt.Sprintf("%%%s%%", *v)) + } + if v := find.MemoID; v != nil { + where, args = append(where, "memo_id = "+placeholder(len(args)+1)), append(args, *v) + } + if find.HasRelatedMemo { + where = append(where, "memo_id IS NOT NULL") + } + if v := find.StorageType; v != nil { + where, args = append(where, "storage_type = "+placeholder(len(args)+1)), append(args, v.String()) + } + + fields := []string{"id", "uid", "filename", "type", "size", "creator_id", "created_ts", "updated_ts", "memo_id", "storage_type", "reference", "payload"} + if find.GetBlob { + fields = append(fields, "blob") + } + + query := fmt.Sprintf(` + SELECT + %s + FROM resource + WHERE %s + ORDER BY updated_ts DESC + `, strings.Join(fields, ", "), strings.Join(where, " AND ")) + if find.Limit != nil { + query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit) + if find.Offset != nil { + query = fmt.Sprintf("%s OFFSET %d", query, *find.Offset) + } + } + + rows, err := d.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + list := make([]*store.Attachment, 0) + for rows.Next() { + attachment := store.Attachment{} + var memoID sql.NullInt32 + var storageType string + var payloadBytes []byte + dests := []any{ + &attachment.ID, + &attachment.UID, + &attachment.Filename, + &attachment.Type, + &attachment.Size, + &attachment.CreatorID, + &attachment.CreatedTs, + &attachment.UpdatedTs, + &memoID, + &storageType, + &attachment.Reference, + &payloadBytes, + } + if find.GetBlob { + dests = append(dests, &attachment.Blob) + } + if err := rows.Scan(dests...); err != nil { + return nil, err + } + + if memoID.Valid { + attachment.MemoID = &memoID.Int32 + } + attachment.StorageType = storepb.AttachmentStorageType(storepb.AttachmentStorageType_value[storageType]) + payload := &storepb.AttachmentPayload{} + if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil { + return nil, err + } + attachment.Payload = payload + list = append(list, &attachment) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} + +func (d *DB) UpdateAttachment(ctx context.Context, update *store.UpdateAttachment) error { + set, args := []string{}, []any{} + + if v := update.UID; v != nil { + set, args = append(set, "uid = "+placeholder(len(args)+1)), append(args, *v) + } + if v := update.UpdatedTs; v != nil { + set, args = append(set, "updated_ts = "+placeholder(len(args)+1)), append(args, *v) + } + if v := update.Filename; v != nil { + set, args = append(set, "filename = "+placeholder(len(args)+1)), append(args, *v) + } + if v := update.MemoID; v != nil { + set, args = append(set, "memo_id = "+placeholder(len(args)+1)), append(args, *v) + } + if v := update.Reference; v != nil { + set, args = append(set, "reference = "+placeholder(len(args)+1)), append(args, *v) + } + if v := update.Payload; v != nil { + bytes, err := protojson.Marshal(v) + if err != nil { + return errors.Wrap(err, "failed to marshal attachment payload") + } + set, args = append(set, "payload = "+placeholder(len(args)+1)), append(args, string(bytes)) + } + + stmt := `UPDATE resource SET ` + strings.Join(set, ", ") + ` WHERE id = ` + placeholder(len(args)+1) + args = append(args, update.ID) + result, err := d.db.ExecContext(ctx, stmt, args...) + if err != nil { + return err + } + if _, err := result.RowsAffected(); err != nil { + return err + } + return nil +} + +func (d *DB) DeleteAttachment(ctx context.Context, delete *store.DeleteAttachment) error { + stmt := `DELETE FROM resource WHERE id = $1` + result, err := d.db.ExecContext(ctx, stmt, delete.ID) + if err != nil { + return err + } + if _, err := result.RowsAffected(); err != nil { + return err + } + return nil +} diff --git a/store/db/postgres/common.go b/store/db/postgres/common.go new file mode 100644 index 0000000..b4f074d --- /dev/null +++ b/store/db/postgres/common.go @@ -0,0 +1,26 @@ +package postgres + +import ( + "fmt" + "strings" + + "google.golang.org/protobuf/encoding/protojson" +) + +var ( + protojsonUnmarshaler = protojson.UnmarshalOptions{ + DiscardUnknown: true, + } +) + +func placeholder(n int) string { + return "$" + fmt.Sprint(n) +} + +func placeholders(n int) string { + list := []string{} + for i := 0; i < n; i++ { + list = append(list, placeholder(i+1)) + } + return strings.Join(list, ", ") +} diff --git a/store/db/postgres/idp.go b/store/db/postgres/idp.go new file mode 100644 index 0000000..34a1165 --- /dev/null +++ b/store/db/postgres/idp.go @@ -0,0 +1,117 @@ +package postgres + +import ( + "context" + "strings" + + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func (d *DB) CreateIdentityProvider(ctx context.Context, create *store.IdentityProvider) (*store.IdentityProvider, error) { + fields := []string{"name", "type", "identifier_filter", "config"} + args := []any{create.Name, create.Type.String(), create.IdentifierFilter, create.Config} + stmt := "INSERT INTO idp (" + strings.Join(fields, ", ") + ") VALUES (" + placeholders(len(args)) + ") RETURNING id" + if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(&create.ID); err != nil { + return nil, err + } + + identityProvider := create + return identityProvider, nil +} + +func (d *DB) ListIdentityProviders(ctx context.Context, find *store.FindIdentityProvider) ([]*store.IdentityProvider, error) { + where, args := []string{"1 = 1"}, []any{} + if v := find.ID; v != nil { + where, args = append(where, "id = "+placeholder(len(args)+1)), append(args, *v) + } + + rows, err := d.db.QueryContext(ctx, ` + SELECT + id, + name, + type, + identifier_filter, + config + FROM idp + WHERE `+strings.Join(where, " AND ")+` ORDER BY id ASC`, + args..., + ) + if err != nil { + return nil, err + } + defer rows.Close() + + var identityProviders []*store.IdentityProvider + for rows.Next() { + var identityProvider store.IdentityProvider + var typeString string + if err := rows.Scan( + &identityProvider.ID, + &identityProvider.Name, + &typeString, + &identityProvider.IdentifierFilter, + &identityProvider.Config, + ); err != nil { + return nil, err + } + + identityProvider.Type = storepb.IdentityProvider_Type(storepb.IdentityProvider_Type_value[typeString]) + identityProviders = append(identityProviders, &identityProvider) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return identityProviders, nil +} + +func (d *DB) UpdateIdentityProvider(ctx context.Context, update *store.UpdateIdentityProvider) (*store.IdentityProvider, error) { + set, args := []string{}, []any{} + if v := update.Name; v != nil { + set, args = append(set, "name = "+placeholder(len(args)+1)), append(args, *v) + } + if v := update.IdentifierFilter; v != nil { + set, args = append(set, "identifier_filter = "+placeholder(len(args)+1)), append(args, *v) + } + if v := update.Config; v != nil { + set, args = append(set, "config = "+placeholder(len(args)+1)), append(args, *v) + } + + stmt := ` + UPDATE idp + SET ` + strings.Join(set, ", ") + ` + WHERE id = ` + placeholder(len(args)+1) + ` + RETURNING id, name, type, identifier_filter, config + ` + args = append(args, update.ID) + + var identityProvider store.IdentityProvider + var typeString string + if err := d.db.QueryRowContext(ctx, stmt, args...).Scan( + &identityProvider.ID, + &identityProvider.Name, + &typeString, + &identityProvider.IdentifierFilter, + &identityProvider.Config, + ); err != nil { + return nil, err + } + + identityProvider.Type = storepb.IdentityProvider_Type(storepb.IdentityProvider_Type_value[typeString]) + return &identityProvider, nil +} + +func (d *DB) DeleteIdentityProvider(ctx context.Context, delete *store.DeleteIdentityProvider) error { + where, args := []string{"id = $1"}, []any{delete.ID} + stmt := `DELETE FROM idp WHERE ` + strings.Join(where, " AND ") + result, err := d.db.ExecContext(ctx, stmt, args...) + if err != nil { + return err + } + if _, err = result.RowsAffected(); err != nil { + return err + } + return nil +} diff --git a/store/db/postgres/inbox.go b/store/db/postgres/inbox.go new file mode 100644 index 0000000..1777d80 --- /dev/null +++ b/store/db/postgres/inbox.go @@ -0,0 +1,141 @@ +package postgres + +import ( + "context" + "fmt" + "strings" + + "github.com/pkg/errors" + "google.golang.org/protobuf/encoding/protojson" + + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func (d *DB) CreateInbox(ctx context.Context, create *store.Inbox) (*store.Inbox, error) { + messageString := "{}" + if create.Message != nil { + bytes, err := protojson.Marshal(create.Message) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal inbox message") + } + messageString = string(bytes) + } + + fields := []string{"sender_id", "receiver_id", "status", "message"} + args := []any{create.SenderID, create.ReceiverID, create.Status, messageString} + stmt := "INSERT INTO inbox (" + strings.Join(fields, ", ") + ") VALUES (" + placeholders(len(args)) + ") RETURNING id, created_ts" + if err := d.db.QueryRowContext(ctx, stmt, args...).Scan( + &create.ID, + &create.CreatedTs, + ); err != nil { + return nil, err + } + + return create, nil +} + +func (d *DB) ListInboxes(ctx context.Context, find *store.FindInbox) ([]*store.Inbox, error) { + where, args := []string{"1 = 1"}, []any{} + + if find.ID != nil { + where, args = append(where, "id = "+placeholder(len(args)+1)), append(args, *find.ID) + } + if find.SenderID != nil { + where, args = append(where, "sender_id = "+placeholder(len(args)+1)), append(args, *find.SenderID) + } + if find.ReceiverID != nil { + where, args = append(where, "receiver_id = "+placeholder(len(args)+1)), append(args, *find.ReceiverID) + } + if find.Status != nil { + where, args = append(where, "status = "+placeholder(len(args)+1)), append(args, *find.Status) + } + + query := "SELECT id, created_ts, sender_id, receiver_id, status, message FROM inbox WHERE " + strings.Join(where, " AND ") + " ORDER BY created_ts DESC" + if find.Limit != nil { + query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit) + if find.Offset != nil { + query = fmt.Sprintf("%s OFFSET %d", query, *find.Offset) + } + } + rows, err := d.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + list := []*store.Inbox{} + for rows.Next() { + inbox := &store.Inbox{} + var messageBytes []byte + if err := rows.Scan( + &inbox.ID, + &inbox.CreatedTs, + &inbox.SenderID, + &inbox.ReceiverID, + &inbox.Status, + &messageBytes, + ); err != nil { + return nil, err + } + + message := &storepb.InboxMessage{} + if err := protojsonUnmarshaler.Unmarshal(messageBytes, message); err != nil { + return nil, err + } + inbox.Message = message + list = append(list, inbox) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} + +func (d *DB) GetInbox(ctx context.Context, find *store.FindInbox) (*store.Inbox, error) { + list, err := d.ListInboxes(ctx, find) + if err != nil { + return nil, errors.Wrap(err, "failed to get inbox") + } + if len(list) != 1 { + return nil, errors.Errorf("unexpected inbox count: %d", len(list)) + } + return list[0], nil +} + +func (d *DB) UpdateInbox(ctx context.Context, update *store.UpdateInbox) (*store.Inbox, error) { + set, args := []string{"status = $1"}, []any{update.Status.String()} + args = append(args, update.ID) + query := "UPDATE inbox SET " + strings.Join(set, ", ") + " WHERE id = $2 RETURNING id, created_ts, sender_id, receiver_id, status, message" + inbox := &store.Inbox{} + var messageBytes []byte + if err := d.db.QueryRowContext(ctx, query, args...).Scan( + &inbox.ID, + &inbox.CreatedTs, + &inbox.SenderID, + &inbox.ReceiverID, + &inbox.Status, + &messageBytes, + ); err != nil { + return nil, err + } + message := &storepb.InboxMessage{} + if err := protojsonUnmarshaler.Unmarshal(messageBytes, message); err != nil { + return nil, err + } + inbox.Message = message + return inbox, nil +} + +func (d *DB) DeleteInbox(ctx context.Context, delete *store.DeleteInbox) error { + result, err := d.db.ExecContext(ctx, "DELETE FROM inbox WHERE id = $1", delete.ID) + if err != nil { + return err + } + if _, err := result.RowsAffected(); err != nil { + return err + } + return nil +} diff --git a/store/db/postgres/memo.go b/store/db/postgres/memo.go new file mode 100644 index 0000000..4ec66df --- /dev/null +++ b/store/db/postgres/memo.go @@ -0,0 +1,279 @@ +package postgres + +import ( + "context" + "fmt" + "strings" + + "github.com/pkg/errors" + "google.golang.org/protobuf/encoding/protojson" + + "github.com/usememos/memos/plugin/filter" + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func (d *DB) CreateMemo(ctx context.Context, create *store.Memo) (*store.Memo, error) { + fields := []string{"uid", "creator_id", "content", "visibility", "payload"} + payload := "{}" + if create.Payload != nil { + payloadBytes, err := protojson.Marshal(create.Payload) + if err != nil { + return nil, err + } + payload = string(payloadBytes) + } + args := []any{create.UID, create.CreatorID, create.Content, create.Visibility, payload} + + stmt := "INSERT INTO memo (" + strings.Join(fields, ", ") + ") VALUES (" + placeholders(len(args)) + ") RETURNING id, created_ts, updated_ts, row_status" + if err := d.db.QueryRowContext(ctx, stmt, args...).Scan( + &create.ID, + &create.CreatedTs, + &create.UpdatedTs, + &create.RowStatus, + ); err != nil { + return nil, err + } + + return create, nil +} + +func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo, error) { + where, args := []string{"1 = 1"}, []any{} + + if v := find.ID; v != nil { + where, args = append(where, "memo.id = "+placeholder(len(args)+1)), append(args, *v) + } + if v := find.UID; v != nil { + where, args = append(where, "memo.uid = "+placeholder(len(args)+1)), append(args, *v) + } + if v := find.CreatorID; v != nil { + where, args = append(where, "memo.creator_id = "+placeholder(len(args)+1)), append(args, *v) + } + if v := find.RowStatus; v != nil { + where, args = append(where, "memo.row_status = "+placeholder(len(args)+1)), append(args, *v) + } + if v := find.CreatedTsBefore; v != nil { + where, args = append(where, "memo.created_ts < "+placeholder(len(args)+1)), append(args, *v) + } + if v := find.CreatedTsAfter; v != nil { + where, args = append(where, "memo.created_ts > "+placeholder(len(args)+1)), append(args, *v) + } + if v := find.UpdatedTsBefore; v != nil { + where, args = append(where, "memo.updated_ts < "+placeholder(len(args)+1)), append(args, *v) + } + if v := find.UpdatedTsAfter; v != nil { + where, args = append(where, "memo.updated_ts > "+placeholder(len(args)+1)), append(args, *v) + } + if v := find.ContentSearch; len(v) != 0 { + for _, s := range v { + where, args = append(where, "memo.content ILIKE "+placeholder(len(args)+1)), append(args, fmt.Sprintf("%%%s%%", s)) + } + } + if v := find.VisibilityList; len(v) != 0 { + holders := []string{} + for _, visibility := range v { + holders = append(holders, placeholder(len(args)+1)) + args = append(args, visibility.String()) + } + where = append(where, fmt.Sprintf("memo.visibility in (%s)", strings.Join(holders, ", "))) + } + if v := find.Pinned; v != nil { + where, args = append(where, "memo.pinned = "+placeholder(len(args)+1)), append(args, *v) + } + if v := find.PayloadFind; v != nil { + if v.Raw != nil { + where, args = append(where, "memo.payload = "+placeholder(len(args)+1)), append(args, *v.Raw) + } + if len(v.TagSearch) != 0 { + for _, tag := range v.TagSearch { + where, args = append(where, "EXISTS (SELECT 1 FROM jsonb_array_elements(memo.payload->'tags') AS tag WHERE tag::text = "+placeholder(len(args)+1)+" OR tag::text LIKE "+placeholder(len(args)+2)+")"), append(args, fmt.Sprintf(`"%s"`, tag), fmt.Sprintf(`"%s/%%"`, tag)) + } + } + if v.HasLink { + where = append(where, "(memo.payload->'property'->>'hasLink')::BOOLEAN IS TRUE") + } + if v.HasTaskList { + where = append(where, "(memo.payload->'property'->>'hasTaskList')::BOOLEAN IS TRUE") + } + if v.HasCode { + where = append(where, "(memo.payload->'property'->>'hasCode')::BOOLEAN IS TRUE") + } + if v.HasIncompleteTasks { + where = append(where, "(memo.payload->'property'->>'hasIncompleteTasks')::BOOLEAN IS TRUE") + } + } + if v := find.Filter; v != nil { + // Parse filter string and return the parsed expression. + // The filter string should be a CEL expression. + parsedExpr, err := filter.Parse(*v, filter.MemoFilterCELAttributes...) + if err != nil { + return nil, err + } + convertCtx := filter.NewConvertContext() + convertCtx.ArgsOffset = len(args) + // ConvertExprToSQL converts the parsed expression to a SQL condition string. + if err := d.ConvertExprToSQL(convertCtx, parsedExpr.GetExpr()); err != nil { + return nil, err + } + condition := convertCtx.Buffer.String() + if condition != "" { + where = append(where, fmt.Sprintf("(%s)", condition)) + args = append(args, convertCtx.Args...) + } + } + if find.ExcludeComments { + where = append(where, "memo_relation.related_memo_id IS NULL") + } + + order := "DESC" + if find.OrderByTimeAsc { + order = "ASC" + } + orderBy := []string{} + if find.OrderByPinned { + orderBy = append(orderBy, "pinned DESC") + } + if find.OrderByUpdatedTs { + orderBy = append(orderBy, "updated_ts "+order) + } else { + orderBy = append(orderBy, "created_ts "+order) + } + fields := []string{ + `memo.id AS id`, + `memo.uid AS uid`, + `memo.creator_id AS creator_id`, + `memo.created_ts AS created_ts`, + `memo.updated_ts AS updated_ts`, + `memo.row_status AS row_status`, + `memo.visibility AS visibility`, + `memo.pinned AS pinned`, + `memo.payload AS payload`, + `memo_relation.related_memo_id AS parent_id`, + } + if !find.ExcludeContent { + fields = append(fields, `memo.content AS content`) + } + + query := `SELECT ` + strings.Join(fields, ", ") + ` + FROM memo + LEFT JOIN memo_relation ON memo.id = memo_relation.memo_id AND memo_relation.type = 'COMMENT' + WHERE ` + strings.Join(where, " AND ") + ` + ORDER BY ` + strings.Join(orderBy, ", ") + if find.Limit != nil { + query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit) + if find.Offset != nil { + query = fmt.Sprintf("%s OFFSET %d", query, *find.Offset) + } + } + + rows, err := d.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + list := make([]*store.Memo, 0) + for rows.Next() { + var memo store.Memo + var payloadBytes []byte + dests := []any{ + &memo.ID, + &memo.UID, + &memo.CreatorID, + &memo.CreatedTs, + &memo.UpdatedTs, + &memo.RowStatus, + &memo.Visibility, + &memo.Pinned, + &payloadBytes, + &memo.ParentID, + } + if !find.ExcludeContent { + dests = append(dests, &memo.Content) + } + if err := rows.Scan(dests...); err != nil { + return nil, err + } + payload := &storepb.MemoPayload{} + if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal payload") + } + memo.Payload = payload + list = append(list, &memo) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} + +func (d *DB) GetMemo(ctx context.Context, find *store.FindMemo) (*store.Memo, error) { + list, err := d.ListMemos(ctx, find) + if err != nil { + return nil, err + } + if len(list) == 0 { + return nil, nil + } + + memo := list[0] + return memo, nil +} + +func (d *DB) UpdateMemo(ctx context.Context, update *store.UpdateMemo) error { + set, args := []string{}, []any{} + if v := update.UID; v != nil { + set, args = append(set, "uid = "+placeholder(len(args)+1)), append(args, *v) + } + if v := update.CreatedTs; v != nil { + set, args = append(set, "created_ts = "+placeholder(len(args)+1)), append(args, *v) + } + if v := update.UpdatedTs; v != nil { + set, args = append(set, "updated_ts = "+placeholder(len(args)+1)), append(args, *v) + } + if v := update.RowStatus; v != nil { + set, args = append(set, "row_status = "+placeholder(len(args)+1)), append(args, *v) + } + if v := update.Content; v != nil { + set, args = append(set, "content = "+placeholder(len(args)+1)), append(args, *v) + } + if v := update.Visibility; v != nil { + set, args = append(set, "visibility = "+placeholder(len(args)+1)), append(args, *v) + } + if v := update.Pinned; v != nil { + set, args = append(set, "pinned = "+placeholder(len(args)+1)), append(args, *v) + } + if v := update.Payload; v != nil { + payloadBytes, err := protojson.Marshal(v) + if err != nil { + return err + } + set, args = append(set, "payload = "+placeholder(len(args)+1)), append(args, string(payloadBytes)) + } + if len(set) == 0 { + return nil + } + + stmt := `UPDATE memo SET ` + strings.Join(set, ", ") + ` WHERE id = ` + placeholder(len(args)+1) + args = append(args, update.ID) + if _, err := d.db.ExecContext(ctx, stmt, args...); err != nil { + return err + } + return nil +} + +func (d *DB) DeleteMemo(ctx context.Context, delete *store.DeleteMemo) error { + where, args := []string{"id = " + placeholder(1)}, []any{delete.ID} + stmt := `DELETE FROM memo WHERE ` + strings.Join(where, " AND ") + result, err := d.db.ExecContext(ctx, stmt, args...) + if err != nil { + return errors.Wrap(err, "failed to delete memo") + } + if _, err := result.RowsAffected(); err != nil { + return err + } + return nil +} diff --git a/store/db/postgres/memo_filter.go b/store/db/postgres/memo_filter.go new file mode 100644 index 0000000..e788e11 --- /dev/null +++ b/store/db/postgres/memo_filter.go @@ -0,0 +1,326 @@ +package postgres + +import ( + "fmt" + "slices" + "strings" + + "github.com/pkg/errors" + exprv1 "google.golang.org/genproto/googleapis/api/expr/v1alpha1" + + "github.com/usememos/memos/plugin/filter" +) + +func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) error { + const dbType = filter.PostgreSQLTemplate + // Fix: Use ctx.ArgsOffset instead of len(ctx.Args) to properly handle parameter indexing + _, err := d.convertWithParameterIndex(ctx, expr, dbType, ctx.ArgsOffset+1) + return err +} + +func (d *DB) convertWithParameterIndex(ctx *filter.ConvertContext, expr *exprv1.Expr, dbType filter.TemplateDBType, paramIndex int) (int, error) { + if v, ok := expr.ExprKind.(*exprv1.Expr_CallExpr); ok { + switch v.CallExpr.Function { + case "_||_", "_&&_": + if len(v.CallExpr.Args) != 2 { + return paramIndex, errors.Errorf("invalid number of arguments for %s", v.CallExpr.Function) + } + if _, err := ctx.Buffer.WriteString("("); err != nil { + return paramIndex, err + } + newParamIndex, err := d.convertWithParameterIndex(ctx, v.CallExpr.Args[0], dbType, paramIndex) + if err != nil { + return paramIndex, err + } + operator := "AND" + if v.CallExpr.Function == "_||_" { + operator = "OR" + } + if _, err := ctx.Buffer.WriteString(fmt.Sprintf(" %s ", operator)); err != nil { + return paramIndex, err + } + newParamIndex, err = d.convertWithParameterIndex(ctx, v.CallExpr.Args[1], dbType, newParamIndex) + if err != nil { + return paramIndex, err + } + if _, err := ctx.Buffer.WriteString(")"); err != nil { + return paramIndex, err + } + return newParamIndex, nil + case "!_": + if len(v.CallExpr.Args) != 1 { + return paramIndex, errors.Errorf("invalid number of arguments for %s", v.CallExpr.Function) + } + if _, err := ctx.Buffer.WriteString("NOT ("); err != nil { + return paramIndex, err + } + newParamIndex, err := d.convertWithParameterIndex(ctx, v.CallExpr.Args[0], dbType, paramIndex) + if err != nil { + return paramIndex, err + } + if _, err := ctx.Buffer.WriteString(")"); err != nil { + return paramIndex, err + } + return newParamIndex, nil + case "_==_", "_!=_", "_<_", "_>_", "_<=_", "_>=_": + if len(v.CallExpr.Args) != 2 { + return paramIndex, errors.Errorf("invalid number of arguments for %s", v.CallExpr.Function) + } + // Check if the left side is a function call like size(tags) + if leftCallExpr, ok := v.CallExpr.Args[0].ExprKind.(*exprv1.Expr_CallExpr); ok { + if leftCallExpr.CallExpr.Function == "size" { + // Handle size(tags) comparison + if len(leftCallExpr.CallExpr.Args) != 1 { + return paramIndex, errors.New("size function requires exactly one argument") + } + identifier, err := filter.GetIdentExprName(leftCallExpr.CallExpr.Args[0]) + if err != nil { + return paramIndex, err + } + if identifier != "tags" { + return paramIndex, errors.Errorf("size function only supports 'tags' identifier, got: %s", identifier) + } + value, err := filter.GetExprValue(v.CallExpr.Args[1]) + if err != nil { + return paramIndex, err + } + valueInt, ok := value.(int64) + if !ok { + return paramIndex, errors.New("size comparison value must be an integer") + } + operator := d.getComparisonOperator(v.CallExpr.Function) + + if _, err := ctx.Buffer.WriteString(fmt.Sprintf("%s %s %s", + filter.GetSQL("json_array_length", dbType), operator, + filter.GetParameterPlaceholder(dbType, paramIndex))); err != nil { + return paramIndex, err + } + ctx.Args = append(ctx.Args, valueInt) + return paramIndex + 1, nil + } + } + + identifier, err := filter.GetIdentExprName(v.CallExpr.Args[0]) + if err != nil { + return paramIndex, err + } + if !slices.Contains([]string{"creator_id", "created_ts", "updated_ts", "visibility", "content", "has_task_list"}, identifier) { + return paramIndex, errors.Errorf("invalid identifier for %s", v.CallExpr.Function) + } + value, err := filter.GetExprValue(v.CallExpr.Args[1]) + if err != nil { + return paramIndex, err + } + operator := d.getComparisonOperator(v.CallExpr.Function) + + if identifier == "created_ts" || identifier == "updated_ts" { + valueInt, ok := value.(int64) + if !ok { + return paramIndex, errors.New("invalid integer timestamp value") + } + + timestampSQL := fmt.Sprintf(filter.GetSQL("timestamp_field", dbType), identifier) + if _, err := ctx.Buffer.WriteString(fmt.Sprintf("%s %s %s", timestampSQL, operator, + filter.GetParameterPlaceholder(dbType, paramIndex))); err != nil { + return paramIndex, err + } + ctx.Args = append(ctx.Args, valueInt) + return paramIndex + 1, nil + } else if identifier == "visibility" || identifier == "content" { + if operator != "=" && operator != "!=" { + return paramIndex, errors.Errorf("invalid operator for %s", v.CallExpr.Function) + } + valueStr, ok := value.(string) + if !ok { + return paramIndex, errors.New("invalid string value") + } + + var sqlTemplate string + if identifier == "visibility" { + sqlTemplate = filter.GetSQL("table_prefix", dbType) + ".visibility" + } else if identifier == "content" { + sqlTemplate = filter.GetSQL("content_like", dbType) + if _, err := ctx.Buffer.WriteString(sqlTemplate); err != nil { + return paramIndex, err + } + ctx.Args = append(ctx.Args, fmt.Sprintf("%%%s%%", valueStr)) + return paramIndex + 1, nil + } + if _, err := ctx.Buffer.WriteString(fmt.Sprintf("%s %s %s", sqlTemplate, operator, + filter.GetParameterPlaceholder(dbType, paramIndex))); err != nil { + return paramIndex, err + } + ctx.Args = append(ctx.Args, valueStr) + return paramIndex + 1, nil + } else if identifier == "creator_id" { + if operator != "=" && operator != "!=" { + return paramIndex, errors.Errorf("invalid operator for %s", v.CallExpr.Function) + } + valueInt, ok := value.(int64) + if !ok { + return paramIndex, errors.New("invalid int value") + } + + sqlTemplate := filter.GetSQL("table_prefix", dbType) + ".creator_id" + if _, err := ctx.Buffer.WriteString(fmt.Sprintf("%s %s %s", sqlTemplate, operator, + filter.GetParameterPlaceholder(dbType, paramIndex))); err != nil { + return paramIndex, err + } + ctx.Args = append(ctx.Args, valueInt) + return paramIndex + 1, nil + } else if identifier == "has_task_list" { + if operator != "=" && operator != "!=" { + return paramIndex, errors.Errorf("invalid operator for %s", v.CallExpr.Function) + } + valueBool, ok := value.(bool) + if !ok { + return paramIndex, errors.New("invalid boolean value for has_task_list") + } + // Use parameterized template for boolean comparison (PostgreSQL only) + placeholder := filter.GetParameterPlaceholder(dbType, paramIndex) + sqlTemplate := fmt.Sprintf(filter.GetSQL("boolean_compare", dbType), operator) + sqlTemplate = strings.Replace(sqlTemplate, "?", placeholder, 1) + if _, err := ctx.Buffer.WriteString(sqlTemplate); err != nil { + return paramIndex, err + } + ctx.Args = append(ctx.Args, valueBool) + return paramIndex + 1, nil + } + case "@in": + if len(v.CallExpr.Args) != 2 { + return paramIndex, errors.Errorf("invalid number of arguments for %s", v.CallExpr.Function) + } + + // Check if this is "element in collection" syntax + if identifier, err := filter.GetIdentExprName(v.CallExpr.Args[1]); err == nil { + // This is "element in collection" - the second argument is the collection + if !slices.Contains([]string{"tags"}, identifier) { + return paramIndex, errors.Errorf("invalid collection identifier for %s: %s", v.CallExpr.Function, identifier) + } + + if identifier == "tags" { + // Handle "element" in tags + element, err := filter.GetConstValue(v.CallExpr.Args[0]) + if err != nil { + return paramIndex, errors.Errorf("first argument must be a constant value for 'element in tags': %v", err) + } + placeholder := filter.GetParameterPlaceholder(dbType, paramIndex) + sql := strings.Replace(filter.GetSQL("json_contains_element", dbType), "?", placeholder, 1) + if _, err := ctx.Buffer.WriteString(sql); err != nil { + return paramIndex, err + } + ctx.Args = append(ctx.Args, filter.GetParameterValue(dbType, "json_contains_element", element)) + return paramIndex + 1, nil + } + return paramIndex, nil + } + + // Original logic for "identifier in [list]" syntax + identifier, err := filter.GetIdentExprName(v.CallExpr.Args[0]) + if err != nil { + return paramIndex, err + } + if !slices.Contains([]string{"tag", "visibility"}, identifier) { + return paramIndex, errors.Errorf("invalid identifier for %s", v.CallExpr.Function) + } + + values := []any{} + for _, element := range v.CallExpr.Args[1].GetListExpr().Elements { + value, err := filter.GetConstValue(element) + if err != nil { + return paramIndex, err + } + values = append(values, value) + } + if identifier == "tag" { + subconditions := []string{} + args := []any{} + currentParamIndex := paramIndex + for _, v := range values { + // Use parameter index for each placeholder + placeholder := filter.GetParameterPlaceholder(dbType, currentParamIndex) + subcondition := strings.Replace(filter.GetSQL("json_contains_tag", dbType), "?", placeholder, 1) + subconditions = append(subconditions, subcondition) + args = append(args, filter.GetParameterValue(dbType, "json_contains_tag", v)) + currentParamIndex++ + } + if len(subconditions) == 1 { + if _, err := ctx.Buffer.WriteString(subconditions[0]); err != nil { + return paramIndex, err + } + } else { + if _, err := ctx.Buffer.WriteString(fmt.Sprintf("(%s)", strings.Join(subconditions, " OR "))); err != nil { + return paramIndex, err + } + } + ctx.Args = append(ctx.Args, args...) + return paramIndex + len(args), nil + } else if identifier == "visibility" { + placeholders := filter.FormatPlaceholders(dbType, len(values), paramIndex) + visibilitySQL := fmt.Sprintf(filter.GetSQL("visibility_in", dbType), strings.Join(placeholders, ",")) + if _, err := ctx.Buffer.WriteString(visibilitySQL); err != nil { + return paramIndex, err + } + ctx.Args = append(ctx.Args, values...) + return paramIndex + len(values), nil + } + case "contains": + if len(v.CallExpr.Args) != 1 { + return paramIndex, errors.Errorf("invalid number of arguments for %s", v.CallExpr.Function) + } + identifier, err := filter.GetIdentExprName(v.CallExpr.Target) + if err != nil { + return paramIndex, err + } + if identifier != "content" { + return paramIndex, errors.Errorf("invalid identifier for %s", v.CallExpr.Function) + } + arg, err := filter.GetConstValue(v.CallExpr.Args[0]) + if err != nil { + return paramIndex, err + } + placeholder := filter.GetParameterPlaceholder(dbType, paramIndex) + sql := strings.Replace(filter.GetSQL("content_like", dbType), "?", placeholder, 1) + if _, err := ctx.Buffer.WriteString(sql); err != nil { + return paramIndex, err + } + ctx.Args = append(ctx.Args, fmt.Sprintf("%%%s%%", arg)) + return paramIndex + 1, nil + } + } else if v, ok := expr.ExprKind.(*exprv1.Expr_IdentExpr); ok { + identifier := v.IdentExpr.GetName() + if !slices.Contains([]string{"pinned", "has_task_list"}, identifier) { + return paramIndex, errors.Errorf("invalid identifier %s", identifier) + } + if identifier == "pinned" { + if _, err := ctx.Buffer.WriteString(filter.GetSQL("table_prefix", dbType) + ".pinned IS TRUE"); err != nil { + return paramIndex, err + } + } else if identifier == "has_task_list" { + // Handle has_task_list as a standalone boolean identifier + if _, err := ctx.Buffer.WriteString(filter.GetSQL("boolean_check", dbType)); err != nil { + return paramIndex, err + } + } + } + return paramIndex, nil +} + +func (*DB) getComparisonOperator(function string) string { + switch function { + case "_==_": + return "=" + case "_!=_": + return "!=" + case "_<_": + return "<" + case "_>_": + return ">" + case "_<=_": + return "<=" + case "_>=_": + return ">=" + default: + return "=" + } +} diff --git a/store/db/postgres/memo_filter_test.go b/store/db/postgres/memo_filter_test.go new file mode 100644 index 0000000..d38e321 --- /dev/null +++ b/store/db/postgres/memo_filter_test.go @@ -0,0 +1,130 @@ +package postgres + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/usememos/memos/plugin/filter" +) + +func TestRestoreExprToSQL(t *testing.T) { + tests := []struct { + filter string + want string + args []any + }{ + { + filter: `tag in ["tag1", "tag2"]`, + want: "(memo.payload->'tags' @> jsonb_build_array($1) OR memo.payload->'tags' @> jsonb_build_array($2))", + args: []any{"tag1", "tag2"}, + }, + { + filter: `!(tag in ["tag1", "tag2"])`, + want: `NOT ((memo.payload->'tags' @> jsonb_build_array($1) OR memo.payload->'tags' @> jsonb_build_array($2)))`, + args: []any{"tag1", "tag2"}, + }, + { + filter: `content.contains("memos")`, + want: "memo.content ILIKE $1", + args: []any{"%memos%"}, + }, + { + filter: `visibility in ["PUBLIC"]`, + want: "memo.visibility IN ($1)", + args: []any{"PUBLIC"}, + }, + { + filter: `visibility in ["PUBLIC", "PRIVATE"]`, + want: "memo.visibility IN ($1,$2)", + args: []any{"PUBLIC", "PRIVATE"}, + }, + { + filter: `tag in ['tag1'] || content.contains('hello')`, + want: "(memo.payload->'tags' @> jsonb_build_array($1) OR memo.content ILIKE $2)", + args: []any{"tag1", "%hello%"}, + }, + { + filter: `1`, + want: "", + args: []any{}, + }, + { + filter: `pinned`, + want: "memo.pinned IS TRUE", + args: []any{}, + }, + { + filter: `has_task_list`, + want: "(memo.payload->'property'->>'hasTaskList')::boolean IS TRUE", + args: []any{}, + }, + { + filter: `has_task_list == true`, + want: "(memo.payload->'property'->>'hasTaskList')::boolean = $1", + args: []any{true}, + }, + { + filter: `has_task_list != false`, + want: "(memo.payload->'property'->>'hasTaskList')::boolean != $1", + args: []any{false}, + }, + { + filter: `has_task_list == false`, + want: "(memo.payload->'property'->>'hasTaskList')::boolean = $1", + args: []any{false}, + }, + { + filter: `!has_task_list`, + want: "NOT ((memo.payload->'property'->>'hasTaskList')::boolean IS TRUE)", + args: []any{}, + }, + { + filter: `has_task_list && pinned`, + want: "((memo.payload->'property'->>'hasTaskList')::boolean IS TRUE AND memo.pinned IS TRUE)", + args: []any{}, + }, + { + filter: `has_task_list && content.contains("todo")`, + want: "((memo.payload->'property'->>'hasTaskList')::boolean IS TRUE AND memo.content ILIKE $1)", + args: []any{"%todo%"}, + }, + { + filter: `created_ts > now() - 60 * 60 * 24`, + want: "EXTRACT(EPOCH FROM memo.created_ts) > $1", + args: []any{time.Now().Unix() - 60*60*24}, + }, + { + filter: `size(tags) == 0`, + want: "jsonb_array_length(COALESCE(memo.payload->'tags', '[]'::jsonb)) = $1", + args: []any{int64(0)}, + }, + { + filter: `size(tags) > 0`, + want: "jsonb_array_length(COALESCE(memo.payload->'tags', '[]'::jsonb)) > $1", + args: []any{int64(0)}, + }, + { + filter: `"work" in tags`, + want: "memo.payload->'tags' @> jsonb_build_array($1)", + args: []any{"work"}, + }, + { + filter: `size(tags) == 2`, + want: "jsonb_array_length(COALESCE(memo.payload->'tags', '[]'::jsonb)) = $1", + args: []any{int64(2)}, + }, + } + + for _, tt := range tests { + db := &DB{} + parsedExpr, err := filter.Parse(tt.filter, filter.MemoFilterCELAttributes...) + require.NoError(t, err) + convertCtx := filter.NewConvertContext() + err = db.ConvertExprToSQL(convertCtx, parsedExpr.GetExpr()) + require.NoError(t, err) + require.Equal(t, tt.want, convertCtx.Buffer.String()) + require.Equal(t, tt.args, convertCtx.Args) + } +} diff --git a/store/db/postgres/memo_relation.go b/store/db/postgres/memo_relation.go new file mode 100644 index 0000000..f764408 --- /dev/null +++ b/store/db/postgres/memo_relation.go @@ -0,0 +1,124 @@ +package postgres + +import ( + "context" + "fmt" + "strings" + + "github.com/usememos/memos/plugin/filter" + "github.com/usememos/memos/store" +) + +func (d *DB) UpsertMemoRelation(ctx context.Context, create *store.MemoRelation) (*store.MemoRelation, error) { + stmt := ` + INSERT INTO memo_relation ( + memo_id, + related_memo_id, + type + ) + VALUES (` + placeholders(3) + `) + RETURNING memo_id, related_memo_id, type + ` + memoRelation := &store.MemoRelation{} + if err := d.db.QueryRowContext( + ctx, + stmt, + create.MemoID, + create.RelatedMemoID, + create.Type, + ).Scan( + &memoRelation.MemoID, + &memoRelation.RelatedMemoID, + &memoRelation.Type, + ); err != nil { + return nil, err + } + + return memoRelation, nil +} + +func (d *DB) ListMemoRelations(ctx context.Context, find *store.FindMemoRelation) ([]*store.MemoRelation, error) { + where, args := []string{"1 = 1"}, []any{} + if find.MemoID != nil { + where, args = append(where, "memo_id = "+placeholder(len(args)+1)), append(args, find.MemoID) + } + if find.RelatedMemoID != nil { + where, args = append(where, "related_memo_id = "+placeholder(len(args)+1)), append(args, find.RelatedMemoID) + } + if find.Type != nil { + where, args = append(where, "type = "+placeholder(len(args)+1)), append(args, find.Type) + } + if find.MemoFilter != nil { + // Parse filter string and return the parsed expression. + // The filter string should be a CEL expression. + parsedExpr, err := filter.Parse(*find.MemoFilter, filter.MemoFilterCELAttributes...) + if err != nil { + return nil, err + } + convertCtx := filter.NewConvertContext() + convertCtx.ArgsOffset = len(args) + // ConvertExprToSQL converts the parsed expression to a SQL condition string. + if err := d.ConvertExprToSQL(convertCtx, parsedExpr.GetExpr()); err != nil { + return nil, err + } + condition := convertCtx.Buffer.String() + if condition != "" { + where = append(where, fmt.Sprintf("memo_id IN (SELECT id FROM memo WHERE %s)", condition)) + where = append(where, fmt.Sprintf("related_memo_id IN (SELECT id FROM memo WHERE %s)", condition)) + args = append(args, convertCtx.Args...) + } + } + + rows, err := d.db.QueryContext(ctx, ` + SELECT + memo_id, + related_memo_id, + type + FROM memo_relation + WHERE `+strings.Join(where, " AND "), args...) + if err != nil { + return nil, err + } + defer rows.Close() + + list := []*store.MemoRelation{} + for rows.Next() { + memoRelation := &store.MemoRelation{} + if err := rows.Scan( + &memoRelation.MemoID, + &memoRelation.RelatedMemoID, + &memoRelation.Type, + ); err != nil { + return nil, err + } + list = append(list, memoRelation) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} + +func (d *DB) DeleteMemoRelation(ctx context.Context, delete *store.DeleteMemoRelation) error { + where, args := []string{"1 = 1"}, []any{} + if delete.MemoID != nil { + where, args = append(where, "memo_id = "+placeholder(len(args)+1)), append(args, delete.MemoID) + } + if delete.RelatedMemoID != nil { + where, args = append(where, "related_memo_id = "+placeholder(len(args)+1)), append(args, delete.RelatedMemoID) + } + if delete.Type != nil { + where, args = append(where, "type = "+placeholder(len(args)+1)), append(args, delete.Type) + } + stmt := `DELETE FROM memo_relation WHERE ` + strings.Join(where, " AND ") + result, err := d.db.ExecContext(ctx, stmt, args...) + if err != nil { + return err + } + if _, err = result.RowsAffected(); err != nil { + return err + } + return nil +} diff --git a/store/db/postgres/migration_history.go b/store/db/postgres/migration_history.go new file mode 100644 index 0000000..385b898 --- /dev/null +++ b/store/db/postgres/migration_history.go @@ -0,0 +1,57 @@ +package postgres + +import ( + "context" + + "github.com/usememos/memos/store" +) + +func (d *DB) FindMigrationHistoryList(ctx context.Context, _ *store.FindMigrationHistory) ([]*store.MigrationHistory, error) { + query := "SELECT version, created_ts FROM migration_history ORDER BY created_ts DESC" + rows, err := d.db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + list := make([]*store.MigrationHistory, 0) + for rows.Next() { + var migrationHistory store.MigrationHistory + if err := rows.Scan( + &migrationHistory.Version, + &migrationHistory.CreatedTs, + ); err != nil { + return nil, err + } + + list = append(list, &migrationHistory) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} + +func (d *DB) UpsertMigrationHistory(ctx context.Context, upsert *store.UpsertMigrationHistory) (*store.MigrationHistory, error) { + stmt := ` + INSERT INTO migration_history ( + version + ) + VALUES ($1) + ON CONFLICT(version) DO UPDATE + SET + version=EXCLUDED.version + RETURNING version, created_ts + ` + var migrationHistory store.MigrationHistory + if err := d.db.QueryRowContext(ctx, stmt, upsert.Version).Scan( + &migrationHistory.Version, + &migrationHistory.CreatedTs, + ); err != nil { + return nil, err + } + + return &migrationHistory, nil +} diff --git a/store/db/postgres/postgres.go b/store/db/postgres/postgres.go new file mode 100644 index 0000000..4cf017e --- /dev/null +++ b/store/db/postgres/postgres.go @@ -0,0 +1,57 @@ +package postgres + +import ( + "context" + "database/sql" + "log" + + // Import the PostgreSQL driver. + _ "github.com/lib/pq" + "github.com/pkg/errors" + + "github.com/usememos/memos/internal/profile" + "github.com/usememos/memos/store" +) + +type DB struct { + db *sql.DB + profile *profile.Profile +} + +func NewDB(profile *profile.Profile) (store.Driver, error) { + if profile == nil { + return nil, errors.New("profile is nil") + } + + // Open the PostgreSQL connection + db, err := sql.Open("postgres", profile.DSN) + if err != nil { + log.Printf("Failed to open database: %s", err) + return nil, errors.Wrapf(err, "failed to open database: %s", profile.DSN) + } + + var driver store.Driver = &DB{ + db: db, + profile: profile, + } + + // Return the DB struct + return driver, nil +} + +func (d *DB) GetDB() *sql.DB { + return d.db +} + +func (d *DB) Close() error { + return d.db.Close() +} + +func (d *DB) IsInitialized(ctx context.Context) (bool, error) { + var exists bool + err := d.db.QueryRowContext(ctx, "SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'memo' AND table_type = 'BASE TABLE')").Scan(&exists) + if err != nil { + return false, errors.Wrap(err, "failed to check if database is initialized") + } + return exists, nil +} diff --git a/store/db/postgres/reaction.go b/store/db/postgres/reaction.go new file mode 100644 index 0000000..295c34d --- /dev/null +++ b/store/db/postgres/reaction.go @@ -0,0 +1,79 @@ +package postgres + +import ( + "context" + "strings" + + "github.com/usememos/memos/store" +) + +func (d *DB) UpsertReaction(ctx context.Context, upsert *store.Reaction) (*store.Reaction, error) { + fields := []string{"creator_id", "content_id", "reaction_type"} + args := []interface{}{upsert.CreatorID, upsert.ContentID, upsert.ReactionType} + stmt := "INSERT INTO reaction (" + strings.Join(fields, ", ") + ") VALUES (" + placeholders(len(args)) + ") RETURNING id, created_ts" + if err := d.db.QueryRowContext(ctx, stmt, args...).Scan( + &upsert.ID, + &upsert.CreatedTs, + ); err != nil { + return nil, err + } + + reaction := upsert + return reaction, nil +} + +func (d *DB) ListReactions(ctx context.Context, find *store.FindReaction) ([]*store.Reaction, error) { + where, args := []string{"1 = 1"}, []interface{}{} + if find.ID != nil { + where, args = append(where, "id = "+placeholder(len(args)+1)), append(args, *find.ID) + } + if find.CreatorID != nil { + where, args = append(where, "creator_id = "+placeholder(len(args)+1)), append(args, *find.CreatorID) + } + if find.ContentID != nil { + where, args = append(where, "content_id = "+placeholder(len(args)+1)), append(args, *find.ContentID) + } + + rows, err := d.db.QueryContext(ctx, ` + SELECT + id, + created_ts, + creator_id, + content_id, + reaction_type + FROM reaction + WHERE `+strings.Join(where, " AND ")+` + ORDER BY id ASC`, + args..., + ) + if err != nil { + return nil, err + } + defer rows.Close() + + list := []*store.Reaction{} + for rows.Next() { + reaction := &store.Reaction{} + if err := rows.Scan( + &reaction.ID, + &reaction.CreatedTs, + &reaction.CreatorID, + &reaction.ContentID, + &reaction.ReactionType, + ); err != nil { + return nil, err + } + list = append(list, reaction) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} + +func (d *DB) DeleteReaction(ctx context.Context, delete *store.DeleteReaction) error { + _, err := d.db.ExecContext(ctx, "DELETE FROM reaction WHERE id = $1", delete.ID) + return err +} diff --git a/store/db/postgres/user.go b/store/db/postgres/user.go new file mode 100644 index 0000000..87ea847 --- /dev/null +++ b/store/db/postgres/user.go @@ -0,0 +1,166 @@ +package postgres + +import ( + "context" + "fmt" + "strings" + + "github.com/usememos/memos/store" +) + +func (d *DB) CreateUser(ctx context.Context, create *store.User) (*store.User, error) { + fields := []string{"username", "role", "email", "nickname", "password_hash", "avatar_url"} + args := []any{create.Username, create.Role, create.Email, create.Nickname, create.PasswordHash, create.AvatarURL} + stmt := "INSERT INTO \"user\" (" + strings.Join(fields, ", ") + ") VALUES (" + placeholders(len(args)) + ") RETURNING id, description, created_ts, updated_ts, row_status" + if err := d.db.QueryRowContext(ctx, stmt, args...).Scan( + &create.ID, + &create.Description, + &create.CreatedTs, + &create.UpdatedTs, + &create.RowStatus, + ); err != nil { + return nil, err + } + + return create, nil +} + +func (d *DB) UpdateUser(ctx context.Context, update *store.UpdateUser) (*store.User, error) { + set, args := []string{}, []any{} + if v := update.UpdatedTs; v != nil { + set, args = append(set, "updated_ts = "+placeholder(len(args)+1)), append(args, *v) + } + if v := update.RowStatus; v != nil { + set, args = append(set, "row_status = "+placeholder(len(args)+1)), append(args, *v) + } + if v := update.Username; v != nil { + set, args = append(set, "username = "+placeholder(len(args)+1)), append(args, *v) + } + if v := update.Email; v != nil { + set, args = append(set, "email = "+placeholder(len(args)+1)), append(args, *v) + } + if v := update.Nickname; v != nil { + set, args = append(set, "nickname = "+placeholder(len(args)+1)), append(args, *v) + } + if v := update.AvatarURL; v != nil { + set, args = append(set, "avatar_url = "+placeholder(len(args)+1)), append(args, *v) + } + if v := update.PasswordHash; v != nil { + set, args = append(set, "password_hash = "+placeholder(len(args)+1)), append(args, *v) + } + if v := update.Description; v != nil { + set, args = append(set, "description = "+placeholder(len(args)+1)), append(args, *v) + } + if v := update.Role; v != nil { + set, args = append(set, "role = "+placeholder(len(args)+1)), append(args, *v) + } + + query := ` + UPDATE "user" + SET ` + strings.Join(set, ", ") + ` + WHERE id = ` + placeholder(len(args)+1) + ` + RETURNING id, username, role, email, nickname, password_hash, avatar_url, description, created_ts, updated_ts, row_status + ` + args = append(args, update.ID) + user := &store.User{} + if err := d.db.QueryRowContext(ctx, query, args...).Scan( + &user.ID, + &user.Username, + &user.Role, + &user.Email, + &user.Nickname, + &user.PasswordHash, + &user.AvatarURL, + &user.Description, + &user.CreatedTs, + &user.UpdatedTs, + &user.RowStatus, + ); err != nil { + return nil, err + } + + return user, nil +} + +func (d *DB) ListUsers(ctx context.Context, find *store.FindUser) ([]*store.User, error) { + where, args := []string{"1 = 1"}, []any{} + + if v := find.ID; v != nil { + where, args = append(where, "id = "+placeholder(len(args)+1)), append(args, *v) + } + if v := find.Username; v != nil { + where, args = append(where, "username = "+placeholder(len(args)+1)), append(args, *v) + } + if v := find.Role; v != nil { + where, args = append(where, "role = "+placeholder(len(args)+1)), append(args, *v) + } + if v := find.Email; v != nil { + where, args = append(where, "email = "+placeholder(len(args)+1)), append(args, *v) + } + if v := find.Nickname; v != nil { + where, args = append(where, "nickname = "+placeholder(len(args)+1)), append(args, *v) + } + + orderBy := []string{"created_ts DESC", "row_status DESC"} + query := ` + SELECT + id, + username, + role, + email, + nickname, + password_hash, + avatar_url, + description, + created_ts, + updated_ts, + row_status + FROM "user" + WHERE ` + strings.Join(where, " AND ") + ` ORDER BY ` + strings.Join(orderBy, ", ") + if v := find.Limit; v != nil { + query += fmt.Sprintf(" LIMIT %d", *v) + } + rows, err := d.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + list := make([]*store.User, 0) + for rows.Next() { + var user store.User + if err := rows.Scan( + &user.ID, + &user.Username, + &user.Role, + &user.Email, + &user.Nickname, + &user.PasswordHash, + &user.AvatarURL, + &user.Description, + &user.CreatedTs, + &user.UpdatedTs, + &user.RowStatus, + ); err != nil { + return nil, err + } + list = append(list, &user) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} + +func (d *DB) DeleteUser(ctx context.Context, delete *store.DeleteUser) error { + result, err := d.db.ExecContext(ctx, `DELETE FROM "user" WHERE id = $1`, delete.ID) + if err != nil { + return err + } + if _, err := result.RowsAffected(); err != nil { + return err + } + return nil +} diff --git a/store/db/postgres/user_setting.go b/store/db/postgres/user_setting.go new file mode 100644 index 0000000..04aec63 --- /dev/null +++ b/store/db/postgres/user_setting.go @@ -0,0 +1,69 @@ +package postgres + +import ( + "context" + "strings" + + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func (d *DB) UpsertUserSetting(ctx context.Context, upsert *store.UserSetting) (*store.UserSetting, error) { + stmt := ` + INSERT INTO user_setting ( + user_id, key, value + ) + VALUES ($1, $2, $3) + ON CONFLICT(user_id, key) DO UPDATE + SET value = EXCLUDED.value + ` + if _, err := d.db.ExecContext(ctx, stmt, upsert.UserID, upsert.Key.String(), upsert.Value); err != nil { + return nil, err + } + return upsert, nil +} + +func (d *DB) ListUserSettings(ctx context.Context, find *store.FindUserSetting) ([]*store.UserSetting, error) { + where, args := []string{"1 = 1"}, []any{} + + if v := find.Key; v != storepb.UserSetting_KEY_UNSPECIFIED { + where, args = append(where, "key = "+placeholder(len(args)+1)), append(args, v.String()) + } + if v := find.UserID; v != nil { + where, args = append(where, "user_id = "+placeholder(len(args)+1)), append(args, *find.UserID) + } + + query := ` + SELECT + user_id, + key, + value + FROM user_setting + WHERE ` + strings.Join(where, " AND ") + rows, err := d.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + userSettingList := make([]*store.UserSetting, 0) + for rows.Next() { + userSetting := &store.UserSetting{} + var keyString string + if err := rows.Scan( + &userSetting.UserID, + &keyString, + &userSetting.Value, + ); err != nil { + return nil, err + } + userSetting.Key = storepb.UserSetting_Key(storepb.UserSetting_Key_value[keyString]) + userSettingList = append(userSettingList, userSetting) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return userSettingList, nil +} diff --git a/store/db/postgres/workspace_setting.go b/store/db/postgres/workspace_setting.go new file mode 100644 index 0000000..038a57b --- /dev/null +++ b/store/db/postgres/workspace_setting.go @@ -0,0 +1,72 @@ +package postgres + +import ( + "context" + "strings" + + "github.com/usememos/memos/store" +) + +func (d *DB) UpsertWorkspaceSetting(ctx context.Context, upsert *store.WorkspaceSetting) (*store.WorkspaceSetting, error) { + stmt := ` + INSERT INTO system_setting ( + name, value, description + ) + VALUES ($1, $2, $3) + ON CONFLICT(name) DO UPDATE + SET + value = EXCLUDED.value, + description = EXCLUDED.description + ` + if _, err := d.db.ExecContext(ctx, stmt, upsert.Name, upsert.Value, upsert.Description); err != nil { + return nil, err + } + + return upsert, nil +} + +func (d *DB) ListWorkspaceSettings(ctx context.Context, find *store.FindWorkspaceSetting) ([]*store.WorkspaceSetting, error) { + where, args := []string{"1 = 1"}, []any{} + if find.Name != "" { + where, args = append(where, "name = "+placeholder(len(args)+1)), append(args, find.Name) + } + + query := ` + SELECT + name, + value, + description + FROM system_setting + WHERE ` + strings.Join(where, " AND ") + + rows, err := d.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + list := []*store.WorkspaceSetting{} + for rows.Next() { + systemSettingMessage := &store.WorkspaceSetting{} + if err := rows.Scan( + &systemSettingMessage.Name, + &systemSettingMessage.Value, + &systemSettingMessage.Description, + ); err != nil { + return nil, err + } + list = append(list, systemSettingMessage) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} + +func (d *DB) DeleteWorkspaceSetting(ctx context.Context, delete *store.DeleteWorkspaceSetting) error { + stmt := `DELETE FROM system_setting WHERE name = $1` + _, err := d.db.ExecContext(ctx, stmt, delete.Name) + return err +} diff --git a/store/db/sqlite/activity.go b/store/db/sqlite/activity.go new file mode 100644 index 0000000..8bbe331 --- /dev/null +++ b/store/db/sqlite/activity.go @@ -0,0 +1,83 @@ +package sqlite + +import ( + "context" + "strings" + + "github.com/pkg/errors" + "google.golang.org/protobuf/encoding/protojson" + + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func (d *DB) CreateActivity(ctx context.Context, create *store.Activity) (*store.Activity, error) { + payloadString := "{}" + if create.Payload != nil { + bytes, err := protojson.Marshal(create.Payload) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal activity payload") + } + payloadString = string(bytes) + } + + fields := []string{"`creator_id`", "`type`", "`level`", "`payload`"} + placeholder := []string{"?", "?", "?", "?"} + args := []any{create.CreatorID, create.Type.String(), create.Level.String(), payloadString} + + stmt := "INSERT INTO activity (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ") RETURNING `id`, `created_ts`" + if err := d.db.QueryRowContext(ctx, stmt, args...).Scan( + &create.ID, + &create.CreatedTs, + ); err != nil { + return nil, err + } + + return create, nil +} + +func (d *DB) ListActivities(ctx context.Context, find *store.FindActivity) ([]*store.Activity, error) { + where, args := []string{"1 = 1"}, []any{} + if find.ID != nil { + where, args = append(where, "`id` = ?"), append(args, *find.ID) + } + if find.Type != nil { + where, args = append(where, "`type` = ?"), append(args, find.Type.String()) + } + + query := "SELECT `id`, `creator_id`, `type`, `level`, `payload`, `created_ts` FROM `activity` WHERE " + strings.Join(where, " AND ") + " ORDER BY `created_ts` DESC" + rows, err := d.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + list := []*store.Activity{} + for rows.Next() { + activity := &store.Activity{} + var payloadBytes []byte + if err := rows.Scan( + &activity.ID, + &activity.CreatorID, + &activity.Type, + &activity.Level, + &payloadBytes, + &activity.CreatedTs, + ); err != nil { + return nil, err + } + + payload := &storepb.ActivityPayload{} + if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil { + return nil, err + } + activity.Payload = payload + list = append(list, activity) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} diff --git a/store/db/sqlite/attachment.go b/store/db/sqlite/attachment.go new file mode 100644 index 0000000..ee547b8 --- /dev/null +++ b/store/db/sqlite/attachment.go @@ -0,0 +1,182 @@ +package sqlite + +import ( + "context" + "database/sql" + "fmt" + "strings" + + "github.com/pkg/errors" + "google.golang.org/protobuf/encoding/protojson" + + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func (d *DB) CreateAttachment(ctx context.Context, create *store.Attachment) (*store.Attachment, error) { + fields := []string{"`uid`", "`filename`", "`blob`", "`type`", "`size`", "`creator_id`", "`memo_id`", "`storage_type`", "`reference`", "`payload`"} + placeholder := []string{"?", "?", "?", "?", "?", "?", "?", "?", "?", "?"} + storageType := "" + if create.StorageType != storepb.AttachmentStorageType_ATTACHMENT_STORAGE_TYPE_UNSPECIFIED { + storageType = create.StorageType.String() + } + payloadString := "{}" + if create.Payload != nil { + bytes, err := protojson.Marshal(create.Payload) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal attachment payload") + } + payloadString = string(bytes) + } + args := []any{create.UID, create.Filename, create.Blob, create.Type, create.Size, create.CreatorID, create.MemoID, storageType, create.Reference, payloadString} + + stmt := "INSERT INTO `resource` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ") RETURNING `id`, `created_ts`, `updated_ts`" + if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(&create.ID, &create.CreatedTs, &create.UpdatedTs); err != nil { + return nil, err + } + + return create, nil +} + +func (d *DB) ListAttachments(ctx context.Context, find *store.FindAttachment) ([]*store.Attachment, error) { + where, args := []string{"1 = 1"}, []any{} + + if v := find.ID; v != nil { + where, args = append(where, "`id` = ?"), append(args, *v) + } + if v := find.UID; v != nil { + where, args = append(where, "`uid` = ?"), append(args, *v) + } + if v := find.CreatorID; v != nil { + where, args = append(where, "`creator_id` = ?"), append(args, *v) + } + if v := find.Filename; v != nil { + where, args = append(where, "`filename` = ?"), append(args, *v) + } + if v := find.FilenameSearch; v != nil { + where, args = append(where, "`filename` LIKE ?"), append(args, fmt.Sprintf("%%%s%%", *v)) + } + if v := find.MemoID; v != nil { + where, args = append(where, "`memo_id` = ?"), append(args, *v) + } + if find.HasRelatedMemo { + where = append(where, "`memo_id` IS NOT NULL") + } + if find.StorageType != nil { + where, args = append(where, "`storage_type` = ?"), append(args, find.StorageType.String()) + } + + fields := []string{"`id`", "`uid`", "`filename`", "`type`", "`size`", "`creator_id`", "`created_ts`", "`updated_ts`", "`memo_id`", "`storage_type`", "`reference`", "`payload`"} + if find.GetBlob { + fields = append(fields, "`blob`") + } + + query := fmt.Sprintf("SELECT %s FROM `resource` WHERE %s ORDER BY `updated_ts` DESC", strings.Join(fields, ", "), strings.Join(where, " AND ")) + if find.Limit != nil { + query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit) + if find.Offset != nil { + query = fmt.Sprintf("%s OFFSET %d", query, *find.Offset) + } + } + + rows, err := d.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + list := make([]*store.Attachment, 0) + for rows.Next() { + attachment := store.Attachment{} + var memoID sql.NullInt32 + var storageType string + var payloadBytes []byte + dests := []any{ + &attachment.ID, + &attachment.UID, + &attachment.Filename, + &attachment.Type, + &attachment.Size, + &attachment.CreatorID, + &attachment.CreatedTs, + &attachment.UpdatedTs, + &memoID, + &storageType, + &attachment.Reference, + &payloadBytes, + } + if find.GetBlob { + dests = append(dests, &attachment.Blob) + } + if err := rows.Scan(dests...); err != nil { + return nil, err + } + + if memoID.Valid { + attachment.MemoID = &memoID.Int32 + } + attachment.StorageType = storepb.AttachmentStorageType(storepb.AttachmentStorageType_value[storageType]) + payload := &storepb.AttachmentPayload{} + if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil { + return nil, err + } + attachment.Payload = payload + list = append(list, &attachment) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} + +func (d *DB) UpdateAttachment(ctx context.Context, update *store.UpdateAttachment) error { + set, args := []string{}, []any{} + + if v := update.UID; v != nil { + set, args = append(set, "`uid` = ?"), append(args, *v) + } + if v := update.UpdatedTs; v != nil { + set, args = append(set, "`updated_ts` = ?"), append(args, *v) + } + if v := update.Filename; v != nil { + set, args = append(set, "`filename` = ?"), append(args, *v) + } + if v := update.MemoID; v != nil { + set, args = append(set, "`memo_id` = ?"), append(args, *v) + } + if v := update.Reference; v != nil { + set, args = append(set, "`reference` = ?"), append(args, *v) + } + if v := update.Payload; v != nil { + bytes, err := protojson.Marshal(v) + if err != nil { + return errors.Wrap(err, "failed to marshal attachment payload") + } + set, args = append(set, "`payload` = ?"), append(args, string(bytes)) + } + + args = append(args, update.ID) + stmt := "UPDATE `resource` SET " + strings.Join(set, ", ") + " WHERE `id` = ?" + result, err := d.db.ExecContext(ctx, stmt, args...) + if err != nil { + return errors.Wrap(err, "failed to update attachment") + } + if _, err := result.RowsAffected(); err != nil { + return err + } + return nil +} + +func (d *DB) DeleteAttachment(ctx context.Context, delete *store.DeleteAttachment) error { + stmt := "DELETE FROM `resource` WHERE `id` = ?" + result, err := d.db.ExecContext(ctx, stmt, delete.ID) + if err != nil { + return err + } + if _, err := result.RowsAffected(); err != nil { + return err + } + return nil +} diff --git a/store/db/sqlite/common.go b/store/db/sqlite/common.go new file mode 100644 index 0000000..edd096c --- /dev/null +++ b/store/db/sqlite/common.go @@ -0,0 +1,9 @@ +package sqlite + +import "google.golang.org/protobuf/encoding/protojson" + +var ( + protojsonUnmarshaler = protojson.UnmarshalOptions{ + DiscardUnknown: true, + } +) diff --git a/store/db/sqlite/idp.go b/store/db/sqlite/idp.go new file mode 100644 index 0000000..6083652 --- /dev/null +++ b/store/db/sqlite/idp.go @@ -0,0 +1,117 @@ +package sqlite + +import ( + "context" + "fmt" + "strings" + + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func (d *DB) CreateIdentityProvider(ctx context.Context, create *store.IdentityProvider) (*store.IdentityProvider, error) { + placeholders := []string{"?", "?", "?", "?"} + fields := []string{"`name`", "`type`", "`identifier_filter`", "`config`"} + args := []any{create.Name, create.Type.String(), create.IdentifierFilter, create.Config} + + stmt := "INSERT INTO `idp` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholders, ", ") + ") RETURNING `id`" + if err := d.db.QueryRowContext(ctx, stmt, args...).Scan(&create.ID); err != nil { + return nil, err + } + + identityProvider := create + return identityProvider, nil +} + +func (d *DB) ListIdentityProviders(ctx context.Context, find *store.FindIdentityProvider) ([]*store.IdentityProvider, error) { + where, args := []string{"1 = 1"}, []any{} + if v := find.ID; v != nil { + where, args = append(where, fmt.Sprintf("id = $%d", len(args)+1)), append(args, *v) + } + + rows, err := d.db.QueryContext(ctx, ` + SELECT + id, + name, + type, + identifier_filter, + config + FROM idp + WHERE `+strings.Join(where, " AND ")+` ORDER BY id ASC`, + args..., + ) + if err != nil { + return nil, err + } + defer rows.Close() + + var identityProviders []*store.IdentityProvider + for rows.Next() { + var identityProvider store.IdentityProvider + var typeString string + if err := rows.Scan( + &identityProvider.ID, + &identityProvider.Name, + &typeString, + &identityProvider.IdentifierFilter, + &identityProvider.Config, + ); err != nil { + return nil, err + } + identityProvider.Type = storepb.IdentityProvider_Type(storepb.IdentityProvider_Type_value[typeString]) + identityProviders = append(identityProviders, &identityProvider) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return identityProviders, nil +} + +func (d *DB) UpdateIdentityProvider(ctx context.Context, update *store.UpdateIdentityProvider) (*store.IdentityProvider, error) { + set, args := []string{}, []any{} + if v := update.Name; v != nil { + set, args = append(set, "name = ?"), append(args, *v) + } + if v := update.IdentifierFilter; v != nil { + set, args = append(set, "identifier_filter = ?"), append(args, *v) + } + if v := update.Config; v != nil { + set, args = append(set, "config = ?"), append(args, *v) + } + args = append(args, update.ID) + + stmt := ` + UPDATE idp + SET ` + strings.Join(set, ", ") + ` + WHERE id = ? + RETURNING id, name, type, identifier_filter, config + ` + var identityProvider store.IdentityProvider + var typeString string + if err := d.db.QueryRowContext(ctx, stmt, args...).Scan( + &identityProvider.ID, + &identityProvider.Name, + &typeString, + &identityProvider.IdentifierFilter, + &identityProvider.Config, + ); err != nil { + return nil, err + } + identityProvider.Type = storepb.IdentityProvider_Type(storepb.IdentityProvider_Type_value[typeString]) + return &identityProvider, nil +} + +func (d *DB) DeleteIdentityProvider(ctx context.Context, delete *store.DeleteIdentityProvider) error { + where, args := []string{"id = ?"}, []any{delete.ID} + stmt := `DELETE FROM idp WHERE ` + strings.Join(where, " AND ") + result, err := d.db.ExecContext(ctx, stmt, args...) + if err != nil { + return err + } + if _, err = result.RowsAffected(); err != nil { + return err + } + return nil +} diff --git a/store/db/sqlite/inbox.go b/store/db/sqlite/inbox.go new file mode 100644 index 0000000..776ed7d --- /dev/null +++ b/store/db/sqlite/inbox.go @@ -0,0 +1,132 @@ +package sqlite + +import ( + "context" + "fmt" + "strings" + + "github.com/pkg/errors" + "google.golang.org/protobuf/encoding/protojson" + + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func (d *DB) CreateInbox(ctx context.Context, create *store.Inbox) (*store.Inbox, error) { + messageString := "{}" + if create.Message != nil { + bytes, err := protojson.Marshal(create.Message) + if err != nil { + return nil, errors.Wrap(err, "failed to marshal inbox message") + } + messageString = string(bytes) + } + + fields := []string{"`sender_id`", "`receiver_id`", "`status`", "`message`"} + placeholder := []string{"?", "?", "?", "?"} + args := []any{create.SenderID, create.ReceiverID, create.Status, messageString} + + stmt := "INSERT INTO `inbox` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ") RETURNING `id`, `created_ts`" + if err := d.db.QueryRowContext(ctx, stmt, args...).Scan( + &create.ID, + &create.CreatedTs, + ); err != nil { + return nil, err + } + + return create, nil +} + +func (d *DB) ListInboxes(ctx context.Context, find *store.FindInbox) ([]*store.Inbox, error) { + where, args := []string{"1 = 1"}, []any{} + + if find.ID != nil { + where, args = append(where, "`id` = ?"), append(args, *find.ID) + } + if find.SenderID != nil { + where, args = append(where, "`sender_id` = ?"), append(args, *find.SenderID) + } + if find.ReceiverID != nil { + where, args = append(where, "`receiver_id` = ?"), append(args, *find.ReceiverID) + } + if find.Status != nil { + where, args = append(where, "`status` = ?"), append(args, *find.Status) + } + + query := "SELECT `id`, `created_ts`, `sender_id`, `receiver_id`, `status`, `message` FROM `inbox` WHERE " + strings.Join(where, " AND ") + " ORDER BY `created_ts` DESC" + if find.Limit != nil { + query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit) + if find.Offset != nil { + query = fmt.Sprintf("%s OFFSET %d", query, *find.Offset) + } + } + rows, err := d.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + list := []*store.Inbox{} + for rows.Next() { + inbox := &store.Inbox{} + var messageBytes []byte + if err := rows.Scan( + &inbox.ID, + &inbox.CreatedTs, + &inbox.SenderID, + &inbox.ReceiverID, + &inbox.Status, + &messageBytes, + ); err != nil { + return nil, err + } + + message := &storepb.InboxMessage{} + if err := protojsonUnmarshaler.Unmarshal(messageBytes, message); err != nil { + return nil, err + } + inbox.Message = message + list = append(list, inbox) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} + +func (d *DB) UpdateInbox(ctx context.Context, update *store.UpdateInbox) (*store.Inbox, error) { + set, args := []string{"`status` = ?"}, []any{update.Status.String()} + args = append(args, update.ID) + query := "UPDATE `inbox` SET " + strings.Join(set, ", ") + " WHERE `id` = ? RETURNING `id`, `created_ts`, `sender_id`, `receiver_id`, `status`, `message`" + inbox := &store.Inbox{} + var messageBytes []byte + if err := d.db.QueryRowContext(ctx, query, args...).Scan( + &inbox.ID, + &inbox.CreatedTs, + &inbox.SenderID, + &inbox.ReceiverID, + &inbox.Status, + &messageBytes, + ); err != nil { + return nil, err + } + message := &storepb.InboxMessage{} + if err := protojsonUnmarshaler.Unmarshal(messageBytes, message); err != nil { + return nil, err + } + inbox.Message = message + return inbox, nil +} + +func (d *DB) DeleteInbox(ctx context.Context, delete *store.DeleteInbox) error { + result, err := d.db.ExecContext(ctx, "DELETE FROM `inbox` WHERE `id` = ?", delete.ID) + if err != nil { + return err + } + if _, err := result.RowsAffected(); err != nil { + return err + } + return nil +} diff --git a/store/db/sqlite/memo.go b/store/db/sqlite/memo.go new file mode 100644 index 0000000..d658025 --- /dev/null +++ b/store/db/sqlite/memo.go @@ -0,0 +1,265 @@ +package sqlite + +import ( + "context" + "fmt" + "strings" + + "github.com/pkg/errors" + "google.golang.org/protobuf/encoding/protojson" + + "github.com/usememos/memos/plugin/filter" + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func (d *DB) CreateMemo(ctx context.Context, create *store.Memo) (*store.Memo, error) { + fields := []string{"`uid`", "`creator_id`", "`content`", "`visibility`", "`payload`"} + placeholder := []string{"?", "?", "?", "?", "?"} + payload := "{}" + if create.Payload != nil { + payloadBytes, err := protojson.Marshal(create.Payload) + if err != nil { + return nil, err + } + payload = string(payloadBytes) + } + args := []any{create.UID, create.CreatorID, create.Content, create.Visibility, payload} + + stmt := "INSERT INTO `memo` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ") RETURNING `id`, `created_ts`, `updated_ts`, `row_status`" + if err := d.db.QueryRowContext(ctx, stmt, args...).Scan( + &create.ID, + &create.CreatedTs, + &create.UpdatedTs, + &create.RowStatus, + ); err != nil { + return nil, err + } + + return create, nil +} + +func (d *DB) ListMemos(ctx context.Context, find *store.FindMemo) ([]*store.Memo, error) { + where, args := []string{"1 = 1"}, []any{} + + if v := find.ID; v != nil { + where, args = append(where, "`memo`.`id` = ?"), append(args, *v) + } + if v := find.UID; v != nil { + where, args = append(where, "`memo`.`uid` = ?"), append(args, *v) + } + if v := find.CreatorID; v != nil { + where, args = append(where, "`memo`.`creator_id` = ?"), append(args, *v) + } + if v := find.RowStatus; v != nil { + where, args = append(where, "`memo`.`row_status` = ?"), append(args, *v) + } + if v := find.CreatedTsBefore; v != nil { + where, args = append(where, "`memo`.`created_ts` < ?"), append(args, *v) + } + if v := find.CreatedTsAfter; v != nil { + where, args = append(where, "`memo`.`created_ts` > ?"), append(args, *v) + } + if v := find.UpdatedTsBefore; v != nil { + where, args = append(where, "`memo`.`updated_ts` < ?"), append(args, *v) + } + if v := find.UpdatedTsAfter; v != nil { + where, args = append(where, "`memo`.`updated_ts` > ?"), append(args, *v) + } + if v := find.ContentSearch; len(v) != 0 { + for _, s := range v { + where, args = append(where, "`memo`.`content` LIKE ?"), append(args, fmt.Sprintf("%%%s%%", s)) + } + } + if v := find.VisibilityList; len(v) != 0 { + placeholder := []string{} + for _, visibility := range v { + placeholder = append(placeholder, "?") + args = append(args, visibility.String()) + } + where = append(where, fmt.Sprintf("`memo`.`visibility` IN (%s)", strings.Join(placeholder, ","))) + } + if v := find.Pinned; v != nil { + where, args = append(where, "`memo`.`pinned` = ?"), append(args, *v) + } + if v := find.PayloadFind; v != nil { + if v.Raw != nil { + where, args = append(where, "`memo`.`payload` = ?"), append(args, *v.Raw) + } + if len(v.TagSearch) != 0 { + for _, tag := range v.TagSearch { + where, args = append(where, "(JSON_EXTRACT(`memo`.`payload`, '$.tags') LIKE ? OR JSON_EXTRACT(`memo`.`payload`, '$.tags') LIKE ?)"), append(args, fmt.Sprintf(`%%"%s"%%`, tag), fmt.Sprintf(`%%"%s/%%`, tag)) + } + } + if v.HasLink { + where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasLink') IS TRUE") + } + if v.HasTaskList { + where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') IS TRUE") + } + if v.HasCode { + where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasCode') IS TRUE") + } + if v.HasIncompleteTasks { + where = append(where, "JSON_EXTRACT(`memo`.`payload`, '$.property.hasIncompleteTasks') IS TRUE") + } + } + if v := find.Filter; v != nil { + // Parse filter string and return the parsed expression. + // The filter string should be a CEL expression. + parsedExpr, err := filter.Parse(*v, filter.MemoFilterCELAttributes...) + if err != nil { + return nil, err + } + convertCtx := filter.NewConvertContext() + // ConvertExprToSQL converts the parsed expression to a SQL condition string. + if err := d.ConvertExprToSQL(convertCtx, parsedExpr.GetExpr()); err != nil { + return nil, err + } + condition := convertCtx.Buffer.String() + if condition != "" { + where = append(where, fmt.Sprintf("(%s)", condition)) + args = append(args, convertCtx.Args...) + } + } + if find.ExcludeComments { + where = append(where, "`parent_id` IS NULL") + } + + order := "DESC" + if find.OrderByTimeAsc { + order = "ASC" + } + orderBy := []string{} + if find.OrderByPinned { + orderBy = append(orderBy, "`pinned` DESC") + } + if find.OrderByUpdatedTs { + orderBy = append(orderBy, "`updated_ts` "+order) + } else { + orderBy = append(orderBy, "`created_ts` "+order) + } + fields := []string{ + "`memo`.`id` AS `id`", + "`memo`.`uid` AS `uid`", + "`memo`.`creator_id` AS `creator_id`", + "`memo`.`created_ts` AS `created_ts`", + "`memo`.`updated_ts` AS `updated_ts`", + "`memo`.`row_status` AS `row_status`", + "`memo`.`visibility` AS `visibility`", + "`memo`.`pinned` AS `pinned`", + "`memo`.`payload` AS `payload`", + "`memo_relation`.`related_memo_id` AS `parent_id`", + } + if !find.ExcludeContent { + fields = append(fields, "`memo`.`content` AS `content`") + } + + query := "SELECT " + strings.Join(fields, ", ") + "FROM `memo` " + + "LEFT JOIN `memo_relation` ON `memo`.`id` = `memo_relation`.`memo_id` AND `memo_relation`.`type` = \"COMMENT\" " + + "WHERE " + strings.Join(where, " AND ") + " " + + "ORDER BY " + strings.Join(orderBy, ", ") + if find.Limit != nil { + query = fmt.Sprintf("%s LIMIT %d", query, *find.Limit) + if find.Offset != nil { + query = fmt.Sprintf("%s OFFSET %d", query, *find.Offset) + } + } + + rows, err := d.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + list := make([]*store.Memo, 0) + for rows.Next() { + var memo store.Memo + var payloadBytes []byte + dests := []any{ + &memo.ID, + &memo.UID, + &memo.CreatorID, + &memo.CreatedTs, + &memo.UpdatedTs, + &memo.RowStatus, + &memo.Visibility, + &memo.Pinned, + &payloadBytes, + &memo.ParentID, + } + if !find.ExcludeContent { + dests = append(dests, &memo.Content) + } + if err := rows.Scan(dests...); err != nil { + return nil, err + } + payload := &storepb.MemoPayload{} + if err := protojsonUnmarshaler.Unmarshal(payloadBytes, payload); err != nil { + return nil, errors.Wrap(err, "failed to unmarshal payload") + } + memo.Payload = payload + list = append(list, &memo) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} + +func (d *DB) UpdateMemo(ctx context.Context, update *store.UpdateMemo) error { + set, args := []string{}, []any{} + if v := update.UID; v != nil { + set, args = append(set, "`uid` = ?"), append(args, *v) + } + if v := update.CreatedTs; v != nil { + set, args = append(set, "`created_ts` = ?"), append(args, *v) + } + if v := update.UpdatedTs; v != nil { + set, args = append(set, "`updated_ts` = ?"), append(args, *v) + } + if v := update.RowStatus; v != nil { + set, args = append(set, "`row_status` = ?"), append(args, *v) + } + if v := update.Content; v != nil { + set, args = append(set, "`content` = ?"), append(args, *v) + } + if v := update.Visibility; v != nil { + set, args = append(set, "`visibility` = ?"), append(args, *v) + } + if v := update.Pinned; v != nil { + set, args = append(set, "`pinned` = ?"), append(args, *v) + } + if v := update.Payload; v != nil { + payloadBytes, err := protojson.Marshal(v) + if err != nil { + return err + } + set, args = append(set, "`payload` = ?"), append(args, string(payloadBytes)) + } + if len(set) == 0 { + return nil + } + args = append(args, update.ID) + + stmt := "UPDATE `memo` SET " + strings.Join(set, ", ") + " WHERE `id` = ?" + if _, err := d.db.ExecContext(ctx, stmt, args...); err != nil { + return err + } + return nil +} + +func (d *DB) DeleteMemo(ctx context.Context, delete *store.DeleteMemo) error { + where, args := []string{"`id` = ?"}, []any{delete.ID} + stmt := "DELETE FROM `memo` WHERE " + strings.Join(where, " AND ") + result, err := d.db.ExecContext(ctx, stmt, args...) + if err != nil { + return err + } + if _, err := result.RowsAffected(); err != nil { + return err + } + return nil +} diff --git a/store/db/sqlite/memo_filter.go b/store/db/sqlite/memo_filter.go new file mode 100644 index 0000000..9ace5c3 --- /dev/null +++ b/store/db/sqlite/memo_filter.go @@ -0,0 +1,304 @@ +package sqlite + +import ( + "fmt" + "slices" + "strings" + + "github.com/pkg/errors" + exprv1 "google.golang.org/genproto/googleapis/api/expr/v1alpha1" + + "github.com/usememos/memos/plugin/filter" +) + +func (d *DB) ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) error { + return d.convertWithTemplates(ctx, expr) +} + +func (d *DB) convertWithTemplates(ctx *filter.ConvertContext, expr *exprv1.Expr) error { + const dbType = filter.SQLiteTemplate + + if v, ok := expr.ExprKind.(*exprv1.Expr_CallExpr); ok { + switch v.CallExpr.Function { + case "_||_", "_&&_": + if len(v.CallExpr.Args) != 2 { + return errors.Errorf("invalid number of arguments for %s", v.CallExpr.Function) + } + if _, err := ctx.Buffer.WriteString("("); err != nil { + return err + } + if err := d.convertWithTemplates(ctx, v.CallExpr.Args[0]); err != nil { + return err + } + operator := "AND" + if v.CallExpr.Function == "_||_" { + operator = "OR" + } + if _, err := ctx.Buffer.WriteString(fmt.Sprintf(" %s ", operator)); err != nil { + return err + } + if err := d.convertWithTemplates(ctx, v.CallExpr.Args[1]); err != nil { + return err + } + if _, err := ctx.Buffer.WriteString(")"); err != nil { + return err + } + case "!_": + if len(v.CallExpr.Args) != 1 { + return errors.Errorf("invalid number of arguments for %s", v.CallExpr.Function) + } + if _, err := ctx.Buffer.WriteString("NOT ("); err != nil { + return err + } + if err := d.convertWithTemplates(ctx, v.CallExpr.Args[0]); err != nil { + return err + } + if _, err := ctx.Buffer.WriteString(")"); err != nil { + return err + } + case "_==_", "_!=_", "_<_", "_>_", "_<=_", "_>=_": + if len(v.CallExpr.Args) != 2 { + return errors.Errorf("invalid number of arguments for %s", v.CallExpr.Function) + } + // Check if the left side is a function call like size(tags) + if leftCallExpr, ok := v.CallExpr.Args[0].ExprKind.(*exprv1.Expr_CallExpr); ok { + if leftCallExpr.CallExpr.Function == "size" { + // Handle size(tags) comparison + if len(leftCallExpr.CallExpr.Args) != 1 { + return errors.New("size function requires exactly one argument") + } + identifier, err := filter.GetIdentExprName(leftCallExpr.CallExpr.Args[0]) + if err != nil { + return err + } + if identifier != "tags" { + return errors.Errorf("size function only supports 'tags' identifier, got: %s", identifier) + } + value, err := filter.GetExprValue(v.CallExpr.Args[1]) + if err != nil { + return err + } + valueInt, ok := value.(int64) + if !ok { + return errors.New("size comparison value must be an integer") + } + operator := d.getComparisonOperator(v.CallExpr.Function) + + if _, err := ctx.Buffer.WriteString(fmt.Sprintf("%s %s ?", + filter.GetSQL("json_array_length", dbType), operator)); err != nil { + return err + } + ctx.Args = append(ctx.Args, valueInt) + return nil + } + } + + identifier, err := filter.GetIdentExprName(v.CallExpr.Args[0]) + if err != nil { + return err + } + if !slices.Contains([]string{"creator_id", "created_ts", "updated_ts", "visibility", "content", "has_task_list"}, identifier) { + return errors.Errorf("invalid identifier for %s", v.CallExpr.Function) + } + value, err := filter.GetExprValue(v.CallExpr.Args[1]) + if err != nil { + return err + } + operator := d.getComparisonOperator(v.CallExpr.Function) + + if identifier == "created_ts" || identifier == "updated_ts" { + valueInt, ok := value.(int64) + if !ok { + return errors.New("invalid integer timestamp value") + } + + timestampSQL := fmt.Sprintf(filter.GetSQL("timestamp_field", dbType), identifier) + if _, err := ctx.Buffer.WriteString(fmt.Sprintf("%s %s ?", timestampSQL, operator)); err != nil { + return err + } + ctx.Args = append(ctx.Args, valueInt) + } else if identifier == "visibility" || identifier == "content" { + if operator != "=" && operator != "!=" { + return errors.Errorf("invalid operator for %s", v.CallExpr.Function) + } + valueStr, ok := value.(string) + if !ok { + return errors.New("invalid string value") + } + + var sqlTemplate string + if identifier == "visibility" { + sqlTemplate = filter.GetSQL("table_prefix", dbType) + ".`visibility`" + } else if identifier == "content" { + sqlTemplate = filter.GetSQL("table_prefix", dbType) + ".`content`" + } + if _, err := ctx.Buffer.WriteString(fmt.Sprintf("%s %s ?", sqlTemplate, operator)); err != nil { + return err + } + ctx.Args = append(ctx.Args, valueStr) + } else if identifier == "creator_id" { + if operator != "=" && operator != "!=" { + return errors.Errorf("invalid operator for %s", v.CallExpr.Function) + } + valueInt, ok := value.(int64) + if !ok { + return errors.New("invalid int value") + } + + sqlTemplate := filter.GetSQL("table_prefix", dbType) + ".`creator_id`" + if _, err := ctx.Buffer.WriteString(fmt.Sprintf("%s %s ?", sqlTemplate, operator)); err != nil { + return err + } + ctx.Args = append(ctx.Args, valueInt) + } else if identifier == "has_task_list" { + if operator != "=" && operator != "!=" { + return errors.Errorf("invalid operator for %s", v.CallExpr.Function) + } + valueBool, ok := value.(bool) + if !ok { + return errors.New("invalid boolean value for has_task_list") + } + // Use template for boolean comparison + var sqlTemplate string + if operator == "=" { + if valueBool { + sqlTemplate = filter.GetSQL("boolean_true", dbType) + } else { + sqlTemplate = filter.GetSQL("boolean_false", dbType) + } + } else { // operator == "!=" + if valueBool { + sqlTemplate = filter.GetSQL("boolean_not_true", dbType) + } else { + sqlTemplate = filter.GetSQL("boolean_not_false", dbType) + } + } + if _, err := ctx.Buffer.WriteString(sqlTemplate); err != nil { + return err + } + } + case "@in": + if len(v.CallExpr.Args) != 2 { + return errors.Errorf("invalid number of arguments for %s", v.CallExpr.Function) + } + + // Check if this is "element in collection" syntax + if identifier, err := filter.GetIdentExprName(v.CallExpr.Args[1]); err == nil { + // This is "element in collection" - the second argument is the collection + if !slices.Contains([]string{"tags"}, identifier) { + return errors.Errorf("invalid collection identifier for %s: %s", v.CallExpr.Function, identifier) + } + + if identifier == "tags" { + // Handle "element" in tags + element, err := filter.GetConstValue(v.CallExpr.Args[0]) + if err != nil { + return errors.Errorf("first argument must be a constant value for 'element in tags': %v", err) + } + if _, err := ctx.Buffer.WriteString(filter.GetSQL("json_contains_element", dbType)); err != nil { + return err + } + ctx.Args = append(ctx.Args, filter.GetParameterValue(dbType, "json_contains_element", element)) + } + return nil + } + + // Original logic for "identifier in [list]" syntax + identifier, err := filter.GetIdentExprName(v.CallExpr.Args[0]) + if err != nil { + return err + } + if !slices.Contains([]string{"tag", "visibility"}, identifier) { + return errors.Errorf("invalid identifier for %s", v.CallExpr.Function) + } + + values := []any{} + for _, element := range v.CallExpr.Args[1].GetListExpr().Elements { + value, err := filter.GetConstValue(element) + if err != nil { + return err + } + values = append(values, value) + } + if identifier == "tag" { + subconditions := []string{} + args := []any{} + for _, v := range values { + subconditions = append(subconditions, filter.GetSQL("json_contains_tag", dbType)) + args = append(args, filter.GetParameterValue(dbType, "json_contains_tag", v)) + } + if len(subconditions) == 1 { + if _, err := ctx.Buffer.WriteString(subconditions[0]); err != nil { + return err + } + } else { + if _, err := ctx.Buffer.WriteString(fmt.Sprintf("(%s)", strings.Join(subconditions, " OR "))); err != nil { + return err + } + } + ctx.Args = append(ctx.Args, args...) + } else if identifier == "visibility" { + placeholders := filter.FormatPlaceholders(dbType, len(values), 1) + visibilitySQL := fmt.Sprintf(filter.GetSQL("visibility_in", dbType), strings.Join(placeholders, ",")) + if _, err := ctx.Buffer.WriteString(visibilitySQL); err != nil { + return err + } + ctx.Args = append(ctx.Args, values...) + } + case "contains": + if len(v.CallExpr.Args) != 1 { + return errors.Errorf("invalid number of arguments for %s", v.CallExpr.Function) + } + identifier, err := filter.GetIdentExprName(v.CallExpr.Target) + if err != nil { + return err + } + if identifier != "content" { + return errors.Errorf("invalid identifier for %s", v.CallExpr.Function) + } + arg, err := filter.GetConstValue(v.CallExpr.Args[0]) + if err != nil { + return err + } + if _, err := ctx.Buffer.WriteString(filter.GetSQL("content_like", dbType)); err != nil { + return err + } + ctx.Args = append(ctx.Args, fmt.Sprintf("%%%s%%", arg)) + } + } else if v, ok := expr.ExprKind.(*exprv1.Expr_IdentExpr); ok { + identifier := v.IdentExpr.GetName() + if !slices.Contains([]string{"pinned", "has_task_list"}, identifier) { + return errors.Errorf("invalid identifier %s", identifier) + } + if identifier == "pinned" { + if _, err := ctx.Buffer.WriteString(filter.GetSQL("table_prefix", dbType) + ".`pinned` IS TRUE"); err != nil { + return err + } + } else if identifier == "has_task_list" { + // Handle has_task_list as a standalone boolean identifier + if _, err := ctx.Buffer.WriteString(filter.GetSQL("boolean_check", dbType)); err != nil { + return err + } + } + } + return nil +} + +func (*DB) getComparisonOperator(function string) string { + switch function { + case "_==_": + return "=" + case "_!=_": + return "!=" + case "_<_": + return "<" + case "_>_": + return ">" + case "_<=_": + return "<=" + case "_>=_": + return ">=" + default: + return "=" + } +} diff --git a/store/db/sqlite/memo_filter_test.go b/store/db/sqlite/memo_filter_test.go new file mode 100644 index 0000000..d19c98c --- /dev/null +++ b/store/db/sqlite/memo_filter_test.go @@ -0,0 +1,151 @@ +package sqlite + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" + + "github.com/usememos/memos/plugin/filter" +) + +func TestConvertExprToSQL(t *testing.T) { + tests := []struct { + filter string + want string + args []any + }{ + { + filter: `tag in ["tag1", "tag2"]`, + want: "(JSON_EXTRACT(`memo`.`payload`, '$.tags') LIKE ? OR JSON_EXTRACT(`memo`.`payload`, '$.tags') LIKE ?)", + args: []any{`%"tag1"%`, `%"tag2"%`}, + }, + { + filter: `!(tag in ["tag1", "tag2"])`, + want: "NOT ((JSON_EXTRACT(`memo`.`payload`, '$.tags') LIKE ? OR JSON_EXTRACT(`memo`.`payload`, '$.tags') LIKE ?))", + args: []any{`%"tag1"%`, `%"tag2"%`}, + }, + { + filter: `tag in ["tag1", "tag2"] || tag in ["tag3", "tag4"]`, + want: "((JSON_EXTRACT(`memo`.`payload`, '$.tags') LIKE ? OR JSON_EXTRACT(`memo`.`payload`, '$.tags') LIKE ?) OR (JSON_EXTRACT(`memo`.`payload`, '$.tags') LIKE ? OR JSON_EXTRACT(`memo`.`payload`, '$.tags') LIKE ?))", + args: []any{`%"tag1"%`, `%"tag2"%`, `%"tag3"%`, `%"tag4"%`}, + }, + { + filter: `content.contains("memos")`, + want: "`memo`.`content` LIKE ?", + args: []any{"%memos%"}, + }, + { + filter: `visibility in ["PUBLIC"]`, + want: "`memo`.`visibility` IN (?)", + args: []any{"PUBLIC"}, + }, + { + filter: `visibility in ["PUBLIC", "PRIVATE"]`, + want: "`memo`.`visibility` IN (?,?)", + args: []any{"PUBLIC", "PRIVATE"}, + }, + { + filter: `tag in ['tag1'] || content.contains('hello')`, + want: "(JSON_EXTRACT(`memo`.`payload`, '$.tags') LIKE ? OR `memo`.`content` LIKE ?)", + args: []any{`%"tag1"%`, "%hello%"}, + }, + { + filter: `1`, + want: "", + args: []any{}, + }, + { + filter: `pinned`, + want: "`memo`.`pinned` IS TRUE", + args: []any{}, + }, + { + filter: `!pinned`, + want: "NOT (`memo`.`pinned` IS TRUE)", + args: []any{}, + }, + { + filter: `creator_id == 101 || visibility in ["PUBLIC", "PRIVATE"]`, + want: "(`memo`.`creator_id` = ? OR `memo`.`visibility` IN (?,?))", + args: []any{int64(101), "PUBLIC", "PRIVATE"}, + }, + { + filter: `has_task_list`, + want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') IS TRUE", + args: []any{}, + }, + { + filter: `has_task_list == true`, + want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = 1", + args: []any{}, + }, + { + filter: `has_task_list != false`, + want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') != 0", + args: []any{}, + }, + { + filter: `has_task_list == false`, + want: "JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') = 0", + args: []any{}, + }, + { + filter: `!has_task_list`, + want: "NOT (JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') IS TRUE)", + args: []any{}, + }, + { + filter: `has_task_list && pinned`, + want: "(JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') IS TRUE AND `memo`.`pinned` IS TRUE)", + args: []any{}, + }, + { + filter: `has_task_list && content.contains("todo")`, + want: "(JSON_EXTRACT(`memo`.`payload`, '$.property.hasTaskList') IS TRUE AND `memo`.`content` LIKE ?)", + args: []any{"%todo%"}, + }, + { + filter: `created_ts > now() - 60 * 60 * 24`, + want: "`memo`.`created_ts` > ?", + args: []any{time.Now().Unix() - 60*60*24}, + }, + { + filter: `size(tags) == 0`, + want: "JSON_ARRAY_LENGTH(COALESCE(JSON_EXTRACT(`memo`.`payload`, '$.tags'), JSON_ARRAY())) = ?", + args: []any{int64(0)}, + }, + { + filter: `size(tags) > 0`, + want: "JSON_ARRAY_LENGTH(COALESCE(JSON_EXTRACT(`memo`.`payload`, '$.tags'), JSON_ARRAY())) > ?", + args: []any{int64(0)}, + }, + { + filter: `"work" in tags`, + want: "JSON_EXTRACT(`memo`.`payload`, '$.tags') LIKE ?", + args: []any{`%"work"%`}, + }, + { + filter: `size(tags) == 2`, + want: "JSON_ARRAY_LENGTH(COALESCE(JSON_EXTRACT(`memo`.`payload`, '$.tags'), JSON_ARRAY())) = ?", + args: []any{int64(2)}, + }, + } + + for _, tt := range tests { + db := &DB{} + parsedExpr, err := filter.Parse(tt.filter, filter.MemoFilterCELAttributes...) + if err != nil { + t.Logf("Failed to parse filter: %s, error: %v", tt.filter, err) + } + require.NoError(t, err) + convertCtx := filter.NewConvertContext() + err = db.ConvertExprToSQL(convertCtx, parsedExpr.GetExpr()) + if err != nil { + t.Logf("Failed to convert filter: %s, error: %v", tt.filter, err) + } + require.NoError(t, err) + require.Equal(t, tt.want, convertCtx.Buffer.String()) + require.Equal(t, tt.args, convertCtx.Args) + } +} diff --git a/store/db/sqlite/memo_relation.go b/store/db/sqlite/memo_relation.go new file mode 100644 index 0000000..9507b16 --- /dev/null +++ b/store/db/sqlite/memo_relation.go @@ -0,0 +1,125 @@ +package sqlite + +import ( + "context" + "fmt" + "strings" + + "github.com/usememos/memos/plugin/filter" + "github.com/usememos/memos/store" +) + +func (d *DB) UpsertMemoRelation(ctx context.Context, create *store.MemoRelation) (*store.MemoRelation, error) { + stmt := ` + INSERT INTO memo_relation ( + memo_id, + related_memo_id, + type + ) + VALUES (?, ?, ?) + RETURNING memo_id, related_memo_id, type + ` + memoRelation := &store.MemoRelation{} + if err := d.db.QueryRowContext( + ctx, + stmt, + create.MemoID, + create.RelatedMemoID, + create.Type, + ).Scan( + &memoRelation.MemoID, + &memoRelation.RelatedMemoID, + &memoRelation.Type, + ); err != nil { + return nil, err + } + + return memoRelation, nil +} + +func (d *DB) ListMemoRelations(ctx context.Context, find *store.FindMemoRelation) ([]*store.MemoRelation, error) { + where, args := []string{"TRUE"}, []any{} + if find.MemoID != nil { + where, args = append(where, "memo_id = ?"), append(args, find.MemoID) + } + if find.RelatedMemoID != nil { + where, args = append(where, "related_memo_id = ?"), append(args, find.RelatedMemoID) + } + if find.Type != nil { + where, args = append(where, "type = ?"), append(args, find.Type) + } + if find.MemoFilter != nil { + // Parse filter string and return the parsed expression. + // The filter string should be a CEL expression. + parsedExpr, err := filter.Parse(*find.MemoFilter, filter.MemoFilterCELAttributes...) + if err != nil { + return nil, err + } + convertCtx := filter.NewConvertContext() + // ConvertExprToSQL converts the parsed expression to a SQL condition string. + if err := d.ConvertExprToSQL(convertCtx, parsedExpr.GetExpr()); err != nil { + return nil, err + } + condition := convertCtx.Buffer.String() + if condition != "" { + where = append(where, fmt.Sprintf("memo_id IN (SELECT id FROM memo WHERE %s)", condition)) + where = append(where, fmt.Sprintf("related_memo_id IN (SELECT id FROM memo WHERE %s)", condition)) + args = append(args, append(convertCtx.Args, convertCtx.Args...)...) + } + } + + rows, err := d.db.QueryContext(ctx, ` + SELECT + memo_id, + related_memo_id, + type + FROM memo_relation + WHERE `+strings.Join(where, " AND "), args...) + if err != nil { + return nil, err + } + defer rows.Close() + + list := []*store.MemoRelation{} + for rows.Next() { + memoRelation := &store.MemoRelation{} + if err := rows.Scan( + &memoRelation.MemoID, + &memoRelation.RelatedMemoID, + &memoRelation.Type, + ); err != nil { + return nil, err + } + list = append(list, memoRelation) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} + +func (d *DB) DeleteMemoRelation(ctx context.Context, delete *store.DeleteMemoRelation) error { + where, args := []string{"TRUE"}, []any{} + if delete.MemoID != nil { + where, args = append(where, "memo_id = ?"), append(args, delete.MemoID) + } + if delete.RelatedMemoID != nil { + where, args = append(where, "related_memo_id = ?"), append(args, delete.RelatedMemoID) + } + if delete.Type != nil { + where, args = append(where, "type = ?"), append(args, delete.Type) + } + stmt := ` + DELETE FROM memo_relation + WHERE ` + strings.Join(where, " AND ") + result, err := d.db.ExecContext(ctx, stmt, args...) + if err != nil { + return err + } + if _, err = result.RowsAffected(); err != nil { + return err + } + return nil +} diff --git a/store/db/sqlite/migration_history.go b/store/db/sqlite/migration_history.go new file mode 100644 index 0000000..b7ec01b --- /dev/null +++ b/store/db/sqlite/migration_history.go @@ -0,0 +1,57 @@ +package sqlite + +import ( + "context" + + "github.com/usememos/memos/store" +) + +func (d *DB) FindMigrationHistoryList(ctx context.Context, _ *store.FindMigrationHistory) ([]*store.MigrationHistory, error) { + query := "SELECT `version`, `created_ts` FROM `migration_history` ORDER BY `created_ts` DESC" + rows, err := d.db.QueryContext(ctx, query) + if err != nil { + return nil, err + } + defer rows.Close() + + list := make([]*store.MigrationHistory, 0) + for rows.Next() { + var migrationHistory store.MigrationHistory + if err := rows.Scan( + &migrationHistory.Version, + &migrationHistory.CreatedTs, + ); err != nil { + return nil, err + } + + list = append(list, &migrationHistory) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} + +func (d *DB) UpsertMigrationHistory(ctx context.Context, upsert *store.UpsertMigrationHistory) (*store.MigrationHistory, error) { + stmt := ` + INSERT INTO migration_history ( + version + ) + VALUES (?) + ON CONFLICT(version) DO UPDATE + SET + version=EXCLUDED.version + RETURNING version, created_ts + ` + var migrationHistory store.MigrationHistory + if err := d.db.QueryRowContext(ctx, stmt, upsert.Version).Scan( + &migrationHistory.Version, + &migrationHistory.CreatedTs, + ); err != nil { + return nil, err + } + + return &migrationHistory, nil +} diff --git a/store/db/sqlite/reaction.go b/store/db/sqlite/reaction.go new file mode 100644 index 0000000..d95a54c --- /dev/null +++ b/store/db/sqlite/reaction.go @@ -0,0 +1,80 @@ +package sqlite + +import ( + "context" + "strings" + + "github.com/usememos/memos/store" +) + +func (d *DB) UpsertReaction(ctx context.Context, upsert *store.Reaction) (*store.Reaction, error) { + fields := []string{"`creator_id`", "`content_id`", "`reaction_type`"} + placeholder := []string{"?", "?", "?"} + args := []interface{}{upsert.CreatorID, upsert.ContentID, upsert.ReactionType} + stmt := "INSERT INTO `reaction` (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ") RETURNING `id`, `created_ts`" + if err := d.db.QueryRowContext(ctx, stmt, args...).Scan( + &upsert.ID, + &upsert.CreatedTs, + ); err != nil { + return nil, err + } + + reaction := upsert + return reaction, nil +} + +func (d *DB) ListReactions(ctx context.Context, find *store.FindReaction) ([]*store.Reaction, error) { + where, args := []string{"1 = 1"}, []interface{}{} + if find.ID != nil { + where, args = append(where, "id = ?"), append(args, *find.ID) + } + if find.CreatorID != nil { + where, args = append(where, "creator_id = ?"), append(args, *find.CreatorID) + } + if find.ContentID != nil { + where, args = append(where, "content_id = ?"), append(args, *find.ContentID) + } + + rows, err := d.db.QueryContext(ctx, ` + SELECT + id, + created_ts, + creator_id, + content_id, + reaction_type + FROM reaction + WHERE `+strings.Join(where, " AND ")+` + ORDER BY id ASC`, + args..., + ) + if err != nil { + return nil, err + } + defer rows.Close() + + list := []*store.Reaction{} + for rows.Next() { + reaction := &store.Reaction{} + if err := rows.Scan( + &reaction.ID, + &reaction.CreatedTs, + &reaction.CreatorID, + &reaction.ContentID, + &reaction.ReactionType, + ); err != nil { + return nil, err + } + list = append(list, reaction) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} + +func (d *DB) DeleteReaction(ctx context.Context, delete *store.DeleteReaction) error { + _, err := d.db.ExecContext(ctx, "DELETE FROM `reaction` WHERE `id` = ?", delete.ID) + return err +} diff --git a/store/db/sqlite/sqlite.go b/store/db/sqlite/sqlite.go new file mode 100644 index 0000000..3b4a30f --- /dev/null +++ b/store/db/sqlite/sqlite.go @@ -0,0 +1,70 @@ +package sqlite + +import ( + "context" + "database/sql" + + "github.com/pkg/errors" + + // Import the SQLite driver. + _ "modernc.org/sqlite" + + "github.com/usememos/memos/internal/profile" + "github.com/usememos/memos/store" +) + +type DB struct { + db *sql.DB + profile *profile.Profile +} + +// NewDB opens a database specified by its database driver name and a +// driver-specific data source name, usually consisting of at least a +// database name and connection information. +func NewDB(profile *profile.Profile) (store.Driver, error) { + // Ensure a DSN is set before attempting to open the database. + if profile.DSN == "" { + return nil, errors.New("dsn required") + } + + // Connect to the database with some sane settings: + // - No shared-cache: it's obsolete; WAL journal mode is a better solution. + // - No foreign key constraints: it's currently disabled by default, but it's a + // good practice to be explicit and prevent future surprises on SQLite upgrades. + // - Journal mode set to WAL: it's the recommended journal mode for most applications + // as it prevents locking issues. + // + // Notes: + // - When using the `modernc.org/sqlite` driver, each pragma must be prefixed with `_pragma=`. + // + // References: + // - https://pkg.go.dev/modernc.org/sqlite#Driver.Open + // - https://www.sqlite.org/sharedcache.html + // - https://www.sqlite.org/pragma.html + sqliteDB, err := sql.Open("sqlite", profile.DSN+"?_pragma=foreign_keys(0)&_pragma=busy_timeout(10000)&_pragma=journal_mode(WAL)") + if err != nil { + return nil, errors.Wrapf(err, "failed to open db with dsn: %s", profile.DSN) + } + + driver := DB{db: sqliteDB, profile: profile} + + return &driver, nil +} + +func (d *DB) GetDB() *sql.DB { + return d.db +} + +func (d *DB) Close() error { + return d.db.Close() +} + +func (d *DB) IsInitialized(ctx context.Context) (bool, error) { + // Check if the database is initialized by checking if the memo table exists. + var exists bool + err := d.db.QueryRowContext(ctx, "SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type='table' AND name='memo')").Scan(&exists) + if err != nil { + return false, errors.Wrap(err, "failed to check if database is initialized") + } + return exists, nil +} diff --git a/store/db/sqlite/user.go b/store/db/sqlite/user.go new file mode 100644 index 0000000..1003107 --- /dev/null +++ b/store/db/sqlite/user.go @@ -0,0 +1,170 @@ +package sqlite + +import ( + "context" + "fmt" + "strings" + + "github.com/usememos/memos/store" +) + +func (d *DB) CreateUser(ctx context.Context, create *store.User) (*store.User, error) { + fields := []string{"`username`", "`role`", "`email`", "`nickname`", "`password_hash`, `avatar_url`"} + placeholder := []string{"?", "?", "?", "?", "?", "?"} + args := []any{create.Username, create.Role, create.Email, create.Nickname, create.PasswordHash, create.AvatarURL} + stmt := "INSERT INTO user (" + strings.Join(fields, ", ") + ") VALUES (" + strings.Join(placeholder, ", ") + ") RETURNING id, description, created_ts, updated_ts, row_status" + if err := d.db.QueryRowContext(ctx, stmt, args...).Scan( + &create.ID, + &create.Description, + &create.CreatedTs, + &create.UpdatedTs, + &create.RowStatus, + ); err != nil { + return nil, err + } + + return create, nil +} + +func (d *DB) UpdateUser(ctx context.Context, update *store.UpdateUser) (*store.User, error) { + set, args := []string{}, []any{} + if v := update.UpdatedTs; v != nil { + set, args = append(set, "updated_ts = ?"), append(args, *v) + } + if v := update.RowStatus; v != nil { + set, args = append(set, "row_status = ?"), append(args, *v) + } + if v := update.Username; v != nil { + set, args = append(set, "username = ?"), append(args, *v) + } + if v := update.Email; v != nil { + set, args = append(set, "email = ?"), append(args, *v) + } + if v := update.Nickname; v != nil { + set, args = append(set, "nickname = ?"), append(args, *v) + } + if v := update.AvatarURL; v != nil { + set, args = append(set, "avatar_url = ?"), append(args, *v) + } + if v := update.PasswordHash; v != nil { + set, args = append(set, "password_hash = ?"), append(args, *v) + } + if v := update.Description; v != nil { + set, args = append(set, "description = ?"), append(args, *v) + } + if v := update.Role; v != nil { + set, args = append(set, "role = ?"), append(args, *v) + } + args = append(args, update.ID) + + query := ` + UPDATE user + SET ` + strings.Join(set, ", ") + ` + WHERE id = ? + RETURNING id, username, role, email, nickname, password_hash, avatar_url, description, created_ts, updated_ts, row_status + ` + user := &store.User{} + if err := d.db.QueryRowContext(ctx, query, args...).Scan( + &user.ID, + &user.Username, + &user.Role, + &user.Email, + &user.Nickname, + &user.PasswordHash, + &user.AvatarURL, + &user.Description, + &user.CreatedTs, + &user.UpdatedTs, + &user.RowStatus, + ); err != nil { + return nil, err + } + + return user, nil +} + +func (d *DB) ListUsers(ctx context.Context, find *store.FindUser) ([]*store.User, error) { + where, args := []string{"1 = 1"}, []any{} + + if v := find.ID; v != nil { + where, args = append(where, "id = ?"), append(args, *v) + } + if v := find.Username; v != nil { + where, args = append(where, "username = ?"), append(args, *v) + } + if v := find.Role; v != nil { + where, args = append(where, "role = ?"), append(args, *v) + } + if v := find.Email; v != nil { + where, args = append(where, "email = ?"), append(args, *v) + } + if v := find.Nickname; v != nil { + where, args = append(where, "nickname = ?"), append(args, *v) + } + + orderBy := []string{"created_ts DESC", "row_status DESC"} + query := ` + SELECT + id, + username, + role, + email, + nickname, + password_hash, + avatar_url, + description, + created_ts, + updated_ts, + row_status + FROM user + WHERE ` + strings.Join(where, " AND ") + ` ORDER BY ` + strings.Join(orderBy, ", ") + if v := find.Limit; v != nil { + query += fmt.Sprintf(" LIMIT %d", *v) + } + + rows, err := d.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + list := make([]*store.User, 0) + for rows.Next() { + var user store.User + if err := rows.Scan( + &user.ID, + &user.Username, + &user.Role, + &user.Email, + &user.Nickname, + &user.PasswordHash, + &user.AvatarURL, + &user.Description, + &user.CreatedTs, + &user.UpdatedTs, + &user.RowStatus, + ); err != nil { + return nil, err + } + list = append(list, &user) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} + +func (d *DB) DeleteUser(ctx context.Context, delete *store.DeleteUser) error { + result, err := d.db.ExecContext(ctx, ` + DELETE FROM user WHERE id = ? + `, delete.ID) + if err != nil { + return err + } + if _, err := result.RowsAffected(); err != nil { + return err + } + return nil +} diff --git a/store/db/sqlite/user_setting.go b/store/db/sqlite/user_setting.go new file mode 100644 index 0000000..c6ca6f3 --- /dev/null +++ b/store/db/sqlite/user_setting.go @@ -0,0 +1,68 @@ +package sqlite + +import ( + "context" + "strings" + + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func (d *DB) UpsertUserSetting(ctx context.Context, upsert *store.UserSetting) (*store.UserSetting, error) { + stmt := ` + INSERT INTO user_setting ( + user_id, key, value + ) + VALUES (?, ?, ?) + ON CONFLICT(user_id, key) DO UPDATE + SET value = EXCLUDED.value + ` + if _, err := d.db.ExecContext(ctx, stmt, upsert.UserID, upsert.Key.String(), upsert.Value); err != nil { + return nil, err + } + return upsert, nil +} + +func (d *DB) ListUserSettings(ctx context.Context, find *store.FindUserSetting) ([]*store.UserSetting, error) { + where, args := []string{"1 = 1"}, []any{} + + if v := find.Key; v != storepb.UserSetting_KEY_UNSPECIFIED { + where, args = append(where, "key = ?"), append(args, v.String()) + } + if v := find.UserID; v != nil { + where, args = append(where, "user_id = ?"), append(args, *find.UserID) + } + + query := ` + SELECT + user_id, + key, + value + FROM user_setting + WHERE ` + strings.Join(where, " AND ") + rows, err := d.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + userSettingList := make([]*store.UserSetting, 0) + for rows.Next() { + userSetting := &store.UserSetting{} + var keyString string + if err := rows.Scan( + &userSetting.UserID, + &keyString, + &userSetting.Value, + ); err != nil { + return nil, err + } + userSetting.Key = storepb.UserSetting_Key(storepb.UserSetting_Key_value[keyString]) + userSettingList = append(userSettingList, userSetting) + } + if err := rows.Err(); err != nil { + return nil, err + } + + return userSettingList, nil +} diff --git a/store/db/sqlite/workspace_setting.go b/store/db/sqlite/workspace_setting.go new file mode 100644 index 0000000..5969206 --- /dev/null +++ b/store/db/sqlite/workspace_setting.go @@ -0,0 +1,72 @@ +package sqlite + +import ( + "context" + "strings" + + "github.com/usememos/memos/store" +) + +func (d *DB) UpsertWorkspaceSetting(ctx context.Context, upsert *store.WorkspaceSetting) (*store.WorkspaceSetting, error) { + stmt := ` + INSERT INTO system_setting ( + name, value, description + ) + VALUES (?, ?, ?) + ON CONFLICT(name) DO UPDATE + SET + value = EXCLUDED.value, + description = EXCLUDED.description + ` + if _, err := d.db.ExecContext(ctx, stmt, upsert.Name, upsert.Value, upsert.Description); err != nil { + return nil, err + } + + return upsert, nil +} + +func (d *DB) ListWorkspaceSettings(ctx context.Context, find *store.FindWorkspaceSetting) ([]*store.WorkspaceSetting, error) { + where, args := []string{"1 = 1"}, []any{} + if find.Name != "" { + where, args = append(where, "name = ?"), append(args, find.Name) + } + + query := ` + SELECT + name, + value, + description + FROM system_setting + WHERE ` + strings.Join(where, " AND ") + + rows, err := d.db.QueryContext(ctx, query, args...) + if err != nil { + return nil, err + } + defer rows.Close() + + list := []*store.WorkspaceSetting{} + for rows.Next() { + systemSettingMessage := &store.WorkspaceSetting{} + if err := rows.Scan( + &systemSettingMessage.Name, + &systemSettingMessage.Value, + &systemSettingMessage.Description, + ); err != nil { + return nil, err + } + list = append(list, systemSettingMessage) + } + + if err := rows.Err(); err != nil { + return nil, err + } + + return list, nil +} + +func (d *DB) DeleteWorkspaceSetting(ctx context.Context, delete *store.DeleteWorkspaceSetting) error { + stmt := "DELETE FROM system_setting WHERE name = ?" + _, err := d.db.ExecContext(ctx, stmt, delete.Name) + return err +} diff --git a/store/driver.go b/store/driver.go new file mode 100644 index 0000000..48fd628 --- /dev/null +++ b/store/driver.go @@ -0,0 +1,79 @@ +package store + +import ( + "context" + "database/sql" + + exprv1 "google.golang.org/genproto/googleapis/api/expr/v1alpha1" + + "github.com/usememos/memos/plugin/filter" +) + +// Driver is an interface for store driver. +// It contains all methods that store database driver should implement. +type Driver interface { + GetDB() *sql.DB + Close() error + + IsInitialized(ctx context.Context) (bool, error) + + // MigrationHistory model related methods. + FindMigrationHistoryList(ctx context.Context, find *FindMigrationHistory) ([]*MigrationHistory, error) + UpsertMigrationHistory(ctx context.Context, upsert *UpsertMigrationHistory) (*MigrationHistory, error) + + // Activity model related methods. + CreateActivity(ctx context.Context, create *Activity) (*Activity, error) + ListActivities(ctx context.Context, find *FindActivity) ([]*Activity, error) + + // Attachment model related methods. + CreateAttachment(ctx context.Context, create *Attachment) (*Attachment, error) + ListAttachments(ctx context.Context, find *FindAttachment) ([]*Attachment, error) + UpdateAttachment(ctx context.Context, update *UpdateAttachment) error + DeleteAttachment(ctx context.Context, delete *DeleteAttachment) error + + // Memo model related methods. + CreateMemo(ctx context.Context, create *Memo) (*Memo, error) + ListMemos(ctx context.Context, find *FindMemo) ([]*Memo, error) + UpdateMemo(ctx context.Context, update *UpdateMemo) error + DeleteMemo(ctx context.Context, delete *DeleteMemo) error + + // MemoRelation model related methods. + UpsertMemoRelation(ctx context.Context, create *MemoRelation) (*MemoRelation, error) + ListMemoRelations(ctx context.Context, find *FindMemoRelation) ([]*MemoRelation, error) + DeleteMemoRelation(ctx context.Context, delete *DeleteMemoRelation) error + + // WorkspaceSetting model related methods. + UpsertWorkspaceSetting(ctx context.Context, upsert *WorkspaceSetting) (*WorkspaceSetting, error) + ListWorkspaceSettings(ctx context.Context, find *FindWorkspaceSetting) ([]*WorkspaceSetting, error) + DeleteWorkspaceSetting(ctx context.Context, delete *DeleteWorkspaceSetting) error + + // User model related methods. + CreateUser(ctx context.Context, create *User) (*User, error) + UpdateUser(ctx context.Context, update *UpdateUser) (*User, error) + ListUsers(ctx context.Context, find *FindUser) ([]*User, error) + DeleteUser(ctx context.Context, delete *DeleteUser) error + + // UserSetting model related methods. + UpsertUserSetting(ctx context.Context, upsert *UserSetting) (*UserSetting, error) + ListUserSettings(ctx context.Context, find *FindUserSetting) ([]*UserSetting, error) + + // IdentityProvider model related methods. + CreateIdentityProvider(ctx context.Context, create *IdentityProvider) (*IdentityProvider, error) + ListIdentityProviders(ctx context.Context, find *FindIdentityProvider) ([]*IdentityProvider, error) + UpdateIdentityProvider(ctx context.Context, update *UpdateIdentityProvider) (*IdentityProvider, error) + DeleteIdentityProvider(ctx context.Context, delete *DeleteIdentityProvider) error + + // Inbox model related methods. + CreateInbox(ctx context.Context, create *Inbox) (*Inbox, error) + ListInboxes(ctx context.Context, find *FindInbox) ([]*Inbox, error) + UpdateInbox(ctx context.Context, update *UpdateInbox) (*Inbox, error) + DeleteInbox(ctx context.Context, delete *DeleteInbox) error + + // Reaction model related methods. + UpsertReaction(ctx context.Context, create *Reaction) (*Reaction, error) + ListReactions(ctx context.Context, find *FindReaction) ([]*Reaction, error) + DeleteReaction(ctx context.Context, delete *DeleteReaction) error + + // Shortcut related methods. + ConvertExprToSQL(ctx *filter.ConvertContext, expr *exprv1.Expr) error +} diff --git a/store/idp.go b/store/idp.go new file mode 100644 index 0000000..88ab6f0 --- /dev/null +++ b/store/idp.go @@ -0,0 +1,182 @@ +package store + +import ( + "context" + + "github.com/pkg/errors" + "google.golang.org/protobuf/encoding/protojson" + + storepb "github.com/usememos/memos/proto/gen/store" +) + +type IdentityProvider struct { + ID int32 + Name string + Type storepb.IdentityProvider_Type + IdentifierFilter string + Config string +} + +type FindIdentityProvider struct { + ID *int32 +} + +type UpdateIdentityProvider struct { + ID int32 + Name *string + IdentifierFilter *string + Config *string +} + +type DeleteIdentityProvider struct { + ID int32 +} + +func (s *Store) CreateIdentityProvider(ctx context.Context, create *storepb.IdentityProvider) (*storepb.IdentityProvider, error) { + raw, err := convertIdentityProviderToRaw(create) + if err != nil { + return nil, err + } + identityProviderRaw, err := s.driver.CreateIdentityProvider(ctx, raw) + if err != nil { + return nil, err + } + + identityProvider, err := convertIdentityProviderFromRaw(identityProviderRaw) + if err != nil { + return nil, err + } + return identityProvider, nil +} + +func (s *Store) ListIdentityProviders(ctx context.Context, find *FindIdentityProvider) ([]*storepb.IdentityProvider, error) { + list, err := s.driver.ListIdentityProviders(ctx, find) + if err != nil { + return nil, err + } + + identityProviders := []*storepb.IdentityProvider{} + for _, raw := range list { + identityProvider, err := convertIdentityProviderFromRaw(raw) + if err != nil { + return nil, err + } + identityProviders = append(identityProviders, identityProvider) + } + return identityProviders, nil +} + +func (s *Store) GetIdentityProvider(ctx context.Context, find *FindIdentityProvider) (*storepb.IdentityProvider, error) { + list, err := s.ListIdentityProviders(ctx, find) + if err != nil { + return nil, err + } + if len(list) == 0 { + return nil, nil + } + if len(list) > 1 { + return nil, errors.Errorf("Found multiple identity providers with ID %d", *find.ID) + } + + identityProvider := list[0] + return identityProvider, nil +} + +type UpdateIdentityProviderV1 struct { + ID int32 + Type storepb.IdentityProvider_Type + Name *string + IdentifierFilter *string + Config *storepb.IdentityProviderConfig +} + +func (s *Store) UpdateIdentityProvider(ctx context.Context, update *UpdateIdentityProviderV1) (*storepb.IdentityProvider, error) { + updateRaw := &UpdateIdentityProvider{ + ID: update.ID, + } + if update.Name != nil { + updateRaw.Name = update.Name + } + if update.IdentifierFilter != nil { + updateRaw.IdentifierFilter = update.IdentifierFilter + } + if update.Config != nil { + configRaw, err := convertIdentityProviderConfigToRaw(update.Type, update.Config) + if err != nil { + return nil, err + } + updateRaw.Config = &configRaw + } + identityProviderRaw, err := s.driver.UpdateIdentityProvider(ctx, updateRaw) + if err != nil { + return nil, err + } + + identityProvider, err := convertIdentityProviderFromRaw(identityProviderRaw) + if err != nil { + return nil, err + } + return identityProvider, nil +} + +func (s *Store) DeleteIdentityProvider(ctx context.Context, delete *DeleteIdentityProvider) error { + err := s.driver.DeleteIdentityProvider(ctx, delete) + if err != nil { + return err + } + return nil +} + +func convertIdentityProviderFromRaw(raw *IdentityProvider) (*storepb.IdentityProvider, error) { + identityProvider := &storepb.IdentityProvider{ + Id: raw.ID, + Name: raw.Name, + Type: raw.Type, + IdentifierFilter: raw.IdentifierFilter, + } + config, err := convertIdentityProviderConfigFromRaw(identityProvider.Type, raw.Config) + if err != nil { + return nil, err + } + identityProvider.Config = config + return identityProvider, nil +} + +func convertIdentityProviderToRaw(identityProvider *storepb.IdentityProvider) (*IdentityProvider, error) { + raw := &IdentityProvider{ + ID: identityProvider.Id, + Name: identityProvider.Name, + Type: identityProvider.Type, + IdentifierFilter: identityProvider.IdentifierFilter, + } + configRaw, err := convertIdentityProviderConfigToRaw(identityProvider.Type, identityProvider.Config) + if err != nil { + return nil, err + } + raw.Config = configRaw + return raw, nil +} + +func convertIdentityProviderConfigFromRaw(identityProviderType storepb.IdentityProvider_Type, raw string) (*storepb.IdentityProviderConfig, error) { + config := &storepb.IdentityProviderConfig{} + if identityProviderType == storepb.IdentityProvider_OAUTH2 { + oauth2Config := &storepb.OAuth2Config{} + if err := protojsonUnmarshaler.Unmarshal([]byte(raw), oauth2Config); err != nil { + return nil, errors.Wrap(err, "Failed to unmarshal OAuth2Config") + } + config.Config = &storepb.IdentityProviderConfig_Oauth2Config{Oauth2Config: oauth2Config} + } + return config, nil +} + +func convertIdentityProviderConfigToRaw(identityProviderType storepb.IdentityProvider_Type, config *storepb.IdentityProviderConfig) (string, error) { + raw := "" + if identityProviderType == storepb.IdentityProvider_OAUTH2 { + bytes, err := protojson.Marshal(config.GetOauth2Config()) + if err != nil { + return "", errors.Wrap(err, "Failed to marshal OAuth2Config") + } + raw = string(bytes) + } + return raw, nil +} diff --git a/store/inbox.go b/store/inbox.go new file mode 100644 index 0000000..e68ce22 --- /dev/null +++ b/store/inbox.go @@ -0,0 +1,64 @@ +package store + +import ( + "context" + + storepb "github.com/usememos/memos/proto/gen/store" +) + +// InboxStatus is the status for an inbox. +type InboxStatus string + +const ( + UNREAD InboxStatus = "UNREAD" + ARCHIVED InboxStatus = "ARCHIVED" +) + +func (s InboxStatus) String() string { + return string(s) +} + +type Inbox struct { + ID int32 + CreatedTs int64 + SenderID int32 + ReceiverID int32 + Status InboxStatus + Message *storepb.InboxMessage +} + +type UpdateInbox struct { + ID int32 + Status InboxStatus +} + +type FindInbox struct { + ID *int32 + SenderID *int32 + ReceiverID *int32 + Status *InboxStatus + + // Pagination + Limit *int + Offset *int +} + +type DeleteInbox struct { + ID int32 +} + +func (s *Store) CreateInbox(ctx context.Context, create *Inbox) (*Inbox, error) { + return s.driver.CreateInbox(ctx, create) +} + +func (s *Store) ListInboxes(ctx context.Context, find *FindInbox) ([]*Inbox, error) { + return s.driver.ListInboxes(ctx, find) +} + +func (s *Store) UpdateInbox(ctx context.Context, update *UpdateInbox) (*Inbox, error) { + return s.driver.UpdateInbox(ctx, update) +} + +func (s *Store) DeleteInbox(ctx context.Context, delete *DeleteInbox) error { + return s.driver.DeleteInbox(ctx, delete) +} diff --git a/store/memo.go b/store/memo.go new file mode 100644 index 0000000..0441e68 --- /dev/null +++ b/store/memo.go @@ -0,0 +1,147 @@ +package store + +import ( + "context" + "errors" + + "github.com/usememos/memos/internal/base" + + storepb "github.com/usememos/memos/proto/gen/store" +) + +// Visibility is the type of a visibility. +type Visibility string + +const ( + // Public is the PUBLIC visibility. + Public Visibility = "PUBLIC" + // Protected is the PROTECTED visibility. + Protected Visibility = "PROTECTED" + // Private is the PRIVATE visibility. + Private Visibility = "PRIVATE" +) + +func (v Visibility) String() string { + switch v { + case Public: + return "PUBLIC" + case Protected: + return "PROTECTED" + case Private: + return "PRIVATE" + } + return "PRIVATE" +} + +type Memo struct { + // ID is the system generated unique identifier for the memo. + ID int32 + // UID is the user defined unique identifier for the memo. + UID string + + // Standard fields + RowStatus RowStatus + CreatorID int32 + CreatedTs int64 + UpdatedTs int64 + + // Domain specific fields + Content string + Visibility Visibility + Pinned bool + Payload *storepb.MemoPayload + + // Composed fields + ParentID *int32 +} + +type FindMemo struct { + ID *int32 + UID *string + + // Standard fields + RowStatus *RowStatus + CreatorID *int32 + CreatedTsAfter *int64 + CreatedTsBefore *int64 + UpdatedTsAfter *int64 + UpdatedTsBefore *int64 + + // Domain specific fields + ContentSearch []string + VisibilityList []Visibility + Pinned *bool + PayloadFind *FindMemoPayload + ExcludeContent bool + ExcludeComments bool + Filter *string + + // Pagination + Limit *int + Offset *int + + // Ordering + OrderByUpdatedTs bool + OrderByPinned bool + OrderByTimeAsc bool +} + +type FindMemoPayload struct { + Raw *string + TagSearch []string + HasLink bool + HasTaskList bool + HasCode bool + HasIncompleteTasks bool +} + +type UpdateMemo struct { + ID int32 + UID *string + CreatedTs *int64 + UpdatedTs *int64 + RowStatus *RowStatus + Content *string + Visibility *Visibility + Pinned *bool + Payload *storepb.MemoPayload +} + +type DeleteMemo struct { + ID int32 +} + +func (s *Store) CreateMemo(ctx context.Context, create *Memo) (*Memo, error) { + if !base.UIDMatcher.MatchString(create.UID) { + return nil, errors.New("invalid uid") + } + return s.driver.CreateMemo(ctx, create) +} + +func (s *Store) ListMemos(ctx context.Context, find *FindMemo) ([]*Memo, error) { + return s.driver.ListMemos(ctx, find) +} + +func (s *Store) GetMemo(ctx context.Context, find *FindMemo) (*Memo, error) { + list, err := s.ListMemos(ctx, find) + if err != nil { + return nil, err + } + if len(list) == 0 { + return nil, nil + } + + memo := list[0] + return memo, nil +} + +func (s *Store) UpdateMemo(ctx context.Context, update *UpdateMemo) error { + if update.UID != nil && !base.UIDMatcher.MatchString(*update.UID) { + return errors.New("invalid uid") + } + return s.driver.UpdateMemo(ctx, update) +} + +func (s *Store) DeleteMemo(ctx context.Context, delete *DeleteMemo) error { + return s.driver.DeleteMemo(ctx, delete) +} diff --git a/store/memo_relation.go b/store/memo_relation.go new file mode 100644 index 0000000..61b0228 --- /dev/null +++ b/store/memo_relation.go @@ -0,0 +1,45 @@ +package store + +import ( + "context" +) + +type MemoRelationType string + +const ( + // MemoRelationReference is the type for a reference memo relation. + MemoRelationReference MemoRelationType = "REFERENCE" + // MemoRelationComment is the type for a comment memo relation. + MemoRelationComment MemoRelationType = "COMMENT" +) + +type MemoRelation struct { + MemoID int32 + RelatedMemoID int32 + Type MemoRelationType +} + +type FindMemoRelation struct { + MemoID *int32 + RelatedMemoID *int32 + Type *MemoRelationType + MemoFilter *string +} + +type DeleteMemoRelation struct { + MemoID *int32 + RelatedMemoID *int32 + Type *MemoRelationType +} + +func (s *Store) UpsertMemoRelation(ctx context.Context, create *MemoRelation) (*MemoRelation, error) { + return s.driver.UpsertMemoRelation(ctx, create) +} + +func (s *Store) ListMemoRelations(ctx context.Context, find *FindMemoRelation) ([]*MemoRelation, error) { + return s.driver.ListMemoRelations(ctx, find) +} + +func (s *Store) DeleteMemoRelation(ctx context.Context, delete *DeleteMemoRelation) error { + return s.driver.DeleteMemoRelation(ctx, delete) +} diff --git a/store/migration/mysql/0.17/00__inbox.sql b/store/migration/mysql/0.17/00__inbox.sql new file mode 100644 index 0000000..57635f4 --- /dev/null +++ b/store/migration/mysql/0.17/00__inbox.sql @@ -0,0 +1,9 @@ +-- inbox +CREATE TABLE `inbox` ( + `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `created_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `sender_id` INT NOT NULL, + `receiver_id` INT NOT NULL, + `status` TEXT NOT NULL, + `message` TEXT NOT NULL +); diff --git a/store/migration/mysql/0.17/01__delete_activity.sql b/store/migration/mysql/0.17/01__delete_activity.sql new file mode 100644 index 0000000..9ea4e0f --- /dev/null +++ b/store/migration/mysql/0.17/01__delete_activity.sql @@ -0,0 +1 @@ +DELETE FROM `activity`; diff --git a/store/migration/mysql/0.18/00__extend_text.sql b/store/migration/mysql/0.18/00__extend_text.sql new file mode 100644 index 0000000..21f3bcb --- /dev/null +++ b/store/migration/mysql/0.18/00__extend_text.sql @@ -0,0 +1,3 @@ +ALTER TABLE `system_setting` MODIFY `value` LONGTEXT NOT NULL; +ALTER TABLE `user_setting` MODIFY `value` LONGTEXT NOT NULL; +ALTER TABLE `user` MODIFY `avatar_url` LONGTEXT NOT NULL; diff --git a/store/migration/mysql/0.18/01__webhook.sql b/store/migration/mysql/0.18/01__webhook.sql new file mode 100644 index 0000000..937f333 --- /dev/null +++ b/store/migration/mysql/0.18/01__webhook.sql @@ -0,0 +1,10 @@ +-- webhook +CREATE TABLE `webhook` ( + `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `created_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `row_status` VARCHAR(256) NOT NULL DEFAULT 'NORMAL', + `creator_id` INT NOT NULL, + `name` TEXT NOT NULL, + `url` TEXT NOT NULL +); diff --git a/store/migration/mysql/0.18/02__user_setting.sql b/store/migration/mysql/0.18/02__user_setting.sql new file mode 100644 index 0000000..99709e4 --- /dev/null +++ b/store/migration/mysql/0.18/02__user_setting.sql @@ -0,0 +1,4 @@ +UPDATE `user_setting` SET `key` = 'USER_SETTING_LOCALE', `value` = REPLACE(`value`, '"', '') WHERE `key` = 'locale'; +UPDATE `user_setting` SET `key` = 'USER_SETTING_APPEARANCE', `value` = REPLACE(`value`, '"', '') WHERE `key` = 'appearance'; +UPDATE `user_setting` SET `key` = 'USER_SETTING_MEMO_VISIBILITY', `value` = REPLACE(`value`, '"', '') WHERE `key` = 'memo-visibility'; +UPDATE `user_setting` SET `key` = 'USER_SETTING_TELEGRAM_USER_ID', `value` = REPLACE(`value`, '"', '') WHERE `key` = 'telegram-user-id'; diff --git a/store/migration/mysql/0.19/00__add_resource_name.sql b/store/migration/mysql/0.19/00__add_resource_name.sql new file mode 100644 index 0000000..4625691 --- /dev/null +++ b/store/migration/mysql/0.19/00__add_resource_name.sql @@ -0,0 +1,15 @@ +ALTER TABLE `memo` ADD COLUMN `resource_name` VARCHAR(256) AFTER `id`; + +UPDATE `memo` SET `resource_name` = uuid(); + +ALTER TABLE `memo` MODIFY COLUMN `resource_name` VARCHAR(256) NOT NULL; + +CREATE UNIQUE INDEX idx_memo_resource_name ON `memo` (`resource_name`); + +ALTER TABLE `resource` ADD COLUMN `resource_name` VARCHAR(256) AFTER `id`; + +UPDATE `resource` SET `resource_name` = uuid(); + +ALTER TABLE `resource` MODIFY COLUMN `resource_name` VARCHAR(256) NOT NULL; + +CREATE UNIQUE INDEX idx_resource_resource_name ON `resource` (`resource_name`); diff --git a/store/migration/mysql/0.20/00__reaction.sql b/store/migration/mysql/0.20/00__reaction.sql new file mode 100644 index 0000000..a25a1ba --- /dev/null +++ b/store/migration/mysql/0.20/00__reaction.sql @@ -0,0 +1,9 @@ +-- reaction +CREATE TABLE `reaction` ( + `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `created_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `creator_id` INT NOT NULL, + `content_id` VARCHAR(256) NOT NULL, + `reaction_type` VARCHAR(256) NOT NULL, + UNIQUE(`creator_id`,`content_id`,`reaction_type`) +); diff --git a/store/migration/mysql/0.21/00__user_description.sql b/store/migration/mysql/0.21/00__user_description.sql new file mode 100644 index 0000000..44df868 --- /dev/null +++ b/store/migration/mysql/0.21/00__user_description.sql @@ -0,0 +1 @@ +ALTER TABLE `user` ADD COLUMN `description` VARCHAR(256) NOT NULL DEFAULT ''; diff --git a/store/migration/mysql/0.21/01__rename_uid.sql b/store/migration/mysql/0.21/01__rename_uid.sql new file mode 100644 index 0000000..78ab079 --- /dev/null +++ b/store/migration/mysql/0.21/01__rename_uid.sql @@ -0,0 +1,3 @@ +ALTER TABLE `memo` RENAME COLUMN `resource_name` TO `uid`; + +ALTER TABLE `resource` RENAME COLUMN `resource_name` TO `uid`; diff --git a/store/migration/mysql/0.22/00__resource_storage_type.sql b/store/migration/mysql/0.22/00__resource_storage_type.sql new file mode 100644 index 0000000..f598d81 --- /dev/null +++ b/store/migration/mysql/0.22/00__resource_storage_type.sql @@ -0,0 +1,11 @@ +ALTER TABLE `resource` ADD COLUMN `storage_type` VARCHAR(256) NOT NULL DEFAULT ''; +ALTER TABLE `resource` ADD COLUMN `reference` VARCHAR(256) NOT NULL DEFAULT ''; +ALTER TABLE `resource` ADD COLUMN `payload` TEXT NOT NULL; + +UPDATE `resource` SET `payload` = '{}'; + +UPDATE `resource` SET `storage_type` = 'LOCAL', `reference` = `internal_path` WHERE `internal_path` IS NOT NULL AND `internal_path` != ''; +UPDATE `resource` SET `storage_type` = 'EXTERNAL', `reference` = `external_link` WHERE `external_link` IS NOT NULL AND `external_link` != ''; + +ALTER TABLE `resource` DROP COLUMN `internal_path`; +ALTER TABLE `resource` DROP COLUMN `external_link`; diff --git a/store/migration/mysql/0.22/01__memo_tags.sql b/store/migration/mysql/0.22/01__memo_tags.sql new file mode 100644 index 0000000..6812f76 --- /dev/null +++ b/store/migration/mysql/0.22/01__memo_tags.sql @@ -0,0 +1,3 @@ +ALTER TABLE `memo` ADD COLUMN `tags_temp` JSON; +UPDATE `memo` SET `tags_temp` = '[]'; +ALTER TABLE `memo` CHANGE COLUMN `tags_temp` `tags` JSON NOT NULL; diff --git a/store/migration/mysql/0.22/02__memo_payload.sql b/store/migration/mysql/0.22/02__memo_payload.sql new file mode 100644 index 0000000..24e7b84 --- /dev/null +++ b/store/migration/mysql/0.22/02__memo_payload.sql @@ -0,0 +1,3 @@ +ALTER TABLE `memo` ADD COLUMN `payload_temp` JSON; +UPDATE `memo` SET `payload_temp` = '{}'; +ALTER TABLE `memo` CHANGE COLUMN `payload_temp` `payload` JSON NOT NULL; diff --git a/store/migration/mysql/0.22/03__drop_tag.sql b/store/migration/mysql/0.22/03__drop_tag.sql new file mode 100644 index 0000000..ba302fa --- /dev/null +++ b/store/migration/mysql/0.22/03__drop_tag.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS `tag`; diff --git a/store/migration/mysql/0.23/00__reactions.sql b/store/migration/mysql/0.23/00__reactions.sql new file mode 100644 index 0000000..13979d1 --- /dev/null +++ b/store/migration/mysql/0.23/00__reactions.sql @@ -0,0 +1,12 @@ +UPDATE `reaction` SET `reaction_type` = '👍' WHERE `reaction_type` = 'THUMBS_UP'; +UPDATE `reaction` SET `reaction_type` = '👎' WHERE `reaction_type` = 'THUMBS_DOWN'; +UPDATE `reaction` SET `reaction_type` = '💛' WHERE `reaction_type` = 'HEART'; +UPDATE `reaction` SET `reaction_type` = '🔥' WHERE `reaction_type` = 'FIRE'; +UPDATE `reaction` SET `reaction_type` = '👏' WHERE `reaction_type` = 'CLAPPING_HANDS'; +UPDATE `reaction` SET `reaction_type` = '😂' WHERE `reaction_type` = 'LAUGH'; +UPDATE `reaction` SET `reaction_type` = '👌' WHERE `reaction_type` = 'OK_HAND'; +UPDATE `reaction` SET `reaction_type` = '🚀' WHERE `reaction_type` = 'ROCKET'; +UPDATE `reaction` SET `reaction_type` = '👀' WHERE `reaction_type` = 'EYES'; +UPDATE `reaction` SET `reaction_type` = '🤔' WHERE `reaction_type` = 'THINKING_FACE'; +UPDATE `reaction` SET `reaction_type` = '🤡' WHERE `reaction_type` = 'CLOWN_FACE'; +UPDATE `reaction` SET `reaction_type` = '❓' WHERE `reaction_type` = 'QUESTION_MARK'; diff --git a/store/migration/mysql/0.24/00__memo.sql b/store/migration/mysql/0.24/00__memo.sql new file mode 100644 index 0000000..64ee71f --- /dev/null +++ b/store/migration/mysql/0.24/00__memo.sql @@ -0,0 +1,2 @@ +-- Drop deprecated tags column. +ALTER TABLE `memo` DROP COLUMN `tags`; \ No newline at end of file diff --git a/store/migration/mysql/0.24/01__memo_pinned.sql b/store/migration/mysql/0.24/01__memo_pinned.sql new file mode 100644 index 0000000..ee4c0fc --- /dev/null +++ b/store/migration/mysql/0.24/01__memo_pinned.sql @@ -0,0 +1,8 @@ +-- Add pinned column. +ALTER TABLE `memo` ADD COLUMN `pinned` BOOLEAN NOT NULL DEFAULT FALSE; + +-- Update pinned column from memo_organizer. +UPDATE memo +JOIN memo_organizer ON memo.id = memo_organizer.memo_id +SET memo.pinned = TRUE +WHERE memo_organizer.pinned = 1; diff --git a/store/migration/mysql/0.24/02__s3_reference_length.sql b/store/migration/mysql/0.24/02__s3_reference_length.sql new file mode 100644 index 0000000..f7da77e --- /dev/null +++ b/store/migration/mysql/0.24/02__s3_reference_length.sql @@ -0,0 +1,2 @@ +-- https://github.com/usememos/memos/issues/4322 +ALTER TABLE `resource` MODIFY `reference` TEXT NOT NULL DEFAULT (''); diff --git a/store/migration/mysql/0.25/00__remove_webhook.sql b/store/migration/mysql/0.25/00__remove_webhook.sql new file mode 100644 index 0000000..f60efc0 --- /dev/null +++ b/store/migration/mysql/0.25/00__remove_webhook.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS webhook; diff --git a/store/migration/mysql/LATEST.sql b/store/migration/mysql/LATEST.sql new file mode 100644 index 0000000..e5ae04d --- /dev/null +++ b/store/migration/mysql/LATEST.sql @@ -0,0 +1,121 @@ +-- migration_history +CREATE TABLE `migration_history` ( + `version` VARCHAR(256) NOT NULL PRIMARY KEY, + `created_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- system_setting +CREATE TABLE `system_setting` ( + `name` VARCHAR(256) NOT NULL PRIMARY KEY, + `value` LONGTEXT NOT NULL, + `description` TEXT NOT NULL +); + +-- user +CREATE TABLE `user` ( + `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `created_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `row_status` VARCHAR(256) NOT NULL DEFAULT 'NORMAL', + `username` VARCHAR(256) NOT NULL UNIQUE, + `role` VARCHAR(256) NOT NULL DEFAULT 'USER', + `email` VARCHAR(256) NOT NULL DEFAULT '', + `nickname` VARCHAR(256) NOT NULL DEFAULT '', + `password_hash` VARCHAR(256) NOT NULL, + `avatar_url` LONGTEXT NOT NULL, + `description` VARCHAR(256) NOT NULL DEFAULT '' +); + +-- user_setting +CREATE TABLE `user_setting` ( + `user_id` INT NOT NULL, + `key` VARCHAR(256) NOT NULL, + `value` LONGTEXT NOT NULL, + UNIQUE(`user_id`,`key`) +); + +-- memo +CREATE TABLE `memo` ( + `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `uid` VARCHAR(256) NOT NULL UNIQUE, + `creator_id` INT NOT NULL, + `created_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `row_status` VARCHAR(256) NOT NULL DEFAULT 'NORMAL', + `content` TEXT NOT NULL, + `visibility` VARCHAR(256) NOT NULL DEFAULT 'PRIVATE', + `pinned` BOOLEAN NOT NULL DEFAULT FALSE, + `payload` JSON NOT NULL +); + +-- memo_organizer +CREATE TABLE `memo_organizer` ( + `memo_id` INT NOT NULL, + `user_id` INT NOT NULL, + `pinned` INT NOT NULL DEFAULT '0', + UNIQUE(`memo_id`,`user_id`) +); + +-- memo_relation +CREATE TABLE `memo_relation` ( + `memo_id` INT NOT NULL, + `related_memo_id` INT NOT NULL, + `type` VARCHAR(256) NOT NULL, + UNIQUE(`memo_id`,`related_memo_id`,`type`) +); + +-- resource +CREATE TABLE `resource` ( + `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `uid` VARCHAR(256) NOT NULL UNIQUE, + `creator_id` INT NOT NULL, + `created_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `filename` TEXT NOT NULL, + `blob` MEDIUMBLOB, + `type` VARCHAR(256) NOT NULL DEFAULT '', + `size` INT NOT NULL DEFAULT '0', + `memo_id` INT DEFAULT NULL, + `storage_type` VARCHAR(256) NOT NULL DEFAULT '', + `reference` TEXT NOT NULL DEFAULT (''), + `payload` TEXT NOT NULL +); + +-- activity +CREATE TABLE `activity` ( + `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `creator_id` INT NOT NULL, + `created_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `type` VARCHAR(256) NOT NULL DEFAULT '', + `level` VARCHAR(256) NOT NULL DEFAULT 'INFO', + `payload` TEXT NOT NULL +); + +-- idp +CREATE TABLE `idp` ( + `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `name` TEXT NOT NULL, + `type` TEXT NOT NULL, + `identifier_filter` VARCHAR(256) NOT NULL DEFAULT '', + `config` TEXT NOT NULL +); + +-- inbox +CREATE TABLE `inbox` ( + `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `created_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `sender_id` INT NOT NULL, + `receiver_id` INT NOT NULL, + `status` TEXT NOT NULL, + `message` TEXT NOT NULL +); + +-- reaction +CREATE TABLE `reaction` ( + `id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY, + `created_ts` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + `creator_id` INT NOT NULL, + `content_id` VARCHAR(256) NOT NULL, + `reaction_type` VARCHAR(256) NOT NULL, + UNIQUE(`creator_id`,`content_id`,`reaction_type`) +); diff --git a/store/migration/postgres/0.19/00__add_resource_name.sql b/store/migration/postgres/0.19/00__add_resource_name.sql new file mode 100644 index 0000000..aa5b560 --- /dev/null +++ b/store/migration/postgres/0.19/00__add_resource_name.sql @@ -0,0 +1,15 @@ +ALTER TABLE memo ADD COLUMN resource_name TEXT; + +UPDATE memo SET resource_name = uuid_in(md5(random()::text || random()::text)::cstring); + +ALTER TABLE memo ALTER COLUMN resource_name SET NOT NULL; + +CREATE UNIQUE INDEX idx_memo_resource_name ON memo (resource_name); + +ALTER TABLE resource ADD COLUMN resource_name TEXT; + +UPDATE resource SET resource_name = uuid_in(md5(random()::text || random()::text)::cstring); + +ALTER TABLE resource ALTER COLUMN resource_name SET NOT NULL; + +CREATE UNIQUE INDEX idx_resource_resource_name ON resource (resource_name); diff --git a/store/migration/postgres/0.20/00__reaction.sql b/store/migration/postgres/0.20/00__reaction.sql new file mode 100644 index 0000000..f9866ee --- /dev/null +++ b/store/migration/postgres/0.20/00__reaction.sql @@ -0,0 +1,9 @@ +-- reaction +CREATE TABLE reaction ( + id SERIAL PRIMARY KEY, + created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()), + creator_id INTEGER NOT NULL, + content_id TEXT NOT NULL, + reaction_type TEXT NOT NULL, + UNIQUE(creator_id, content_id, reaction_type) +); diff --git a/store/migration/postgres/0.21/00__user_description.sql b/store/migration/postgres/0.21/00__user_description.sql new file mode 100644 index 0000000..b8f1aea --- /dev/null +++ b/store/migration/postgres/0.21/00__user_description.sql @@ -0,0 +1 @@ +ALTER TABLE "user" ADD COLUMN description TEXT NOT NULL DEFAULT ''; diff --git a/store/migration/postgres/0.21/01__rename_uid.sql b/store/migration/postgres/0.21/01__rename_uid.sql new file mode 100644 index 0000000..12a0402 --- /dev/null +++ b/store/migration/postgres/0.21/01__rename_uid.sql @@ -0,0 +1,3 @@ +ALTER TABLE memo RENAME COLUMN resource_name TO uid; + +ALTER TABLE resource RENAME COLUMN resource_name TO uid; diff --git a/store/migration/postgres/0.22/00__resource_storage_type.sql b/store/migration/postgres/0.22/00__resource_storage_type.sql new file mode 100644 index 0000000..00cd9b2 --- /dev/null +++ b/store/migration/postgres/0.22/00__resource_storage_type.sql @@ -0,0 +1,11 @@ +ALTER TABLE resource ADD COLUMN storage_type TEXT NOT NULL DEFAULT ''; +ALTER TABLE resource ADD COLUMN reference TEXT NOT NULL DEFAULT ''; +ALTER TABLE resource ADD COLUMN payload TEXT NOT NULL DEFAULT '{}'; + +UPDATE resource SET storage_type = 'LOCAL', reference = internal_path WHERE internal_path IS NOT NULL AND internal_path != ''; + +UPDATE resource SET storage_type = 'EXTERNAL', reference = external_link WHERE external_link IS NOT NULL AND external_link != ''; + +ALTER TABLE resource DROP COLUMN internal_path; + +ALTER TABLE resource DROP COLUMN external_link; diff --git a/store/migration/postgres/0.22/01__memo_tags.sql b/store/migration/postgres/0.22/01__memo_tags.sql new file mode 100644 index 0000000..53012f9 --- /dev/null +++ b/store/migration/postgres/0.22/01__memo_tags.sql @@ -0,0 +1 @@ +ALTER TABLE memo ADD COLUMN tags JSONB NOT NULL DEFAULT '[]'; diff --git a/store/migration/postgres/0.22/02__memo_payload.sql b/store/migration/postgres/0.22/02__memo_payload.sql new file mode 100644 index 0000000..68dc159 --- /dev/null +++ b/store/migration/postgres/0.22/02__memo_payload.sql @@ -0,0 +1 @@ +ALTER TABLE memo ADD COLUMN payload JSONB NOT NULL DEFAULT '{}'; diff --git a/store/migration/postgres/0.22/03__drop_tag.sql b/store/migration/postgres/0.22/03__drop_tag.sql new file mode 100644 index 0000000..8e3d418 --- /dev/null +++ b/store/migration/postgres/0.22/03__drop_tag.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS tag; diff --git a/store/migration/postgres/0.23/00__reactions.sql b/store/migration/postgres/0.23/00__reactions.sql new file mode 100644 index 0000000..a01e95a --- /dev/null +++ b/store/migration/postgres/0.23/00__reactions.sql @@ -0,0 +1,12 @@ +UPDATE "reaction" SET "reaction_type" = '👍' WHERE "reaction_type" = 'THUMBS_UP'; +UPDATE "reaction" SET "reaction_type" = '👎' WHERE "reaction_type" = 'THUMBS_DOWN'; +UPDATE "reaction" SET "reaction_type" = '💛' WHERE "reaction_type" = 'HEART'; +UPDATE "reaction" SET "reaction_type" = '🔥' WHERE "reaction_type" = 'FIRE'; +UPDATE "reaction" SET "reaction_type" = '👏' WHERE "reaction_type" = 'CLAPPING_HANDS'; +UPDATE "reaction" SET "reaction_type" = '😂' WHERE "reaction_type" = 'LAUGH'; +UPDATE "reaction" SET "reaction_type" = '👌' WHERE "reaction_type" = 'OK_HAND'; +UPDATE "reaction" SET "reaction_type" = '🚀' WHERE "reaction_type" = 'ROCKET'; +UPDATE "reaction" SET "reaction_type" = '👀' WHERE "reaction_type" = 'EYES'; +UPDATE "reaction" SET "reaction_type" = '🤔' WHERE "reaction_type" = 'THINKING_FACE'; +UPDATE "reaction" SET "reaction_type" = '🤡' WHERE "reaction_type" = 'CLOWN_FACE'; +UPDATE "reaction" SET "reaction_type" = '❓' WHERE "reaction_type" = 'QUESTION_MARK'; diff --git a/store/migration/postgres/0.24/00__memo.sql b/store/migration/postgres/0.24/00__memo.sql new file mode 100644 index 0000000..0ea2d22 --- /dev/null +++ b/store/migration/postgres/0.24/00__memo.sql @@ -0,0 +1,2 @@ +-- Drop deprecated tags column. +ALTER TABLE memo DROP COLUMN IF EXISTS tags; \ No newline at end of file diff --git a/store/migration/postgres/0.24/01__memo_pinned.sql b/store/migration/postgres/0.24/01__memo_pinned.sql new file mode 100644 index 0000000..66adf63 --- /dev/null +++ b/store/migration/postgres/0.24/01__memo_pinned.sql @@ -0,0 +1,8 @@ +-- Add pinned column. +ALTER TABLE memo ADD COLUMN pinned BOOLEAN NOT NULL DEFAULT FALSE; + +-- Update pinned column from memo_organizer. +UPDATE memo +SET pinned = TRUE +FROM memo_organizer +WHERE memo.id = memo_organizer.memo_id AND memo_organizer.pinned = 1; \ No newline at end of file diff --git a/store/migration/postgres/0.25/00__remove_webhook.sql b/store/migration/postgres/0.25/00__remove_webhook.sql new file mode 100644 index 0000000..f60efc0 --- /dev/null +++ b/store/migration/postgres/0.25/00__remove_webhook.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS webhook; diff --git a/store/migration/postgres/LATEST.sql b/store/migration/postgres/LATEST.sql new file mode 100644 index 0000000..66df284 --- /dev/null +++ b/store/migration/postgres/LATEST.sql @@ -0,0 +1,121 @@ +-- migration_history +CREATE TABLE migration_history ( + version TEXT NOT NULL PRIMARY KEY, + created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()) +); + +-- system_setting +CREATE TABLE system_setting ( + name TEXT NOT NULL PRIMARY KEY, + value TEXT NOT NULL, + description TEXT NOT NULL +); + +-- user +CREATE TABLE "user" ( + id SERIAL PRIMARY KEY, + created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()), + updated_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()), + row_status TEXT NOT NULL DEFAULT 'NORMAL', + username TEXT NOT NULL UNIQUE, + role TEXT NOT NULL DEFAULT 'USER', + email TEXT NOT NULL DEFAULT '', + nickname TEXT NOT NULL DEFAULT '', + password_hash TEXT NOT NULL, + avatar_url TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '' +); + +-- user_setting +CREATE TABLE user_setting ( + user_id INTEGER NOT NULL, + key TEXT NOT NULL, + value TEXT NOT NULL, + UNIQUE(user_id, key) +); + +-- memo +CREATE TABLE memo ( + id SERIAL PRIMARY KEY, + uid TEXT NOT NULL UNIQUE, + creator_id INTEGER NOT NULL, + created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()), + updated_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()), + row_status TEXT NOT NULL DEFAULT 'NORMAL', + content TEXT NOT NULL, + visibility TEXT NOT NULL DEFAULT 'PRIVATE', + pinned BOOLEAN NOT NULL DEFAULT FALSE, + payload JSONB NOT NULL DEFAULT '{}' +); + +-- memo_organizer +CREATE TABLE memo_organizer ( + memo_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + pinned INTEGER NOT NULL DEFAULT 0, + UNIQUE(memo_id, user_id) +); + +-- memo_relation +CREATE TABLE memo_relation ( + memo_id INTEGER NOT NULL, + related_memo_id INTEGER NOT NULL, + type TEXT NOT NULL, + UNIQUE(memo_id, related_memo_id, type) +); + +-- resource +CREATE TABLE resource ( + id SERIAL PRIMARY KEY, + uid TEXT NOT NULL UNIQUE, + creator_id INTEGER NOT NULL, + created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()), + updated_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()), + filename TEXT NOT NULL, + blob BYTEA, + type TEXT NOT NULL DEFAULT '', + size INTEGER NOT NULL DEFAULT 0, + memo_id INTEGER DEFAULT NULL, + storage_type TEXT NOT NULL DEFAULT '', + reference TEXT NOT NULL DEFAULT '', + payload TEXT NOT NULL DEFAULT '{}' +); + +-- activity +CREATE TABLE activity ( + id SERIAL PRIMARY KEY, + creator_id INTEGER NOT NULL, + created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()), + type TEXT NOT NULL DEFAULT '', + level TEXT NOT NULL DEFAULT 'INFO', + payload JSONB NOT NULL DEFAULT '{}' +); + +-- idp +CREATE TABLE idp ( + id SERIAL PRIMARY KEY, + name TEXT NOT NULL, + type TEXT NOT NULL, + identifier_filter TEXT NOT NULL DEFAULT '', + config JSONB NOT NULL DEFAULT '{}' +); + +-- inbox +CREATE TABLE inbox ( + id SERIAL PRIMARY KEY, + created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()), + sender_id INTEGER NOT NULL, + receiver_id INTEGER NOT NULL, + status TEXT NOT NULL, + message TEXT NOT NULL +); + +-- reaction +CREATE TABLE reaction ( + id SERIAL PRIMARY KEY, + created_ts BIGINT NOT NULL DEFAULT EXTRACT(EPOCH FROM NOW()), + creator_id INTEGER NOT NULL, + content_id TEXT NOT NULL, + reaction_type TEXT NOT NULL, + UNIQUE(creator_id, content_id, reaction_type) +); diff --git a/store/migration/sqlite/0.10/00__activity.sql b/store/migration/sqlite/0.10/00__activity.sql new file mode 100644 index 0000000..459468d --- /dev/null +++ b/store/migration/sqlite/0.10/00__activity.sql @@ -0,0 +1,9 @@ +-- activity +CREATE TABLE activity ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + creator_id INTEGER NOT NULL, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + type TEXT NOT NULL DEFAULT '', + level TEXT NOT NULL CHECK (level IN ('INFO', 'WARN', 'ERROR')) DEFAULT 'INFO', + payload TEXT NOT NULL DEFAULT '{}' +); \ No newline at end of file diff --git a/store/migration/sqlite/0.11/00__user_avatar.sql b/store/migration/sqlite/0.11/00__user_avatar.sql new file mode 100644 index 0000000..11c63e9 --- /dev/null +++ b/store/migration/sqlite/0.11/00__user_avatar.sql @@ -0,0 +1,4 @@ +ALTER TABLE + user +ADD + COLUMN avatar_url TEXT NOT NULL DEFAULT ''; \ No newline at end of file diff --git a/store/migration/sqlite/0.11/01__idp.sql b/store/migration/sqlite/0.11/01__idp.sql new file mode 100644 index 0000000..be5cf49 --- /dev/null +++ b/store/migration/sqlite/0.11/01__idp.sql @@ -0,0 +1,8 @@ +-- idp +CREATE TABLE idp ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + type TEXT NOT NULL, + identifier_filter TEXT NOT NULL DEFAULT '', + config TEXT NOT NULL DEFAULT '{}' +); \ No newline at end of file diff --git a/store/migration/sqlite/0.11/02__storage.sql b/store/migration/sqlite/0.11/02__storage.sql new file mode 100644 index 0000000..8dbf563 --- /dev/null +++ b/store/migration/sqlite/0.11/02__storage.sql @@ -0,0 +1,7 @@ +-- storage +CREATE TABLE storage ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + type TEXT NOT NULL, + config TEXT NOT NULL DEFAULT '{}' +); \ No newline at end of file diff --git a/store/migration/sqlite/0.12/00__user_setting.sql b/store/migration/sqlite/0.12/00__user_setting.sql new file mode 100644 index 0000000..b9fbbe6 --- /dev/null +++ b/store/migration/sqlite/0.12/00__user_setting.sql @@ -0,0 +1,6 @@ +UPDATE + user_setting +SET + key = 'memo-visibility' +WHERE + key = 'memoVisibility'; \ No newline at end of file diff --git a/store/migration/sqlite/0.12/01__system_setting.sql b/store/migration/sqlite/0.12/01__system_setting.sql new file mode 100644 index 0000000..12fabb7 --- /dev/null +++ b/store/migration/sqlite/0.12/01__system_setting.sql @@ -0,0 +1,69 @@ +UPDATE + system_setting +SET + name = 'server-id' +WHERE + name = 'serverId'; + +UPDATE + system_setting +SET + name = 'secret-session' +WHERE + name = 'secretSessionName'; + +UPDATE + system_setting +SET + name = 'allow-signup' +WHERE + name = 'allowSignUp'; + +UPDATE + system_setting +SET + name = 'disable-public-memos' +WHERE + name = 'disablePublicMemos'; + +UPDATE + system_setting +SET + name = 'additional-style' +WHERE + name = 'additionalStyle'; + +UPDATE + system_setting +SET + name = 'additional-script' +WHERE + name = 'additionalScript'; + +UPDATE + system_setting +SET + name = 'customized-profile' +WHERE + name = 'customizedProfile'; + +UPDATE + system_setting +SET + name = 'storage-service-id' +WHERE + name = 'storageServiceId'; + +UPDATE + system_setting +SET + name = 'local-storage-path' +WHERE + name = 'localStoragePath'; + +UPDATE + system_setting +SET + name = 'openai-config' +WHERE + name = 'openAIConfig'; \ No newline at end of file diff --git a/store/migration/sqlite/0.12/03__resource_internal_path.sql b/store/migration/sqlite/0.12/03__resource_internal_path.sql new file mode 100644 index 0000000..97f37a3 --- /dev/null +++ b/store/migration/sqlite/0.12/03__resource_internal_path.sql @@ -0,0 +1,4 @@ +ALTER TABLE + resource +ADD + COLUMN internal_path TEXT NOT NULL DEFAULT ''; \ No newline at end of file diff --git a/store/migration/sqlite/0.12/04__resource_public_id.sql b/store/migration/sqlite/0.12/04__resource_public_id.sql new file mode 100644 index 0000000..ef08d02 --- /dev/null +++ b/store/migration/sqlite/0.12/04__resource_public_id.sql @@ -0,0 +1,18 @@ +ALTER TABLE + resource +ADD + COLUMN public_id TEXT NOT NULL DEFAULT ''; + +CREATE UNIQUE INDEX resource_id_public_id_unique_index ON resource (id, public_id); + +UPDATE + resource +SET + public_id = printf ( + '%s-%s-%s-%s-%s', + lower(hex(randomblob(4))), + lower(hex(randomblob(2))), + lower(hex(randomblob(2))), + lower(hex(randomblob(2))), + lower(hex(randomblob(6))) + ); \ No newline at end of file diff --git a/store/migration/sqlite/0.13/00__memo_relation.sql b/store/migration/sqlite/0.13/00__memo_relation.sql new file mode 100644 index 0000000..821a598 --- /dev/null +++ b/store/migration/sqlite/0.13/00__memo_relation.sql @@ -0,0 +1,7 @@ +-- memo_relation +CREATE TABLE memo_relation ( + memo_id INTEGER NOT NULL, + related_memo_id INTEGER NOT NULL, + type TEXT NOT NULL, + UNIQUE(memo_id, related_memo_id, type) +); \ No newline at end of file diff --git a/store/migration/sqlite/0.13/01__remove_memo_organizer_id.sql b/store/migration/sqlite/0.13/01__remove_memo_organizer_id.sql new file mode 100644 index 0000000..6c4156c --- /dev/null +++ b/store/migration/sqlite/0.13/01__remove_memo_organizer_id.sql @@ -0,0 +1,22 @@ +DROP TABLE IF EXISTS memo_organizer_temp; + +CREATE TABLE memo_organizer_temp ( + memo_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0, + UNIQUE(memo_id, user_id) +); + +INSERT INTO + memo_organizer_temp (memo_id, user_id, pinned) +SELECT + memo_id, + user_id, + pinned +FROM + memo_organizer; + +DROP TABLE memo_organizer; + +ALTER TABLE + memo_organizer_temp RENAME TO memo_organizer; \ No newline at end of file diff --git a/store/migration/sqlite/0.14/00__drop_resource_public_id.sql b/store/migration/sqlite/0.14/00__drop_resource_public_id.sql new file mode 100644 index 0000000..c6e636d --- /dev/null +++ b/store/migration/sqlite/0.14/00__drop_resource_public_id.sql @@ -0,0 +1,25 @@ +DROP TABLE IF EXISTS resource_temp; + +CREATE TABLE resource_temp ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + creator_id INTEGER NOT NULL, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + filename TEXT NOT NULL DEFAULT '', + blob BLOB DEFAULT NULL, + external_link TEXT NOT NULL DEFAULT '', + type TEXT NOT NULL DEFAULT '', + size INTEGER NOT NULL DEFAULT 0, + internal_path TEXT NOT NULL DEFAULT '' +); + +INSERT INTO + resource_temp (id, creator_id, created_ts, updated_ts, filename, blob, external_link, type, size, internal_path) +SELECT + id, creator_id, created_ts, updated_ts, filename, blob, external_link, type, size, internal_path +FROM + resource; + +DROP TABLE resource; + +ALTER TABLE resource_temp RENAME TO resource; diff --git a/store/migration/sqlite/0.14/01__create_indexes.sql b/store/migration/sqlite/0.14/01__create_indexes.sql new file mode 100644 index 0000000..721c05d --- /dev/null +++ b/store/migration/sqlite/0.14/01__create_indexes.sql @@ -0,0 +1,5 @@ +CREATE INDEX IF NOT EXISTS idx_user_username ON user (username); +CREATE INDEX IF NOT EXISTS idx_memo_creator_id ON memo (creator_id); +CREATE INDEX IF NOT EXISTS idx_memo_content ON memo (content); +CREATE INDEX IF NOT EXISTS idx_memo_visibility ON memo (visibility); +CREATE INDEX IF NOT EXISTS idx_resource_creator_id ON resource (creator_id); diff --git a/store/migration/sqlite/0.15/00__drop_user_open_id.sql b/store/migration/sqlite/0.15/00__drop_user_open_id.sql new file mode 100644 index 0000000..711d32a --- /dev/null +++ b/store/migration/sqlite/0.15/00__drop_user_open_id.sql @@ -0,0 +1,25 @@ +DROP TABLE IF EXISTS user_temp; + +CREATE TABLE user_temp ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL', + username TEXT NOT NULL UNIQUE, + role TEXT NOT NULL CHECK (role IN ('HOST', 'ADMIN', 'USER')) DEFAULT 'USER', + email TEXT NOT NULL DEFAULT '', + nickname TEXT NOT NULL DEFAULT '', + password_hash TEXT NOT NULL, + avatar_url TEXT NOT NULL DEFAULT '' +); + +INSERT INTO + user_temp (id, created_ts, updated_ts, row_status, username, role, email, nickname, password_hash, avatar_url) +SELECT + id, created_ts, updated_ts, row_status, username, role, email, nickname, password_hash, avatar_url +FROM + user; + +DROP TABLE user; + +ALTER TABLE user_temp RENAME TO user; diff --git a/store/migration/sqlite/0.16/00__add_memo_id_to_resource.sql b/store/migration/sqlite/0.16/00__add_memo_id_to_resource.sql new file mode 100644 index 0000000..5af8ad6 --- /dev/null +++ b/store/migration/sqlite/0.16/00__add_memo_id_to_resource.sql @@ -0,0 +1,13 @@ +ALTER TABLE resource ADD COLUMN memo_id INTEGER; + +UPDATE resource +SET memo_id = ( + SELECT memo_id + FROM memo_resource + WHERE resource.id = memo_resource.resource_id + LIMIT 1 +); + +CREATE INDEX idx_resource_memo_id ON resource (memo_id); + +DROP TABLE IF EXISTS memo_resource; diff --git a/store/migration/sqlite/0.16/01__drop_shortcut_table.sql b/store/migration/sqlite/0.16/01__drop_shortcut_table.sql new file mode 100644 index 0000000..d830a5b --- /dev/null +++ b/store/migration/sqlite/0.16/01__drop_shortcut_table.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS shortcut; diff --git a/store/migration/sqlite/0.17/00__inbox.sql b/store/migration/sqlite/0.17/00__inbox.sql new file mode 100644 index 0000000..ed5f309 --- /dev/null +++ b/store/migration/sqlite/0.17/00__inbox.sql @@ -0,0 +1,9 @@ +-- inbox +CREATE TABLE inbox ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + sender_id INTEGER NOT NULL, + receiver_id INTEGER NOT NULL, + status TEXT NOT NULL, + message TEXT NOT NULL DEFAULT '{}' +); diff --git a/store/migration/sqlite/0.17/01__delete_activities.sql b/store/migration/sqlite/0.17/01__delete_activities.sql new file mode 100644 index 0000000..24593d4 --- /dev/null +++ b/store/migration/sqlite/0.17/01__delete_activities.sql @@ -0,0 +1 @@ +DELETE FROM activity; diff --git a/store/migration/sqlite/0.18/00__webhook.sql b/store/migration/sqlite/0.18/00__webhook.sql new file mode 100644 index 0000000..426ce9c --- /dev/null +++ b/store/migration/sqlite/0.18/00__webhook.sql @@ -0,0 +1,12 @@ +-- webhook +CREATE TABLE webhook ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL', + creator_id INTEGER NOT NULL, + name TEXT NOT NULL, + url TEXT NOT NULL +); + +CREATE INDEX idx_webhook_creator_id ON webhook (creator_id); diff --git a/store/migration/sqlite/0.18/01__user_setting.sql b/store/migration/sqlite/0.18/01__user_setting.sql new file mode 100644 index 0000000..fe79a8f --- /dev/null +++ b/store/migration/sqlite/0.18/01__user_setting.sql @@ -0,0 +1,4 @@ +UPDATE user_setting SET key = 'USER_SETTING_LOCALE', value = REPLACE(value, '"', '') WHERE key = 'locale'; +UPDATE user_setting SET key = 'USER_SETTING_APPEARANCE', value = REPLACE(value, '"', '') WHERE key = 'appearance'; +UPDATE user_setting SET key = 'USER_SETTING_MEMO_VISIBILITY', value = REPLACE(value, '"', '') WHERE key = 'memo-visibility'; +UPDATE user_setting SET key = 'USER_SETTING_TELEGRAM_USER_ID', value = REPLACE(value, '"', '') WHERE key = 'telegram-user-id'; diff --git a/store/migration/sqlite/0.19/00__add_resource_name.sql b/store/migration/sqlite/0.19/00__add_resource_name.sql new file mode 100644 index 0000000..33add15 --- /dev/null +++ b/store/migration/sqlite/0.19/00__add_resource_name.sql @@ -0,0 +1,11 @@ +ALTER TABLE memo ADD COLUMN resource_name TEXT NOT NULL DEFAULT ""; + +UPDATE memo SET resource_name = lower(hex(randomblob(8))); + +CREATE UNIQUE INDEX idx_memo_resource_name ON memo (resource_name); + +ALTER TABLE resource ADD COLUMN resource_name TEXT NOT NULL DEFAULT ""; + +UPDATE resource SET resource_name = lower(hex(randomblob(8))); + +CREATE UNIQUE INDEX idx_resource_resource_name ON resource (resource_name); diff --git a/store/migration/sqlite/0.2/00__user_role.sql b/store/migration/sqlite/0.2/00__user_role.sql new file mode 100644 index 0000000..e6a312e --- /dev/null +++ b/store/migration/sqlite/0.2/00__user_role.sql @@ -0,0 +1,60 @@ +-- change user role field from "OWNER"/"USER" to "HOST"/"USER". +PRAGMA foreign_keys = off; + +DROP TABLE IF EXISTS _user_old; + +ALTER TABLE + user RENAME TO _user_old; + +CREATE TABLE user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL', + email TEXT NOT NULL UNIQUE, + role TEXT NOT NULL CHECK (role IN ('HOST', 'USER')) DEFAULT 'USER', + name TEXT NOT NULL, + password_hash TEXT NOT NULL, + open_id TEXT NOT NULL UNIQUE +); + +INSERT INTO + user ( + id, + created_ts, + updated_ts, + row_status, + email, + name, + password_hash, + open_id + ) +SELECT + id, + created_ts, + updated_ts, + row_status, + email, + name, + password_hash, + open_id +FROM + _user_old; + +UPDATE + user +SET + role = 'HOST' +WHERE + id IN ( + SELECT + id + FROM + _user_old + WHERE + role = 'OWNER' + ); + +DROP TABLE IF EXISTS _user_old; + +PRAGMA foreign_keys = on; \ No newline at end of file diff --git a/store/migration/sqlite/0.2/01__memo_visibility.sql b/store/migration/sqlite/0.2/01__memo_visibility.sql new file mode 100644 index 0000000..84a00da --- /dev/null +++ b/store/migration/sqlite/0.2/01__memo_visibility.sql @@ -0,0 +1,4 @@ +ALTER TABLE + memo +ADD + COLUMN visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PRIVATE')) DEFAULT 'PRIVATE'; \ No newline at end of file diff --git a/store/migration/sqlite/0.20/00__reaction.sql b/store/migration/sqlite/0.20/00__reaction.sql new file mode 100644 index 0000000..b4e8bd6 --- /dev/null +++ b/store/migration/sqlite/0.20/00__reaction.sql @@ -0,0 +1,9 @@ +-- reaction +CREATE TABLE reaction ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + creator_id INTEGER NOT NULL, + content_id TEXT NOT NULL, + reaction_type TEXT NOT NULL, + UNIQUE(creator_id, content_id, reaction_type) +); diff --git a/store/migration/sqlite/0.21/00__user_description.sql b/store/migration/sqlite/0.21/00__user_description.sql new file mode 100644 index 0000000..53408fe --- /dev/null +++ b/store/migration/sqlite/0.21/00__user_description.sql @@ -0,0 +1 @@ +ALTER TABLE user ADD COLUMN description TEXT NOT NULL DEFAULT ""; diff --git a/store/migration/sqlite/0.21/01__rename_uid.sql b/store/migration/sqlite/0.21/01__rename_uid.sql new file mode 100644 index 0000000..12a0402 --- /dev/null +++ b/store/migration/sqlite/0.21/01__rename_uid.sql @@ -0,0 +1,3 @@ +ALTER TABLE memo RENAME COLUMN resource_name TO uid; + +ALTER TABLE resource RENAME COLUMN resource_name TO uid; diff --git a/store/migration/sqlite/0.22/00__resource_storage_type.sql b/store/migration/sqlite/0.22/00__resource_storage_type.sql new file mode 100644 index 0000000..daa46de --- /dev/null +++ b/store/migration/sqlite/0.22/00__resource_storage_type.sql @@ -0,0 +1,17 @@ +ALTER TABLE resource ADD COLUMN storage_type TEXT NOT NULL DEFAULT ''; + +ALTER TABLE resource ADD COLUMN reference TEXT NOT NULL DEFAULT ''; + +ALTER TABLE resource ADD COLUMN payload TEXT NOT NULL DEFAULT '{}'; + +UPDATE resource +SET storage_type = 'LOCAL', reference = internal_path +WHERE internal_path IS NOT NULL AND internal_path != ''; + +UPDATE resource +SET storage_type = 'EXTERNAL', reference = external_link +WHERE external_link IS NOT NULL AND external_link != ''; + +ALTER TABLE resource DROP COLUMN internal_path; + +ALTER TABLE resource DROP COLUMN external_link; diff --git a/store/migration/sqlite/0.22/01__memo_tags.sql b/store/migration/sqlite/0.22/01__memo_tags.sql new file mode 100644 index 0000000..5816269 --- /dev/null +++ b/store/migration/sqlite/0.22/01__memo_tags.sql @@ -0,0 +1,3 @@ +ALTER TABLE memo ADD COLUMN tags TEXT NOT NULL DEFAULT '[]'; + +CREATE INDEX idx_memo_tags ON memo (tags); diff --git a/store/migration/sqlite/0.22/02__memo_payload.sql b/store/migration/sqlite/0.22/02__memo_payload.sql new file mode 100644 index 0000000..267a749 --- /dev/null +++ b/store/migration/sqlite/0.22/02__memo_payload.sql @@ -0,0 +1 @@ +ALTER TABLE memo ADD COLUMN payload TEXT NOT NULL DEFAULT '{}'; diff --git a/store/migration/sqlite/0.22/03__drop_tag.sql b/store/migration/sqlite/0.22/03__drop_tag.sql new file mode 100644 index 0000000..6411efb --- /dev/null +++ b/store/migration/sqlite/0.22/03__drop_tag.sql @@ -0,0 +1 @@ +DROP TABLE tag; \ No newline at end of file diff --git a/store/migration/sqlite/0.23/00__reactions.sql b/store/migration/sqlite/0.23/00__reactions.sql new file mode 100644 index 0000000..13979d1 --- /dev/null +++ b/store/migration/sqlite/0.23/00__reactions.sql @@ -0,0 +1,12 @@ +UPDATE `reaction` SET `reaction_type` = '👍' WHERE `reaction_type` = 'THUMBS_UP'; +UPDATE `reaction` SET `reaction_type` = '👎' WHERE `reaction_type` = 'THUMBS_DOWN'; +UPDATE `reaction` SET `reaction_type` = '💛' WHERE `reaction_type` = 'HEART'; +UPDATE `reaction` SET `reaction_type` = '🔥' WHERE `reaction_type` = 'FIRE'; +UPDATE `reaction` SET `reaction_type` = '👏' WHERE `reaction_type` = 'CLAPPING_HANDS'; +UPDATE `reaction` SET `reaction_type` = '😂' WHERE `reaction_type` = 'LAUGH'; +UPDATE `reaction` SET `reaction_type` = '👌' WHERE `reaction_type` = 'OK_HAND'; +UPDATE `reaction` SET `reaction_type` = '🚀' WHERE `reaction_type` = 'ROCKET'; +UPDATE `reaction` SET `reaction_type` = '👀' WHERE `reaction_type` = 'EYES'; +UPDATE `reaction` SET `reaction_type` = '🤔' WHERE `reaction_type` = 'THINKING_FACE'; +UPDATE `reaction` SET `reaction_type` = '🤡' WHERE `reaction_type` = 'CLOWN_FACE'; +UPDATE `reaction` SET `reaction_type` = '❓' WHERE `reaction_type` = 'QUESTION_MARK'; diff --git a/store/migration/sqlite/0.24/00__memo.sql b/store/migration/sqlite/0.24/00__memo.sql new file mode 100644 index 0000000..2620851 --- /dev/null +++ b/store/migration/sqlite/0.24/00__memo.sql @@ -0,0 +1,7 @@ +-- Remove deprecated indexes. +DROP INDEX IF EXISTS idx_memo_tags; +DROP INDEX IF EXISTS idx_memo_content; +DROP INDEX IF EXISTS idx_memo_visibility; + +-- Drop deprecated tags column. +ALTER TABLE memo DROP COLUMN tags; \ No newline at end of file diff --git a/store/migration/sqlite/0.24/01__memo_pinned.sql b/store/migration/sqlite/0.24/01__memo_pinned.sql new file mode 100644 index 0000000..8fa12e6 --- /dev/null +++ b/store/migration/sqlite/0.24/01__memo_pinned.sql @@ -0,0 +1,11 @@ +-- Add pinned column. +ALTER TABLE memo ADD COLUMN pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0; + +-- Update pinned column from memo_organizer. +UPDATE memo +SET pinned = 1 +WHERE EXISTS ( + SELECT 1 + FROM memo_organizer + WHERE memo.id = memo_organizer.memo_id AND memo_organizer.pinned = 1 +); diff --git a/store/migration/sqlite/0.25/00__remove_webhook.sql b/store/migration/sqlite/0.25/00__remove_webhook.sql new file mode 100644 index 0000000..90d89dc --- /dev/null +++ b/store/migration/sqlite/0.25/00__remove_webhook.sql @@ -0,0 +1,3 @@ +DROP INDEX IF EXISTS idx_webhook_creator_id; + +DROP TABLE IF EXISTS webhook; diff --git a/store/migration/sqlite/0.3/00__memo_visibility_protected.sql b/store/migration/sqlite/0.3/00__memo_visibility_protected.sql new file mode 100644 index 0000000..b960f4a --- /dev/null +++ b/store/migration/sqlite/0.3/00__memo_visibility_protected.sql @@ -0,0 +1,43 @@ +-- change memo visibility field from "PRIVATE"/"PUBLIC" to "PRIVATE"/"PROTECTED"/"PUBLIC". +PRAGMA foreign_keys = off; + +DROP TABLE IF EXISTS _memo_old; + +ALTER TABLE + memo RENAME TO _memo_old; + +CREATE TABLE memo ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + creator_id INTEGER NOT NULL, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL', + content TEXT NOT NULL DEFAULT '', + visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE', + FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE +); + +INSERT INTO + memo ( + id, + creator_id, + created_ts, + updated_ts, + row_status, + content, + visibility + ) +SELECT + id, + creator_id, + created_ts, + updated_ts, + row_status, + content, + visibility +FROM + _memo_old; + +DROP TABLE IF EXISTS _memo_old; + +PRAGMA foreign_keys = on; \ No newline at end of file diff --git a/store/migration/sqlite/0.4/00__user_setting.sql b/store/migration/sqlite/0.4/00__user_setting.sql new file mode 100644 index 0000000..1f9e54c --- /dev/null +++ b/store/migration/sqlite/0.4/00__user_setting.sql @@ -0,0 +1,9 @@ +-- user_setting +CREATE TABLE user_setting ( + user_id INTEGER NOT NULL, + key TEXT NOT NULL, + value TEXT NOT NULL, + FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE +); + +CREATE UNIQUE INDEX user_setting_key_user_id_index ON user_setting(key, user_id); \ No newline at end of file diff --git a/store/migration/sqlite/0.5/00__regenerate_foreign_keys.sql b/store/migration/sqlite/0.5/00__regenerate_foreign_keys.sql new file mode 100644 index 0000000..00ed430 --- /dev/null +++ b/store/migration/sqlite/0.5/00__regenerate_foreign_keys.sql @@ -0,0 +1,217 @@ +PRAGMA foreign_keys = off; + +DROP TABLE IF EXISTS _user_old; + +ALTER TABLE + user RENAME TO _user_old; + +-- user +CREATE TABLE user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL', + email TEXT NOT NULL UNIQUE, + role TEXT NOT NULL CHECK (role IN ('HOST', 'USER')) DEFAULT 'USER', + name TEXT NOT NULL, + password_hash TEXT NOT NULL, + open_id TEXT NOT NULL UNIQUE +); + +INSERT INTO + user +SELECT + * +FROM + _user_old; + +DROP TABLE IF EXISTS _user_old; + +DROP TRIGGER IF EXISTS `trigger_update_user_modification_time`; + +CREATE TRIGGER IF NOT EXISTS `trigger_update_user_modification_time` +AFTER +UPDATE + ON `user` FOR EACH ROW BEGIN +UPDATE + `user` +SET + updated_ts = (strftime('%s', 'now')) +WHERE + rowid = old.rowid; + +END; + +DROP TABLE IF EXISTS _memo_old; + +ALTER TABLE + memo RENAME TO _memo_old; + +-- memo +CREATE TABLE memo ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + creator_id INTEGER NOT NULL, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL', + content TEXT NOT NULL DEFAULT '', + visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE', + FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE +); + +INSERT INTO + memo +SELECT + * +FROM + _memo_old; + +DROP TABLE IF EXISTS _memo_old; + +DROP TRIGGER IF EXISTS `trigger_update_memo_modification_time`; + +CREATE TRIGGER IF NOT EXISTS `trigger_update_memo_modification_time` +AFTER +UPDATE + ON `memo` FOR EACH ROW BEGIN +UPDATE + `memo` +SET + updated_ts = (strftime('%s', 'now')) +WHERE + rowid = old.rowid; + +END; + +DROP TABLE IF EXISTS _memo_organizer_old; + +ALTER TABLE + memo_organizer RENAME TO _memo_organizer_old; + +-- memo_organizer +CREATE TABLE memo_organizer ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + memo_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0, + FOREIGN KEY(memo_id) REFERENCES memo(id) ON DELETE CASCADE, + FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE, + UNIQUE(memo_id, user_id) +); + +INSERT INTO + memo_organizer +SELECT + * +FROM + _memo_organizer_old; + +DROP TABLE IF EXISTS _memo_organizer_old; + +DROP TABLE IF EXISTS _shortcut_old; + +ALTER TABLE + shortcut RENAME TO _shortcut_old; + +-- shortcut +CREATE TABLE shortcut ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + creator_id INTEGER NOT NULL, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL', + title TEXT NOT NULL DEFAULT '', + payload TEXT NOT NULL DEFAULT '{}', + FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE +); + +INSERT INTO + shortcut +SELECT + * +FROM + _shortcut_old; + +DROP TABLE IF EXISTS _shortcut_old; + +DROP TRIGGER IF EXISTS `trigger_update_shortcut_modification_time`; + +CREATE TRIGGER IF NOT EXISTS `trigger_update_shortcut_modification_time` +AFTER +UPDATE + ON `shortcut` FOR EACH ROW BEGIN +UPDATE + `shortcut` +SET + updated_ts = (strftime('%s', 'now')) +WHERE + rowid = old.rowid; + +END; + +DROP TABLE IF EXISTS _resource_old; + +ALTER TABLE + resource RENAME TO _resource_old; + +-- resource +CREATE TABLE resource ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + creator_id INTEGER NOT NULL, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + filename TEXT NOT NULL DEFAULT '', + blob BLOB DEFAULT NULL, + type TEXT NOT NULL DEFAULT '', + size INTEGER NOT NULL DEFAULT 0, + FOREIGN KEY(creator_id) REFERENCES user(id) ON DELETE CASCADE +); + +INSERT INTO + resource +SELECT + * +FROM + _resource_old; + +DROP TABLE IF EXISTS _resource_old; + +DROP TRIGGER IF EXISTS `trigger_update_resource_modification_time`; + +CREATE TRIGGER IF NOT EXISTS `trigger_update_resource_modification_time` +AFTER +UPDATE + ON `resource` FOR EACH ROW BEGIN +UPDATE + `resource` +SET + updated_ts = (strftime('%s', 'now')) +WHERE + rowid = old.rowid; + +END; + +DROP TABLE IF EXISTS _user_setting_old; + +ALTER TABLE + user_setting RENAME TO _user_setting_old; + +-- user_setting +CREATE TABLE user_setting ( + user_id INTEGER NOT NULL, + key TEXT NOT NULL, + value TEXT NOT NULL, + FOREIGN KEY(user_id) REFERENCES user(id) ON DELETE CASCADE, + UNIQUE(user_id, key) +); + +INSERT INTO + user_setting +SELECT + * +FROM + _user_setting_old; + +DROP TABLE IF EXISTS _user_setting_old; + +PRAGMA foreign_keys = on; \ No newline at end of file diff --git a/store/migration/sqlite/0.5/01__memo_resource.sql b/store/migration/sqlite/0.5/01__memo_resource.sql new file mode 100644 index 0000000..0a798ae --- /dev/null +++ b/store/migration/sqlite/0.5/01__memo_resource.sql @@ -0,0 +1,10 @@ +-- memo_resource +CREATE TABLE memo_resource ( + memo_id INTEGER NOT NULL, + resource_id INTEGER NOT NULL, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + FOREIGN KEY(memo_id) REFERENCES memo(id) ON DELETE CASCADE, + FOREIGN KEY(resource_id) REFERENCES resource(id) ON DELETE CASCADE, + UNIQUE(memo_id, resource_id) +); \ No newline at end of file diff --git a/store/migration/sqlite/0.5/02__system_setting.sql b/store/migration/sqlite/0.5/02__system_setting.sql new file mode 100644 index 0000000..9087909 --- /dev/null +++ b/store/migration/sqlite/0.5/02__system_setting.sql @@ -0,0 +1,7 @@ +-- system_setting +CREATE TABLE system_setting ( + name TEXT NOT NULL, + value TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + UNIQUE(name) +); \ No newline at end of file diff --git a/store/migration/sqlite/0.5/03__resource_extermal_link.sql b/store/migration/sqlite/0.5/03__resource_extermal_link.sql new file mode 100644 index 0000000..b346bda --- /dev/null +++ b/store/migration/sqlite/0.5/03__resource_extermal_link.sql @@ -0,0 +1,4 @@ +ALTER TABLE + resource +ADD + COLUMN external_link TEXT NOT NULL DEFAULT ''; \ No newline at end of file diff --git a/store/migration/sqlite/0.6/00__recreate_triggers.sql b/store/migration/sqlite/0.6/00__recreate_triggers.sql new file mode 100644 index 0000000..4f1f1dc --- /dev/null +++ b/store/migration/sqlite/0.6/00__recreate_triggers.sql @@ -0,0 +1,59 @@ +DROP TRIGGER IF EXISTS `trigger_update_user_modification_time`; + +CREATE TRIGGER IF NOT EXISTS `trigger_update_user_modification_time` +AFTER +UPDATE + ON `user` FOR EACH ROW BEGIN +UPDATE + `user` +SET + updated_ts = (strftime('%s', 'now')) +WHERE + rowid = old.rowid; + +END; + +DROP TRIGGER IF EXISTS `trigger_update_memo_modification_time`; + +CREATE TRIGGER IF NOT EXISTS `trigger_update_memo_modification_time` +AFTER +UPDATE + ON `memo` FOR EACH ROW BEGIN +UPDATE + `memo` +SET + updated_ts = (strftime('%s', 'now')) +WHERE + rowid = old.rowid; + +END; + +DROP TRIGGER IF EXISTS `trigger_update_shortcut_modification_time`; + +CREATE TRIGGER IF NOT EXISTS `trigger_update_shortcut_modification_time` +AFTER +UPDATE + ON `shortcut` FOR EACH ROW BEGIN +UPDATE + `shortcut` +SET + updated_ts = (strftime('%s', 'now')) +WHERE + rowid = old.rowid; + +END; + +DROP TRIGGER IF EXISTS `trigger_update_resource_modification_time`; + +CREATE TRIGGER IF NOT EXISTS `trigger_update_resource_modification_time` +AFTER +UPDATE + ON `resource` FOR EACH ROW BEGIN +UPDATE + `resource` +SET + updated_ts = (strftime('%s', 'now')) +WHERE + rowid = old.rowid; + +END; \ No newline at end of file diff --git a/store/migration/sqlite/0.7/00__remove_fk.sql b/store/migration/sqlite/0.7/00__remove_fk.sql new file mode 100644 index 0000000..84d4112 --- /dev/null +++ b/store/migration/sqlite/0.7/00__remove_fk.sql @@ -0,0 +1,191 @@ +PRAGMA foreign_keys = off; + +DROP TABLE IF EXISTS _user_old; + +ALTER TABLE + user RENAME TO _user_old; + +-- user +CREATE TABLE user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL', + email TEXT NOT NULL UNIQUE, + role TEXT NOT NULL CHECK (role IN ('HOST', 'USER')) DEFAULT 'USER', + name TEXT NOT NULL, + password_hash TEXT NOT NULL, + open_id TEXT NOT NULL UNIQUE +); + +INSERT INTO + user +SELECT + * +FROM + _user_old; + +DROP TABLE IF EXISTS _user_old; + +DROP TABLE IF EXISTS _memo_old; + +ALTER TABLE + memo RENAME TO _memo_old; + +-- memo +CREATE TABLE memo ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + creator_id INTEGER NOT NULL, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL', + content TEXT NOT NULL DEFAULT '', + visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE' +); + +INSERT INTO + memo +SELECT + * +FROM + _memo_old; + +DROP TABLE IF EXISTS _memo_old; + +DROP TABLE IF EXISTS _memo_organizer_old; + +ALTER TABLE + memo_organizer RENAME TO _memo_organizer_old; + +-- memo_organizer +CREATE TABLE memo_organizer ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + memo_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0, + UNIQUE(memo_id, user_id) +); + +INSERT INTO + memo_organizer +SELECT + * +FROM + _memo_organizer_old; + +DROP TABLE IF EXISTS _memo_organizer_old; + +DROP TABLE IF EXISTS _shortcut_old; + +ALTER TABLE + shortcut RENAME TO _shortcut_old; + +-- shortcut +CREATE TABLE shortcut ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + creator_id INTEGER NOT NULL, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL', + title TEXT NOT NULL DEFAULT '', + payload TEXT NOT NULL DEFAULT '{}' +); + +INSERT INTO + shortcut +SELECT + * +FROM + _shortcut_old; + +DROP TABLE IF EXISTS _shortcut_old; + +DROP TABLE IF EXISTS _resource_old; + +ALTER TABLE + resource RENAME TO _resource_old; + +-- resource +CREATE TABLE resource ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + creator_id INTEGER NOT NULL, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + filename TEXT NOT NULL DEFAULT '', + blob BLOB DEFAULT NULL, + external_link TEXT NOT NULL DEFAULT '', + type TEXT NOT NULL DEFAULT '', + size INTEGER NOT NULL DEFAULT 0 +); + +INSERT INTO + resource ( + id, + creator_id, + created_ts, + updated_ts, + filename, + blob, + external_link, + type, + size + ) +SELECT + id, + creator_id, + created_ts, + updated_ts, + filename, + blob, + external_link, + type, + size +FROM + _resource_old; + +DROP TABLE IF EXISTS _resource_old; + +DROP TABLE IF EXISTS _user_setting_old; + +ALTER TABLE + user_setting RENAME TO _user_setting_old; + +-- user_setting +CREATE TABLE user_setting ( + user_id INTEGER NOT NULL, + key TEXT NOT NULL, + value TEXT NOT NULL, + UNIQUE(user_id, key) +); + +INSERT INTO + user_setting +SELECT + * +FROM + _user_setting_old; + +DROP TABLE IF EXISTS _user_setting_old; + +DROP TABLE IF EXISTS _memo_resource_old; + +ALTER TABLE + memo_resource RENAME TO _memo_resource_old; + +-- memo_resource +CREATE TABLE memo_resource ( + memo_id INTEGER NOT NULL, + resource_id INTEGER NOT NULL, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + UNIQUE(memo_id, resource_id) +); + +INSERT INTO + memo_resource +SELECT + * +FROM + _memo_resource_old; + +DROP TABLE IF EXISTS _memo_resource_old; \ No newline at end of file diff --git a/store/migration/sqlite/0.7/01__remove_triggers.sql b/store/migration/sqlite/0.7/01__remove_triggers.sql new file mode 100644 index 0000000..7f04c82 --- /dev/null +++ b/store/migration/sqlite/0.7/01__remove_triggers.sql @@ -0,0 +1,7 @@ +DROP TRIGGER IF EXISTS `trigger_update_user_modification_time`; + +DROP TRIGGER IF EXISTS `trigger_update_memo_modification_time`; + +DROP TRIGGER IF EXISTS `trigger_update_shortcut_modification_time`; + +DROP TRIGGER IF EXISTS `trigger_update_resource_modification_time`; \ No newline at end of file diff --git a/store/migration/sqlite/0.8/00__migration_history.sql b/store/migration/sqlite/0.8/00__migration_history.sql new file mode 100644 index 0000000..9612344 --- /dev/null +++ b/store/migration/sqlite/0.8/00__migration_history.sql @@ -0,0 +1,5 @@ +-- migration_history +CREATE TABLE IF NOT EXISTS migration_history ( + version TEXT NOT NULL PRIMARY KEY, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')) +); \ No newline at end of file diff --git a/store/migration/sqlite/0.8/01__user_username.sql b/store/migration/sqlite/0.8/01__user_username.sql new file mode 100644 index 0000000..273ffe6 --- /dev/null +++ b/store/migration/sqlite/0.8/01__user_username.sql @@ -0,0 +1,50 @@ +-- add column username TEXT NOT NULL UNIQUE +-- rename column name to nickname +-- add role `ADMIN` +DROP TABLE IF EXISTS _user_old; + +ALTER TABLE + user RENAME TO _user_old; + +-- user +CREATE TABLE user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL', + username TEXT NOT NULL UNIQUE, + role TEXT NOT NULL CHECK (role IN ('HOST', 'ADMIN', 'USER')) DEFAULT 'USER', + email TEXT NOT NULL DEFAULT '', + nickname TEXT NOT NULL DEFAULT '', + password_hash TEXT NOT NULL, + open_id TEXT NOT NULL UNIQUE +); + +INSERT INTO + user ( + id, + created_ts, + updated_ts, + row_status, + username, + role, + email, + nickname, + password_hash, + open_id + ) +SELECT + id, + created_ts, + updated_ts, + row_status, + email, + role, + email, + name, + password_hash, + open_id +FROM + _user_old; + +DROP TABLE IF EXISTS _user_old; \ No newline at end of file diff --git a/store/migration/sqlite/0.9/00__tag.sql b/store/migration/sqlite/0.9/00__tag.sql new file mode 100644 index 0000000..89802d4 --- /dev/null +++ b/store/migration/sqlite/0.9/00__tag.sql @@ -0,0 +1,6 @@ +-- tag +CREATE TABLE tag ( + name TEXT NOT NULL, + creator_id INTEGER NOT NULL, + UNIQUE(name, creator_id) +); \ No newline at end of file diff --git a/store/migration/sqlite/LATEST.sql b/store/migration/sqlite/LATEST.sql new file mode 100644 index 0000000..37f7712 --- /dev/null +++ b/store/migration/sqlite/LATEST.sql @@ -0,0 +1,130 @@ +-- migration_history +CREATE TABLE migration_history ( + version TEXT NOT NULL PRIMARY KEY, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')) +); + +-- system_setting +CREATE TABLE system_setting ( + name TEXT NOT NULL, + value TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + UNIQUE(name) +); + +-- user +CREATE TABLE user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL', + username TEXT NOT NULL UNIQUE, + role TEXT NOT NULL CHECK (role IN ('HOST', 'ADMIN', 'USER')) DEFAULT 'USER', + email TEXT NOT NULL DEFAULT '', + nickname TEXT NOT NULL DEFAULT '', + password_hash TEXT NOT NULL, + avatar_url TEXT NOT NULL DEFAULT '', + description TEXT NOT NULL DEFAULT '' +); + +CREATE INDEX idx_user_username ON user (username); + +-- user_setting +CREATE TABLE user_setting ( + user_id INTEGER NOT NULL, + key TEXT NOT NULL, + value TEXT NOT NULL, + UNIQUE(user_id, key) +); + +-- memo +CREATE TABLE memo ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + uid TEXT NOT NULL UNIQUE, + creator_id INTEGER NOT NULL, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + row_status TEXT NOT NULL CHECK (row_status IN ('NORMAL', 'ARCHIVED')) DEFAULT 'NORMAL', + content TEXT NOT NULL DEFAULT '', + visibility TEXT NOT NULL CHECK (visibility IN ('PUBLIC', 'PROTECTED', 'PRIVATE')) DEFAULT 'PRIVATE', + pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0, + payload TEXT NOT NULL DEFAULT '{}' +); + +CREATE INDEX idx_memo_creator_id ON memo (creator_id); + +-- memo_organizer +CREATE TABLE memo_organizer ( + memo_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + pinned INTEGER NOT NULL CHECK (pinned IN (0, 1)) DEFAULT 0, + UNIQUE(memo_id, user_id) +); + +-- memo_relation +CREATE TABLE memo_relation ( + memo_id INTEGER NOT NULL, + related_memo_id INTEGER NOT NULL, + type TEXT NOT NULL, + UNIQUE(memo_id, related_memo_id, type) +); + +-- resource +CREATE TABLE resource ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + uid TEXT NOT NULL UNIQUE, + creator_id INTEGER NOT NULL, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + updated_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + filename TEXT NOT NULL DEFAULT '', + blob BLOB DEFAULT NULL, + type TEXT NOT NULL DEFAULT '', + size INTEGER NOT NULL DEFAULT 0, + memo_id INTEGER, + storage_type TEXT NOT NULL DEFAULT '', + reference TEXT NOT NULL DEFAULT '', + payload TEXT NOT NULL DEFAULT '{}' +); + +CREATE INDEX idx_resource_creator_id ON resource (creator_id); + +CREATE INDEX idx_resource_memo_id ON resource (memo_id); + +-- activity +CREATE TABLE activity ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + creator_id INTEGER NOT NULL, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + type TEXT NOT NULL DEFAULT '', + level TEXT NOT NULL CHECK (level IN ('INFO', 'WARN', 'ERROR')) DEFAULT 'INFO', + payload TEXT NOT NULL DEFAULT '{}' +); + +-- idp +CREATE TABLE idp ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + type TEXT NOT NULL, + identifier_filter TEXT NOT NULL DEFAULT '', + config TEXT NOT NULL DEFAULT '{}' +); + +-- inbox +CREATE TABLE inbox ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + sender_id INTEGER NOT NULL, + receiver_id INTEGER NOT NULL, + status TEXT NOT NULL, + message TEXT NOT NULL DEFAULT '{}' +); + +-- reaction +CREATE TABLE reaction ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_ts BIGINT NOT NULL DEFAULT (strftime('%s', 'now')), + creator_id INTEGER NOT NULL, + content_id TEXT NOT NULL, + reaction_type TEXT NOT NULL, + UNIQUE(creator_id, content_id, reaction_type) +); diff --git a/store/migration_history.go b/store/migration_history.go new file mode 100644 index 0000000..693663b --- /dev/null +++ b/store/migration_history.go @@ -0,0 +1,13 @@ +package store + +type MigrationHistory struct { + Version string + CreatedTs int64 +} + +type UpsertMigrationHistory struct { + Version string +} + +type FindMigrationHistory struct { +} diff --git a/store/migrator.go b/store/migrator.go new file mode 100644 index 0000000..2d56e73 --- /dev/null +++ b/store/migrator.go @@ -0,0 +1,327 @@ +package store + +import ( + "context" + "database/sql" + "embed" + "fmt" + "io/fs" + "log/slog" + "path/filepath" + "sort" + "strconv" + "strings" + + "github.com/pkg/errors" + + "github.com/usememos/memos/internal/version" + storepb "github.com/usememos/memos/proto/gen/store" +) + +//go:embed migration +var migrationFS embed.FS + +//go:embed seed +var seedFS embed.FS + +const ( + // MigrateFileNameSplit is the split character between the patch version and the description in the migration file name. + // For example, "1__create_table.sql". + MigrateFileNameSplit = "__" + // LatestSchemaFileName is the name of the latest schema file. + // This file is used to apply the latest schema when no migration history is found. + LatestSchemaFileName = "LATEST.sql" +) + +// Migrate applies the latest schema to the database. +func (s *Store) Migrate(ctx context.Context) error { + if err := s.preMigrate(ctx); err != nil { + return errors.Wrap(err, "failed to pre-migrate") + } + + if s.profile.Mode == "prod" { + workspaceBasicSetting, err := s.GetWorkspaceBasicSetting(ctx) + if err != nil { + return errors.Wrap(err, "failed to get workspace basic setting") + } + currentSchemaVersion, err := s.GetCurrentSchemaVersion() + if err != nil { + return errors.Wrap(err, "failed to get current schema version") + } + if version.IsVersionGreaterThan(workspaceBasicSetting.SchemaVersion, currentSchemaVersion) { + slog.Error("cannot downgrade schema version", + slog.String("databaseVersion", workspaceBasicSetting.SchemaVersion), + slog.String("currentVersion", currentSchemaVersion), + ) + return errors.Errorf("cannot downgrade schema version from %s to %s", workspaceBasicSetting.SchemaVersion, currentSchemaVersion) + } + if version.IsVersionGreaterThan(currentSchemaVersion, workspaceBasicSetting.SchemaVersion) { + filePaths, err := fs.Glob(migrationFS, fmt.Sprintf("%s*/*.sql", s.getMigrationBasePath())) + if err != nil { + return errors.Wrap(err, "failed to read migration files") + } + sort.Strings(filePaths) + + // Start a transaction to apply the latest schema. + tx, err := s.driver.GetDB().Begin() + if err != nil { + return errors.Wrap(err, "failed to start transaction") + } + defer tx.Rollback() + + slog.Info("start migration", slog.String("currentSchemaVersion", workspaceBasicSetting.SchemaVersion), slog.String("targetSchemaVersion", currentSchemaVersion)) + for _, filePath := range filePaths { + fileSchemaVersion, err := s.getSchemaVersionOfMigrateScript(filePath) + if err != nil { + return errors.Wrap(err, "failed to get schema version of migrate script") + } + if version.IsVersionGreaterThan(fileSchemaVersion, workspaceBasicSetting.SchemaVersion) && version.IsVersionGreaterOrEqualThan(currentSchemaVersion, fileSchemaVersion) { + bytes, err := migrationFS.ReadFile(filePath) + if err != nil { + return errors.Wrapf(err, "failed to read minor version migration file: %s", filePath) + } + stmt := string(bytes) + if err := s.execute(ctx, tx, stmt); err != nil { + return errors.Wrapf(err, "migrate error: %s", stmt) + } + } + } + if err := tx.Commit(); err != nil { + return errors.Wrap(err, "failed to commit transaction") + } + slog.Info("end migrate") + if err := s.updateCurrentSchemaVersion(ctx, currentSchemaVersion); err != nil { + return errors.Wrap(err, "failed to update current schema version") + } + } + } else if s.profile.Mode == "demo" { + // In demo mode, we should seed the database. + if err := s.seed(ctx); err != nil { + return errors.Wrap(err, "failed to seed") + } + } + return nil +} + +func (s *Store) preMigrate(ctx context.Context) error { + initialized, err := s.driver.IsInitialized(ctx) + if err != nil { + return errors.Wrap(err, "failed to check if database is initialized") + } + + if !initialized { + filePath := s.getMigrationBasePath() + LatestSchemaFileName + bytes, err := migrationFS.ReadFile(filePath) + if err != nil { + return errors.Errorf("failed to read latest schema file: %s", err) + } + // Start a transaction to apply the latest schema. + tx, err := s.driver.GetDB().Begin() + if err != nil { + return errors.Wrap(err, "failed to start transaction") + } + defer tx.Rollback() + if err := s.execute(ctx, tx, string(bytes)); err != nil { + return errors.Errorf("failed to execute SQL file %s, err %s", filePath, err) + } + if err := tx.Commit(); err != nil { + return errors.Wrap(err, "failed to commit transaction") + } + + // Upsert current schema version to database. + schemaVersion, err := s.GetCurrentSchemaVersion() + if err != nil { + return errors.Wrap(err, "failed to get current schema version") + } + if err := s.updateCurrentSchemaVersion(ctx, schemaVersion); err != nil { + return errors.Wrap(err, "failed to update current schema version") + } + } + + if s.profile.Mode == "prod" { + if err := s.normalizeMigrationHistoryList(ctx); err != nil { + return errors.Wrap(err, "failed to normalize migration history list") + } + if err := s.migrateSchemaVersionToSetting(ctx); err != nil { + return errors.Wrap(err, "failed to migrate schema version to setting") + } + } + return nil +} + +func (s *Store) getMigrationBasePath() string { + return fmt.Sprintf("migration/%s/", s.profile.Driver) +} + +func (s *Store) getSeedBasePath() string { + return fmt.Sprintf("seed/%s/", s.profile.Driver) +} + +func (s *Store) seed(ctx context.Context) error { + // Only seed for SQLite. + if s.profile.Driver != "sqlite" { + slog.Warn("seed is only supported for SQLite") + return nil + } + + filenames, err := fs.Glob(seedFS, fmt.Sprintf("%s*.sql", s.getSeedBasePath())) + if err != nil { + return errors.Wrap(err, "failed to read seed files") + } + + // Sort seed files by name. This is important to ensure that seed files are applied in order. + sort.Strings(filenames) + // Start a transaction to apply the seed files. + tx, err := s.driver.GetDB().Begin() + if err != nil { + return errors.Wrap(err, "failed to start transaction") + } + defer tx.Rollback() + // Loop over all seed files and execute them in order. + for _, filename := range filenames { + bytes, err := seedFS.ReadFile(filename) + if err != nil { + return errors.Wrapf(err, "failed to read seed file, filename=%s", filename) + } + if err := s.execute(ctx, tx, string(bytes)); err != nil { + return errors.Wrapf(err, "seed error: %s", filename) + } + } + return tx.Commit() +} + +func (s *Store) GetCurrentSchemaVersion() (string, error) { + currentVersion := version.GetCurrentVersion(s.profile.Mode) + minorVersion := version.GetMinorVersion(currentVersion) + filePaths, err := fs.Glob(migrationFS, fmt.Sprintf("%s%s/*.sql", s.getMigrationBasePath(), minorVersion)) + if err != nil { + return "", errors.Wrap(err, "failed to read migration files") + } + + sort.Strings(filePaths) + if len(filePaths) == 0 { + return fmt.Sprintf("%s.0", minorVersion), nil + } + return s.getSchemaVersionOfMigrateScript(filePaths[len(filePaths)-1]) +} + +func (s *Store) getSchemaVersionOfMigrateScript(filePath string) (string, error) { + // If the file is the latest schema file, return the current schema version. + if strings.HasSuffix(filePath, LatestSchemaFileName) { + return s.GetCurrentSchemaVersion() + } + + normalizedPath := filepath.ToSlash(filePath) + elements := strings.Split(normalizedPath, "/") + if len(elements) < 2 { + return "", errors.Errorf("invalid file path: %s", filePath) + } + minorVersion := elements[len(elements)-2] + rawPatchVersion := strings.Split(elements[len(elements)-1], MigrateFileNameSplit)[0] + patchVersion, err := strconv.Atoi(rawPatchVersion) + if err != nil { + return "", errors.Wrapf(err, "failed to convert patch version to int: %s", rawPatchVersion) + } + return fmt.Sprintf("%s.%d", minorVersion, patchVersion+1), nil +} + +// execute runs a single SQL statement within a transaction. +func (*Store) execute(ctx context.Context, tx *sql.Tx, stmt string) error { + if _, err := tx.ExecContext(ctx, stmt); err != nil { + return errors.Wrap(err, "failed to execute statement") + } + return nil +} + +func (s *Store) updateCurrentSchemaVersion(ctx context.Context, schemaVersion string) error { + workspaceBasicSetting, err := s.GetWorkspaceBasicSetting(ctx) + if err != nil { + return errors.Wrap(err, "failed to get workspace basic setting") + } + workspaceBasicSetting.SchemaVersion = schemaVersion + if _, err := s.UpsertWorkspaceSetting(ctx, &storepb.WorkspaceSetting{ + Key: storepb.WorkspaceSettingKey_BASIC, + Value: &storepb.WorkspaceSetting_BasicSetting{BasicSetting: workspaceBasicSetting}, + }); err != nil { + return errors.Wrap(err, "failed to upsert workspace setting") + } + return nil +} + +func (s *Store) normalizeMigrationHistoryList(ctx context.Context) error { + migrationHistoryList, err := s.driver.FindMigrationHistoryList(ctx, &FindMigrationHistory{}) + if err != nil { + return errors.Wrap(err, "failed to find migration history") + } + versions := []string{} + for _, migrationHistory := range migrationHistoryList { + versions = append(versions, migrationHistory.Version) + } + if len(versions) == 0 { + return nil + } + sort.Sort(version.SortVersion(versions)) + latestVersion := versions[len(versions)-1] + latestMinorVersion := version.GetMinorVersion(latestVersion) + + // If the latest version is greater than 0.22, return. + // As of 0.22, the migration history is already normalized. + if version.IsVersionGreaterThan(latestMinorVersion, "0.22") { + return nil + } + + schemaVersionMap := map[string]string{} + filePaths, err := fs.Glob(migrationFS, fmt.Sprintf("%s*/*.sql", s.getMigrationBasePath())) + if err != nil { + return errors.Wrap(err, "failed to read migration files") + } + sort.Strings(filePaths) + for _, filePath := range filePaths { + fileSchemaVersion, err := s.getSchemaVersionOfMigrateScript(filePath) + if err != nil { + return errors.Wrap(err, "failed to get schema version of migrate script") + } + schemaVersionMap[version.GetMinorVersion(fileSchemaVersion)] = fileSchemaVersion + } + + latestSchemaVersion := schemaVersionMap[latestMinorVersion] + if latestSchemaVersion == "" { + return errors.Errorf("latest schema version not found") + } + if version.IsVersionGreaterOrEqualThan(latestVersion, latestSchemaVersion) { + return nil + } + if _, err := s.driver.UpsertMigrationHistory(ctx, &UpsertMigrationHistory{ + Version: latestSchemaVersion, + }); err != nil { + return errors.Wrap(err, "failed to upsert latest migration history") + } + return nil +} + +func (s *Store) migrateSchemaVersionToSetting(ctx context.Context) error { + migrationHistoryList, err := s.driver.FindMigrationHistoryList(ctx, &FindMigrationHistory{}) + if err != nil { + return errors.Wrap(err, "failed to find migration history") + } + versions := []string{} + for _, migrationHistory := range migrationHistoryList { + versions = append(versions, migrationHistory.Version) + } + if len(versions) == 0 { + return nil + } + sort.Sort(version.SortVersion(versions)) + latestVersion := versions[len(versions)-1] + + workspaceBasicSetting, err := s.GetWorkspaceBasicSetting(ctx) + if err != nil { + return errors.Wrap(err, "failed to get workspace basic setting") + } + if version.IsVersionGreaterOrEqualThan(workspaceBasicSetting.SchemaVersion, latestVersion) { + if err := s.updateCurrentSchemaVersion(ctx, latestVersion); err != nil { + return errors.Wrap(err, "failed to update current schema version") + } + } + return nil +} diff --git a/store/reaction.go b/store/reaction.go new file mode 100644 index 0000000..be25b5f --- /dev/null +++ b/store/reaction.go @@ -0,0 +1,36 @@ +package store + +import ( + "context" +) + +type Reaction struct { + ID int32 + CreatedTs int64 + CreatorID int32 + // ContentID is the id of the content that the reaction is for. + ContentID string + ReactionType string +} + +type FindReaction struct { + ID *int32 + CreatorID *int32 + ContentID *string +} + +type DeleteReaction struct { + ID int32 +} + +func (s *Store) UpsertReaction(ctx context.Context, upsert *Reaction) (*Reaction, error) { + return s.driver.UpsertReaction(ctx, upsert) +} + +func (s *Store) ListReactions(ctx context.Context, find *FindReaction) ([]*Reaction, error) { + return s.driver.ListReactions(ctx, find) +} + +func (s *Store) DeleteReaction(ctx context.Context, delete *DeleteReaction) error { + return s.driver.DeleteReaction(ctx, delete) +} diff --git a/store/seed/sqlite/00__reset.sql b/store/seed/sqlite/00__reset.sql new file mode 100644 index 0000000..65e32e7 --- /dev/null +++ b/store/seed/sqlite/00__reset.sql @@ -0,0 +1,11 @@ +DELETE FROM system_setting; +DELETE FROM user; +DELETE FROM user_setting; +DELETE FROM memo; +DELETE FROM memo_organizer; +DELETE FROM memo_relation; +DELETE FROM resource; +DELETE FROM activity; +DELETE FROM idp; +DELETE FROM inbox; +DELETE FROM reaction; diff --git a/store/seed/sqlite/01__dump.sql b/store/seed/sqlite/01__dump.sql new file mode 100644 index 0000000..b045bc5 --- /dev/null +++ b/store/seed/sqlite/01__dump.sql @@ -0,0 +1,13 @@ +INSERT INTO user (id,username,role,nickname,password_hash) VALUES(1,'yourselfhosted','HOST','yourselfhosted','$2a$10$.VLFG.mpAqpwdjHJHI2jSOfwfGey4oPXzbLSUhOke5GTL5kIuvbUq'); +INSERT INTO memo (id,uid,creator_id,content,visibility,payload) VALUES(1,'hjnAKZx9q27tDasapLAm3P',1,'Hello world. This is my first memo! #hello','PUBLIC','{"tags":["hello"]}'); +INSERT INTO memo (id,uid,creator_id,content,visibility,payload) VALUES(2,'a2KkqjW4hyQMUeSRRehRQ5',1,'Ok, I''m able to upload **some images**. #features','PUBLIC','{"tags":["features"]}'); +INSERT INTO memo (id,uid,creator_id,content,visibility,payload) VALUES(3,'8x7bm252MAJfGBqW5dHHPE',1,replace('And here are my **tasks**. #todo\n- [x] deploy memos for myself;\n- [ ] share to my friends;\n- [ ] sounds good to me!','\n',char(10)),'PUBLIC','{"tags":["todo"],"property":{"hasTaskList":true,"hasIncompleteTasks":true}}'); +INSERT INTO memo (id,uid,creator_id,content,visibility,payload) VALUES(4,'kBfJKAyFvE52kQ9dmSZMfE',1,'Wow, it can be **referenced** too! REALLY GREAT!!! #features','PUBLIC','{"tags":["features"]}'); +INSERT INTO memo (id,uid,creator_id,content,visibility,payload) VALUES(5,'RF9XnAcWpcBzKttK83zQtf',1,replace('#sponsor **[yourselfhosted/slash](https://github.com/yourselfhosted/slash)**: An open source, self-hosted platform for sharing and managing your most frequently used links. Easily create customizable, human-readable shortcuts to streamline your link management.\n![demo](https://raw.githubusercontent.com/yourselfhosted/slash/main/docs/assets/demo.png)','\n',char(10)),'PUBLIC','{"tags":["sponsor"]}'); +INSERT INTO memo_organizer VALUES(5,1,1); +INSERT INTO memo_relation VALUES(4,1,'REFERENCE'); +INSERT INTO resource VALUES(1,'oQ9JTLpGKisxDwnAosJcaA',1,1722096752,1722096788,'memos-brands.png',X'89504e470d0a1a0a0000000d4948445200000960000004ec080600000004e8b90d0000000970485973000016250000162501495224f0000000017352474200aece1ce90000000467414d410000b18f0bfc610500077b73494441547801ecbdeb962cc9529d1b55dd4faccd46e886fa65d9208140772401abcec9eee15d9616d32eee119195b9d637c76832c2dd6c9af9e58bf5a7c6e6ede3ffd7f624bab5f2f6f6b67d95bebafe3389bd88f5ac7bf3ea6706ffcf23f62216fc5f23f87f1eb117b1e0ff1ac1fff388bd8805ffd708fe9f47ec452cf8bf46f0ff3c622f62c1ff3582ffe7117b110bfeaf11fc3f8fd88b58f07f8de0ff79c45ec482ff6bf423f2ffbe5d24fb775db767fbeee7875616efebf8b12a27f38962677b5af15855b5363b16f5955dc499b574cf23ca9b19afeed7d133b0f9b7bdb9fa4cab33abf8e9f477e51ae0fff319fee1ff483d3b06ff791d3f56e5643e51ec6c4ff00fff47ead931f8cfebf8b12a27f38962677b827ff83f52cf8ec17f5ec78f5539994f143bdb13fcc3ff917a760cfef33a7eaccac97ca2d8d99ee01ffe8fd4b363f09fd7f163554ee613c5cef604fff07fa49e1d83ffbc8e1fab72329f2876b627f887ff23f5ec18fce775fc589593f944b1b33dc13ffc1fa967c7e03fafe3c7aa9ccc278a9ded29f3788f0ed527da18f5ec63fc66aaf7ec92296f35ae6af88b986d7805fbf0eaee87ea4981915d9ca856d463d4eff81d7b6d7d475f6a4fd59a6d5e7607fcdad55d887af7f3be86bf337e3d9122f8ab7df76bb1b1be5e5443f957f7a892ddd38891ecde46ef559c8a877ff8cfce43dd85a8773f0fff5a764fe17f5f03fee13faa65dfab7df76bb1b1f00fff7e1cfef76b577721eaddcfc3bf96dd53f8dfd7807ff88f6ad9f76adffd5a6c2cfcc3bf1f87fffddad55d887af7f3f0af65f714fef735e01ffea35af6bdda77bf161b0bfff0efc7e17fbf767517a2defd3cfc6bd93d85ff7d0df887ffa8967daff6ddafc5c6c23ffcfbf115fe7f8e2e9205cdc2eacd3d0416140fa08ff34ddb3aaa695fd73fabc5db5fafcc57cdab71ebadd615f5a700ca6a451e7e7f7d3df56ebd230f9f9bf96c62bdeafcfc5ef97c5f4fc566e0fafacacbd78b72d4de64b5bca78f556b518a788962559e7d8fee99ead5d7847ff8877ff887ff380ffeeffb847ff8877ff887fffb58b51625f8877f550bfef57ab2faf00ffff6375aabf255f36a1cfeeffb847ff8877ff887fffb58b51625f8877f550bfef57ab2faf00ffff6375aabf255f36a1cfeeffb847ff887ff3eff6fdfbe7dfb889aaf3e0e3e36bafc5933337556fabaa98aaf3e346ac32bb0d4c7b0eb971d5ab4cf59de59f36779567bb8b2ff51cdc8d3eae89d8cea577df878353e72aafc15efb3ee02fcc3ffcc3cfceb78353e72e07fadaf9be0ff9cf9b33ce15fc7abf19103ff6b7ddd04ffe7cc9fe509ff3a5e8d8f1cf85febeb26f83f67fe2c4ff8d7f16a7ce4c0ff5a5f37c1ff39f36779c2bf8e57e32307fed7faba09fecf993fcb13fe75bc1a1f39f0bfd6d74df07fcefc599ef0afe3d5f8c881ffb5be6e82ff73e6cff2847f1dafc6470efcaff575d353f3ff2ffff22f1f3e5905beb2ec05fe11f48c6767cf60f4f78ce7f2a3dd9568bdf0ffba82ff75c1ff6f82ffd715fcaf0bfe7f13fcbfaee07f5df0ff9be0ff7505ffeb82ffdf04ffaf2bf85f17fcff26f87f5dc1ffbae0ff37c1ffeb0afed705ffbf09fe5f57f0bf2ef8ff4df0ff18bd471b3d00e9c8c6f99cd9f74e8d99bc9b8e6cfeca25ccf6e32c45beeadcc6bbff9df1edf452fd45a58dbfbd3f2314554fd5fe9c71ded1fdb9a236fccfd59dcd81ffdf04ff7dc17f5e6326ef26f887ffaee0ffd30bfee17fa617f8877ff8877f25f8ef0bfef31a337937c13ffc7705ff9f5ef00fff33bdc03ffcc33ffc2bc17f5ff09fd798c9bb09fee1bf2bf8fff482ffebf97fbffd1f0fc7901a57b0f9054607aa62abbc313ef2321f7b693a1f19fb9bf53ce63b1f3ddfabbde0597ef6acfa88cec603a52eaeedcfc7467721eb59fd656b068c8f517d74eadaf7a866b6872b70457d0cd93df0752b0f7bb69d73f3311d5e556ff00fff7e4ee57a9f21f8ff14fcc33ffcc3ffc707fcab31f88ffb899ee17f0be73ee0ffaeb6f7fe80ffb04e7677d4fc07fccb39f8ff14fcc33ffcc3ffc707fcab31f88ffb899ee17f0be73ee0ffaeb6f7fe80ffb04e7677d4fc07fccb39f8ff14fcc33ffcc3ffc707fcab31f8bfd7ddff0b429f309ed585b073b61135ee1bb363c34bc5aa9c282fbb1099a7cf8b6a2aaf6dab2f6cf7c2293fdf977fb679be37751136d19b5abf7daefa9f5973d5d34cadaa8f0cd8ce5d9ce9af7bef155f911477118fd9daa37ba5fa807ff8877ff8877ff8877ff8877f2df8df4adf2a1efee11ffee13ff2aa6a557dc03ffc773ce11ffe551cfcc37f24f8df4adf2a1efee11ffee13ff2aa6a557dc03ffc773ce11ffe551cfcc37fa433f9fff50fb0b642d1c6f998e8e2773f3451ede8129c295567b6d7993a9d3e3ab5577b8cfac8d69f81d8f1cefae8e644ffb8747387d49ab27f903a3dad28cbedec6de71c67e6a2782bf83f5ea7d347a736fcc33ffc1f17fcefc7e11ffe47bc15fc1fafd3e9a3531bfee11ffe8f0bfef7e3f00fff23de0afe8fd7e9f4d1a90dfff00fffc705fffb71f887ff116f05ffc7eb74fae8d4867ff887ffe382fffd38fcc3ff88b782ffe375867efa8ffff13ffee23778c6ec480323dffbdcde57bdedef4aee597157c9eecd1597325a5f04d2157767d653c547632b1fd895fa2b3167d7bcda0bfe1f2ff8efc5c3fff55ef0ff78c17f2f1efeaff782ffc70bfe7bf1f07fbd17fc3f5ef0df8b87ffebbde0fff182ff5e3cfc5fef05ff8f17fcf7e2e1ff7a2ff87fbce0bf170fffd77bc1ffe305ffbd78f8bfde0bfe8fabf5bf80e565ffd26d35cefeb599fd3da2a31eabf92aaff2f297f04af95eaedaa733cef0ab75f6ddbee9ca3df98a3d87ff3a0ffe5f53f07f5e4df887ff5713fc9f5713fee1ffd504ffe7d5847ff87f35c1ff7935e11ffe5f4df07f5e4df887ff5713fc9f5713fee1ffd504ffe7d5847ff87f35c1ff7935e1bfe6ffddbef86795e4e7d5986ac0d71873feb7abe167ebdb4befe7a31eadd426aa5f3f3ff2a279e561737c8f6a6d4a2a47d5caf2bd573496cd575a8dc9fca373addefdb8f750774829badbd623ba4f99fc9956ebef7e08a3bbe2dfbbf72d1b1be3f00fffbece4c0cfcc37fd68baf0fffba5696efbde01ffe2b9f6a6c8cc33ffcfb3a3331f00fff592fbe3efceb5a59bef7827ff8af7caab1310efff0efebccc4c03ffc67bdf8faf0af6b65f9de0bfee1bff2a9c6c638fcc3bfaf331303fff09ff5e2ebc3bfae95e57b2ff887ffcaa71a1be3f05ff3ffde293c9ea3cd520bb763fe3dbbbc4a6a13ad4775d16ff3a3befa88f81aeab2aacbeed7ed7bca7a8ce22229306daef2f067e0cf46f51bf5ec6ba9739efde044e7a96a8e78b5f6e8be663dd9b5a873f5b90a78bf07d9c7d2bf77c6547ef7039931e063e11ffee17fdbf502fff00ffff7f3de17fee11ffee15ff500fff7b97e1dd598ca87ff6de701fff0effde01ffeed3cfcc3bf1a877ff8f735e07fdb79c23ffcc33ffc7b5ff8877ff8877fd503fcdfe7fa7554632a1ffeb79dc72bf3ffd35ffee55ffee24dbb8a2e7106a58ff187111d4e0568b4411e52df5fa5eaa36363aef4527b6df7505dbace1aa3dc9bd49eaafc4e4e740e56762d3edeafdf7f84fd9caa6ddf551fea1fa8ea4316f9be05ffc845397e1ffd6f94afe6aa7de8fa5482fffb982bbde01ffee17fef01fff06ffde01ffea379d55f25f8877f9f0bff7b3ff8bfd60bfee11ffef71ef00fffd60ffee13f9a57fd55827ff8f7b9f0bff783ff6bbde01ffee17fef01fff06ffde01ffea379d55fa5cbf9ffe77ffee75e27cea8bb80b3a436328a7b746f676b760d473eb6556c96b3e2d7c93bbbf64c7c274e7d60bee2cef98fcfa37a80ff6b05fff0df11fceb38f83f166763e13f8f81ffc708fe8fc7c33ffc9f5517feaf13fcc37f47f0afe3e0ff589c8d85ff3c06fe1f23f83f1e0ffff07f565df8bf4ef00fff1dc1bf8e83ff63713616fef318f87f8cbe57fe5b7f80350ef8475774b8637fae3afc67be54cf0cfeb3ed5ba71fcf9acd39cae1ea7ec0ff6f82ffbde0bf2ff87f6dc1ff5ef0df17fcbfb6e07f2ff8ef0bfe5f5bf0bf17fcf705ffaf2df8df0bfefb82ffd716fcef05ff7dc1ff6b0bfef782ffbee0ffb505ff7bc17f5ff0ffdaba82fff761e00d7de133642f5254d3ce458b893661a5974ecce829ca19fb93f56be766d6958dafd4cad6d11dafce2a3abfac37b5c7595d359ed5525af9185535677266ef603776b63efce731f00fff9d5cf8df5af5e11ffe550efcc73933b1b3f5e13f8f817ff8efe4c2ffd6aa0ffff0af72e03fce99899dad0fff790cfcc37f2717feb7567df8877f9503ff71ce4cec6c7df8cf63e01ffe3bb9f0bfb5eac33ffcab1cf88f73666267ebc37f1e7325ff3fdb22d1468fb9113b9efd5f7ca97c1ba7f2a2c3f60dfbfa764e3d5b9f28debeabbf5eb3fd2a296fb5d66a5faa7a6add91875f7b24dfa3f757f3d6d3ff7620cdfaf2b5a273b5b191afddf3e8ce46f3aa86da97687d914725d5a71d57733e3f5b5374a7e01ffee11ffee11ffee11ffea3dee07f0bd756ad15feebbee01ffead07fceb1e2b0fbff648f00ffffe17fee13f9bb79ef0bf856b88de33c1ffbd07fceb1e2b0fbff648f00ffffe17fee13f9bb79ef0bf856b88de33c1ffbd07fceb1e2b0fbff648f00ffffe17fe1fcfffcf1d73fb7b1bcf36d32fdcfb57f0443e3656c565fd2b307d4fd598ea39fbe8f83d51f05b8fec43d1f9b8d95c5bd77afb7a19b4d547e72df928ab3e22d0a2b1aa5e0671f4a1f5f7387af6e7ebc1cfeebbed375bb3fa7028a63a52b1dec3afa1ba53f00fff762df00ffff07f3f07ffdb6ead511ff00fffaa2ff8877f3506fff01ff5a1e2b2fee11ffee11ffee11ffe7d7ff01f7bc2ffbd9f9f83ff6db7d6a80ff8877fd517fcc3bf1a837ff88ffa507159fff00ffff00fff4fc9ff3ffdd33f7da8e2d90588e6d566669abd68d1a5ae0e74461ec82105ac7aaf3c3b7319bc4761cd6a1cc98d3e6a1d8f33ee819dbbc9ef4df4a18dfeb19939af6cbc9b3b6a573afbdc56e2e01ffee1bfdf532777d4ae04fff06f63e07f3f7713fcafe7c23ffccfe6c23ffcc37fedd99983fff9fa76ee26f85fcf857ff89fcd857ff887ffdab33307fff3f5eddc4df0bf9e0bfff03f9b0bfff00fffb567670efee7ebdbb99be07f3d17fe1fc8ff3ffff33f7fac82f0a3e88a7d39027214ab2ecfa3fa5ef1e8fee333ebbb32ffe8ba47cef94c1df987f04711fcc7358f78c03ffcbf82e03fae79c403fee1ff1504ff71cd231ef00fffaf20f88f6b1ef1807ff87f05c17f5cf38807fcc3ff2b08fee39a473ce01ffe5f41f01fd73ce201fff0ff0a82ffb8e6110ff887ff57d095fccbff05ac2ba43e0ce3f9a8aff2ccfc576afbfe677b5b91cf9ff13b6b7f8f7a7762afecf568dd23bd7dd5ba9e51f03f2ff8bf56f0ff38c1ffbce0ff5ac1ffe304fff382ff6b05ff8f13fccf0bfeaf15fc3f4ef03f2ff8bf56f0ff38c1ffbce0ff5ac1ffe304fff382ff6b05ff8f13fccf0bfeaf15fc3f4ef0aff57e0bb2ffd964f59f9df3b15523b6996c73d46ff47cf319f5c7b35a43554bad51d58a72fdb38daff646fd46f92a4ec9ef6f673f3b7b95a9eacd8fdb738b7a50fbbb72ff547d3fbf3296c9dfc78ea2b564e3f6d7c7675eeabf2a46f502fff01fd5c83cecb9453da8fd5db97faabe9f5f19cb04ff9ff3f0bf953df858f887ffceb8fdf5f1f00fff51deca5e659ae5cc9e5bd483dadf95fba7eafbf995b14cf0ff390fff5bd9838f857ff8ef8cdb5f1f0ffff01fe5adec55a659ceecb9453da8fd5db97faabe9f5f19cb04ff9ff3f0bf953df858f887ffceb8fdf5f1f00fff51deca5e659ae5cc9e5bd483dadf95fba7eafbf995b14cf0ff390fff5bd9838f857ff8ef8cdb5f1f0fffcfcdfffb08f41be44dc6bc05c6c7a8621e3015a70e343b20b5d9d6d31fb8af9ff5afea6539fe52d90b617bf1fdfb0b64d71341e1eb7d1450fa77b5af7e7dd159762e93daefa8de885367e3a1b27ba4fa1cefd54742ed85da7ba50cf0a886edcb7b65efaa4e047f56678c47b53cdbf0afeb6539f0bfef0dfee15ff5aef66a3cc33ffcdb31f8877f25f8877f5bc37ac13ffcdb7cf8bf7f877ff8877ff8cfd607fff0aff2e11ffe95b7ea0dfee15ff5aef66a3cc33ffcdb31f8877f25f8877f5bc37ac13ffcdbfc57e0ff675fcc5fca6803a3f8e822f818f5ece3ed0233efea9267de767d7e9d555eb6be6ace5fba15a98ba5ce4b5d3075e1a30fa7df23f50f88efc9cfbd898fadca8ff6af7af6bd457b10dd4dff1b7d9cfd9a7c7de5dd6124ebcbfaa87aeafcfcbccf53f70ffee1dfbefb795b03fee11ffee3f55573f0bfc97cf88fd70dfff06f05fff06fe7e07f9f1ff56df3e01ffefd2ffcc33ffc6fbb77f887ffd9b5a958f8877f950ffff1bae11ffeade01ffeed1cfceff3a3be6d1efcc3bfff857ff87f04ff3ffd87fff01f7eb1c5eda23265f1b3b9634c357fa49edf5855af1adb26eb5717b1aa91f9aa8f9dbf40d53e563a9aa73ebad9c7cfe6abf5471f77f5aef256d713f96673331f0d9b37b3aeaa3f753f3aeaf6d089877ff8b7fdc03ffc67b9630cfee11ffee1ffac7af03f2ff88f05ffbd78f8877fdb0ffcc37f963bc6e01ffee11ffecfaa07fff382ff58f0df8b877ff8b7fdc03ffc67b9630cfee11ffee1ffac7aafc0ffdbfffb7fffefe308b099aef23d4b3317b45acbb3af75e8d5d671763f67f9cdfa3cf3fd80ff5af0ff3582ffeb05ffb5e0ff6b04ffd70bfe6bc1ffd708feaf17fcd782ffaf11fc5f2ff8af05ff5f23f8bf5ef05f0bfebf46f07fbde0bf16fc7f8de0ff7ac17f2df8ff1ac1fff582ff5a5771f39e1976a562c7989df3713335ba75b33abe97aabeff6bc24c472f9aef27eb6de6d2785597ceaec3eedbd1b3f29edd79dfcfec1d8afc3afb1b9dc70a6c51bc3af7eebd5e51f72ce11ffee11ffebb9ed118fcf704ff7a1efeef638f0afe3fc7e07f93f33e1ffee17f3616fee704ff7a1efeef638f0afe3fc7e07f93f33e1ffee17f3616fee704ff7a1efeef638f0afe3fc7e07f93f33e1ffee17f3616fee704ff7a1efeef638f0afe3fc79e81ff779b6c9bf2177516dadbd8f8cf7b6679aa56f4ae16ad3679f430fb178bfe377af6f5663f0a632dd5c7c1ae5bed57f6f1b2b12a3feb5df5d6fd80a967156b6ba9be7c1f2a3ef3f37d677761ccd918eb33f6af7b67fd3aecafdadbe81fc9ea0cfc5eabb86c0fe17fef03fff0ef7de05ff7e5d700ffb9b715fc7fd6527df93e547ce607ff9f1ef05f0bfef3dee01ffe7dbe5f03fce7de56f0ff594bf5e5fb50f1991ffc7f7ac07f2df8cf7b837ff8f7f97e0df09f7b5bc1ff672dd597ef43c5677ef0ffe901ffb5e03fef0dfee1dfe7fb35c07fee6d05ff9fb5545fbe0f159ff9c1ffa707fcd77a14ffbfffbf201c0baa1a55b13ea713b36be4adf7ffbf32fa48f95a9967e5a1ded59c5a8b07d52bab19ad41293ab30e283e5e7d5cb2f354cf516f7e2ceac78e55b1aa76744e918fefb7ea25aba3ce203bfbee07a1cb4776f6aaef237dc1ff7d1f6acde31dfee1dfd751f191e01ffee15ff70efff0efc7e11ffee15faf17fee11ffee11ffee11ffeb75dbff00ffff00ffff00fffea1dfee13faa03fff00fffba373f16f563c7e0ff3eded7ab7a53eb85ff27e5fffffedfff1bdfd6466376cc17509b72449d8f8ccae91caccf19aa608ee62360b30f6eb7ee4a3fab79d15977f630026805bc4e3fddfbb1b24f3377b9fb8f40f71f63abe883d0a9bb2af8ef792b0ff8cfc7e15fe7c1bfeec3d7e8e4c0ff7a1efcef6bc03ffc575ef00fffd9bcea13feeb3e7c8d4e0efcafe7c1ffbe06fcc37fe505fff09fcdab3ee1bfeec3d7e8e4c0ff7a1efcef6bc03ffc575ef00fffd9bcea13feeb3e7c8d4e0efcafe7c1ffbe06fcc37fe5d5e5ffa7bffccbbffc65734523b3aae9e8502adfe8b753d3c7ce7acc28f254e39dc3c9f22b6ffb3cd3d7cc7c94e32fb49f9ff553de7e3ccaedc23db3176a7f3b1fd48ecef81845fb34eba17eb3d86e4fdd3cf88ff32b6ffb0cfff0bfe2a17eb3d86e4fdd3cf88ff32b6ffb0cfff0bfe2a17eb3d86e4fdd3cf88ff32b6ffb0cfff0bfe2a17eb3d86e4fdd3cf88ff32b6ffb0cfff0bfe2a17eb3d86e4fdd3cf88ff32b6ffb0cfff0bfe2a17eb3d86e4fdd3cf88ff32b6ffb0cfff0bfe2a17eb3d86e4fdd3cf88ff32b6ffb0cfff0bfe2a17eb3d86e4fdd3cf88ff32b6ffb0cfff0bfe2a17eb3d86e4fdd3cf8df7aff0b5899d45fe9fd687ad41ea8bf9255753bfddc62dedfdf0f7f0c566a3fa31ed1f74a8d67df4ff887ffd9dacf28f85f13fcc3ff6ced6714fcaf09fee17fb6f6330afed704fff03f5bfb1905ff6b827ff89fadfd8c82ff35c13ffccfd67e46c1ff9ae01ffe676b3fa3e07f4df00fffb3b59f51f0bf26f83fb607ef03400ba21a53ef5943d1afaa63e7663e089d781513adaf8a89fa1d9bdfd9439fdbedf9265b273b74ff91b6bfe3d9fe85deecde67716a2f667c66f764f57cfdbcdfcb997bd8a96df75cdda348d13fb06a6c855bf8d782ff58f05f7bc23ffc4735e03ff681ff7bc17f5e07feb7306ec4dadff10cfff01ff5bbe29379c2bfee09fee11ffeeffb877ff88f62e17f93dedee763825bf88ff37c0cfc6f3bafacc7151ff8bf17fce775e07f0be346acfd1dcff00fff51bf2b3e9927fceb9ee01ffee1ffbeff1f8dff7775c063cccfa942557e166bdf7dcde840ecc2a2589fd7f928f8b9d1bbcdf5fd8eff46ae02acbbbf558ef2882e89edc1af43f947e7dda9dd55f50f8a1f53b1ea2369c7ed9aa33e3bf7a20b52d4a7f29e61ca4bdd4d3fefefa2bf035bb016f8dfe41cfcf76a7705fff00fff7aadbe3efceb9af0bf490ff8877feb03fff00ffff0afd6a1fce11ffeb33564bdc23ffc7704fff00fff7aadbe3efceb9af0bf490ff8877feb03fff00ffff0afd6a1fce11ffeb33564bdc2ff31fe7fff7f41983550c1131d8c6f3afb28582fe5e36b568a0e5ff5305bafb327d958d4ebccfa7cae7fcee6d5dab33354f3d11e4479debbea37dbe3e88314f51ec54471aa77e53fc6d4beaa9c6c8dd15e77f6395b837ab7e3519ff00ffff00ffff00fff6a1efee1dff6e83de07f4b3d941ffcc37fe4ad04ffb1e03f9e87ff2d8d837ff8cfeac13ffcc33ffcab9a3e1efee13f5a03fcc3bfefa58a8d72fd73360fff5b1a07fff09fd5837ff8877ff857357d7cd5f34ffffedffffb5f4690df904c19f81dd0b30f83f5b1bf7e3c9acb2e6de5add69079d8ffec7a7c8ffe3f5f27f257bffe598da97d517b1bad498dab0f53b53fd1c772e68cb21e3a3e957776bf7c2d5bb37bf7ec9e56773cbb1bdd7be363b3b1a87625f887ffcc47d5b4f1336794f5d0f1a9bcb3fbe56bd99af00ffff00fff360efee1ff0dfe654d1b3f7346590f1d9fca3bbb5fbe96ad09fff00ffff06fe3e01ffedfe05fd6b4f1336794f5d0f1a9bcb3fbe56bd99af00ffff00fff360efee1ff0dfe654d1b3f7346590f1d9fca3bbb5fbe96ad09fff00ffff06fe3e01ffedf26f8fff50fb0b6499d0dd6d9f3abea7e586ccc0afc338a6a74f7e0e85ef90ba37a3a0becb3ce3503f6884f151b9d53878799333ee31f9d2382ffcf18f887ff110bfff07fb6e07f5ef01fd73943f0ff1903fff03f62e11ffecf16fccf0bfee33a6708fe3f63e01ffe472cfcc3ffd982ff79c17f5ce70cc1ff670cfcc3ff88857ff83f5bf03f2ff88feb9c21f8ff8c39caffdbfff93fff473a547ffd35ab67f17bc447f311baade3dbb76f5317bfda333f3fbbc7b687ac9fe199cd676bb17d3de23cfd5f73aee4aece7ba97ffcfcfacf600dfe9f5bf00fffea7dc5f38cbe5ec50ffee17f56f07f5cf07faee01ffed5fb8ae7197dbd8a1ffcc3fface0ffb8e0ff5cc13ffcabf715cf33fa7a153ff887ff59c1ff71c1ffb9827ff857ef2b9e67f4f52a7ef00fffb382ffe3827fadf75b331d70558c1daf80c9decf98eb2a5b8bfaf5cf2aa7caeff453f5e5e36f1f5f1537db8787b9d3afed239bf3fb93e52a1fff6cfb8cd677d61df1f5ecaf5a47c442e71f1b553bdb5b3ba6f62af254aace228b89d6ace2b2f733e6baaa3883ffba5fdb473607fff0afe66662bb735d559cc17fddafed239b837ff8577333b1ddb9ae2acee0bfeed7f691cdc13ffcabb999d8ee5c571567f05ff76bfbc8e6e01ffed5dc4c6c77aeab8a33f8affbb57d6473f00fff6a6e26b63bd755c519fcd7fdda3eb239f8877f353713db9debaae20cfeeb7e6d1fd91cfcc3bf9a9b89edce75557106ff75bfb68f6c0efee15fcdcdc476e7baaa387b34ffefb7ffe3ffead18e4587ea632278ed9cf5ca2e92f2183e9d4b567d08bcafea3f03c7afc3e6fbbf94ec5c28b5ffbe8f31777bb7ffd975f8f8a88eda972a5efdfadaaa67dfb7ca8f7cfcb96497fbe3a3f78f811acf405239f6acfdb3bfa72a5ff5acee61f77cd49c7df7ff605577cc8ec13ffcc33ffcc3ffbe16fcc33ffcc33ffcc33ffcc3bf1d83ff4df6adf2231ff8877f3b0ffff0eff3fd5ce40bfff0afe2d4befbb5a97eec3ae01ffee11ffeed18fc6fb26f951ff9c03ffcdb79f8877f9fefe7225ff8877f15a7f6ddaf4df563d77184ff5fff5f10aa06a2cb108df967bfc9fe5d79a903557df8b96eed2acf2b5b83afa57af4b97e6e66cfa30bd8596f5657e5677b10c5443dabf577c7a24b1cede5cc1977ee971db379d94725fa804677d3ab7b17bbf9595fd90714fee15fed411413f5acd6df1d83ff7abc8a837ff8cfeac23ffcc33ffcc3ff26df557ff0bf1f837ff8573dabf577c7e0bf1eafe2e01ffeb3baf00ffff00ffff0bfc977d51ffcefc7e01ffe55cf6afddd31f8afc7ab38f887ffac2efcc33ffc7f1dffef36e8f63cfef363558c2d9269c4d806a30b609bb5357ded5b4c76f17dbda8a6cf5117d5cb8f456b52b5b33565eb526bb1bd5bcf085a555bed4775b61558630dd54754f535de55bd0cb4088c686d59ddc8439dbb5d53f491cbf6339a9ff9f8467dfaf5f9fb6763e01ffe554d25f8df7671f05ff708fff03ffce1ffbe7f3f0ffff0effb817ff857f9519ff00fffbe4ff8877ff88f7b5467e8a5ee81f2877ff8877ff8b773f00fff6a6d7e1efee1dff703fff0aff2a33ee11ffe7d9ff00ffff01ff7a8ced04bdd03e51ff1fffbff02d623d4bd4857e5cffa5e556fb59faff67a95fa5fbd66ab4e2f9dfb37b3a6e81fa64eec9582ff63fd7cb5d7abd4877ff83fc317fe5fb33efcc3ff19bef0ff9af5e11ffecff085ffd7ac0ffff07f862ffcbf667df887ff337ce1ff35ebc33ffc9fe10bffaf591ffee1ff0c5ff87fcdfaf00fff67f83e626fdefef11fff31ad706be0d64857b3f15fadaadfaf5a8fad9bf5f0ecfb1df5d7ed7b657d9d33bda9f25ddd5bfff13b22d543b7ff3304fff07f44f00fffcf2cf8bf56f00fffcf2cf8bf56f00fffcf2cf8bf56f00fffcf2cf8bf56f00fffcf2cf8bf56f00fffcf2cf8bf56f00fffcf2cf8bf56f00fffcfacafe6ffbd0ae8168f2e85bd8c55ae1a9f9d1b63ddbab77e7dac07a8eb99f974e2adec3efa1ea2fe66fcab18bf66b507fe597945f7a77baf6c5c673d23c7ef97dfb3ec1f341be7c7677ab0ff8076737d8cea33ea3ff35777b3d30ffcc3bffa55cff0bfc91af0dfcb55e3f00fffaa5e9503fff91cfcc37f1603fff01fd5cffce13f7eafe2ade07f1f07ff7bef2c379b837ff8cf62e01ffea3fa993ffcc7ef55bc15fcefe3e07fef9de56673f00fff590cfcc37f543ff387fff8bd8ab782ff7d1cfcefbdb3dc6c0efe9f83ff9f7d335bd148e7e0fd61d877e511798e78fb5c8133e63b17416deeb8dcd95afda175f6c6c6a8fdb27be5c18ff651adc3cf293fd573d49bad91e5456bcefaf6fed93df177a05a7ff4ab62bc3af0aa5875f7ec3ad4dea9356435948fad93add52bea730bd606fff00fff712cfcdfbfc33ffc677d7b7ff8877fb51ef8877ff8df76b1f01fd784ff2df4533dc3ffbeff8ee01ffee11ffe87affdb57dab75c1ffbe0fb51ef8877ff8df76b1f01fd784ff2df4f3fdd838f887ff2d581bfcc33ffcc7b1f07fff0eff35ffefaab9ce81fb867c9e8df5cf6353c77f3ed75f5abf701f1bc9c6fa1efc9aec587661d5e5a92eb8fab55e7effa2c3cdf6d9f6ef6bd8bd5675b33ad1258df6d48f7bcf2c2ff2b68af6d4ae41ada9734fbd6f546378a8b545f7597d94d47d8a54c5a97b58eda5ed2f7b87fffb3c3f76847fb57f67f26ff7e151fc5bc13ffcc37fccbf5fa3fdf5fe2bfcfbfd7e04ff9e07f8877fef05fff0eff3226fab37f80fe7e17fdf1bfcc33ffcc33ffcc33ffc6bc13ffcc3ffdeff0dfe436f1b0bfff00fffdb9d37fcc33ffcc3bf8df5f9f00fff6ffff88ffff8512dc4169a9d9b895139a3b7e8a3d0f1b087d05d43b786ca1b52e39dfea37567f5b38f4c9473f6bcafddddd39971bfde2ad6f653d58bd4f98077f6abdab7a1d9bdefdc711513ddd56eddcedc4c8cca19bdc13ffcdb31f8d763f01fd785fffb9cb3e7e1bfce87ffbee01ffee15f7b4563f01fd785fffb9cb3e7e1bfce87ffbee01ffee15f7b4563f01fd785fffb9cb3e7e1bfce87ffbee01ffee15f7b4563f01fd785fffb9cb3e7e1bfce87ffbe7e04fe7ffa77ffeedffde22f807f9f6960469dba33bd1cc939a2a8deec3edae7ecf08eaccfc237eb33b3ce23f7a2ea417d40ceae5f7957f3d97bb7ee78f6bf670bfe8f09fee13fcb85ff38377b8fc6667daf16fcc37f960bff716ef61e8dcdfa5e2df887ff2c17fee3dcec3d1a9bf5bd5af00fff592efcc7b9d97b3436eb7bb5e01ffeb35cf88f73b3f7686cd6f76ac13ffc67b9f01fe766efd1d8acefd5827ff8cf72e13fcecddea3b159dfab05fff09fe5c27f9c9bbd4763b3be57eb08ff6ffffb7fffeff32911aa80b1ba0adeafd2ccda57bd558d2beb7614d59f395ff5d78fafa233fa7db53547827ff81f82ffc77a3c83e01ffe87e0ffb11ecf20f887ff21f87facc73308fee17f08fe1febf10c827ff81f82ffc77a3c83e01ffe87e0ffb11ecf20f887ff21f87facc73308fee17f08fe1febf10c827fbdf6771f581945e3764ec5a98f838abb8d7dfbf66d1757f92b9f8e3abedd75ab58ff01e9f8763472fd65b53dad5ce4ce5afdefaa8f8f8bd6d1bd972b679e8dcdccab787be7bb77cbafa5fa6875d6d2bd77f00ffff0dfcfa9e2e1ff53f00fffdec7c7c13ffcc37f4ff0bfaf05ffb96796938d77f2e05fc7c27fece3e3e01ffee1bf27f8dfd782ffdc33cbc9c63b79f0af63e13ff6f171f00ffff0df13fcef6bc17fee99e564e39d3cf8d7b1f01ffbf838f887ff1f8dffbb3fc0ea02eb9b8e0aa8585b2bfaeb48ef37c6c682a38f4eb421d99afc5f577a2f3fae7af5ebaa6a29a94b6661f4cf515dd5d3c8ebd4cc7ab735549eff8d6af97f24c698dd9f51cfae27da3fd557b6ae284ee5d9fd8efcec985d971dcb7aef7cb0a2bad507dad650bda9b84af0bfedfaf27e592d25f8877f3f1ed5a8e2a21af0bfc97a761df01ff70effdacf8ec17f1e6b6bc17fdc27fcc37f54a38a8b6ac0ff26ebd975c07fdc3bfc6b3f3b06ff79acad05ff719ff00fff518d2a2eaa01ff9bac67d701ff71eff0affdec18fce7b1b616fcc77dc23ffc4735aab8a806fc6fb29e5d07fcc7bdc3bff6b363f09fc7da5af0bf6d3f579be62fbfba4cd1c1aa4ba9fca33175101e641fe741c97a8dd69dc1961d6676c1d5c5517319ec917fb5e6a8c7e82361fba92e983f0b1be3bdd539aab54677c2f7a572aa75fb1ebdd4c7def7ae6254ac5dfbb6edcf2f5abbda2fef97ddfdec439add511fa37a857ff8577ef0bf8fb56bdf36f8877ff8b7fdc33ffcc33ffcabde9427fcc33ffcc33ffcc37fe40bfff09faddbf7e805fff00ffff00fff7a3fe01ffe951ffcef63eddab70dfee11ffe6dfff00fff3f3affbfff2f606597c96f882fe8e754213fdf012fdaac0c8e916741b187e673bc6ff6c1c9fab035ad570654b5af7e9f6cef6aafa2439eb9b0de238b897af731913c50aae7e8aef81a0a7cff3cfcec5cb68eecec23ff682d0a4ed59beac9aed5c6280ed45db7bf519ff00ffff0bfc939351ff9c33ffc5b2ff88f05fff06f7de0ffd3cb7af89af00ffff0bfaf05fff09f3d0f3f3b07ff9b9ccffa511ef00fff761efeefc7e01ffee17f936b81ffed6e0efee1df7af89af00ffff0bfaf05fff09f3d0f3f3b07ff9b9ccffa511ef0bfc6ffdbfffa5fff6bdf49436a03fd9c5aa4f2893e1c9ddcac4e741154bd95fadd9a47e2a2dc9b66f267ea9db5175fe57fa6aafb393bdeadb7a24eee11debccf10fce7358fc445b937c1fff582ffd86708fef39a47e2a2dc9be0ff7ac17fec3304ff79cd237151ee4df07fbde03ff61982ffbce691b828f726f8bf5ef01ffb0cc17f5ef3485c947b13fc5f2ff88f7d86e03faf79242ecabd09feaf17fcc73e43f09fd73c1217e5de04ffd70bfe639f21f8cf6b1e898b726f82ffeb05ffb1cf10fce735b3b8dd1f603de203729657b6c1477b198754c53db3667aacd61bf97ec57d79941ef9119dcd9939af2bfa807ff8f7cfdd9c5711fc1f8f3bb366270ffe3f05ffc704ffc7e3ceacd9c983ff4fc1ff31c1fff1b8336b76f2e0ff53f07f4cf07f3ceecc9a9d3cf8ff14fc1f13fc1f8f3bb366270ffe3f05ffc704ffc7e3ceacd9c983ff4fc1ff31c1fff1b8336b76f2e0ff53f07f4cf07f3ceecc9a9dbc57e3ffdd0f44a67e7cb5f8589c1fb3bf2a676bf6d01dcbfabb496db07d57eb98d1151fa323673403b3df8795b82bce6e461ddf6fdfbe6d2baaee86da9799b3bbeae33bbc3be3f00ffffe79260efeef9fe11ffe8f0afefb82ffd8bb330efff0ef9f67e2e0fffe19fee1ffa8e0bf2ff88fbd3be3f00ffffe79260efeef9fe11ffe8f0afefb82ffd8bb330efff0ef9f67e2e0fffe19fee1ffa8e0bf2ff88fbd3be3f07f0effef95896d68c6d83f0f553e7e6176f347ae8da92e6a75599557b4f1b37bd0bd38d95a66c63a7b5b69ec87eaa9fb4155fbd5b9b03ede8e557dcc7ef03af91ec2ceb966005bdfa8868a89e6fc391df9b8455abdfbfed9f79ee5c37f1c9b8dc1bf16fcc37fa71efcc3bfef07fee11ffeb7f4399a837ff88f7acff2e13f8ecdc6e05f0bfee1bf530ffee1dff703fff00fff5bfa1ccdc13ffc47bd67f9f01fc76663f0af05fff0dfa907fff0effb817ff8bf92ff77db844a1c172402325b80cd551be99bba8d8fffac148476ae73093b7dfa9c6d8b61b77b13497d80ec3a7dae02deafcf2bea4ff5ebfbc8faf667e0cf2d3a5b3b67eb79cf99731bf199d4def95c3b3fc6aafdf5e3761f66cfbfe39dfd663ebe6674b7230ee01ffe6d1ff07fdf87da07f887ff99f3877ff8afbcb3dfccc7d7fc80ffe93e7dceb6c1bfefc5f70afff00fff5af00fffca23f3ce7e331f5ff303fea7fbf439db06ffbe17df2bfcc33ffc6bc13ffc2b8fcc3bfbcd7c7ccd0ff89feed3e76c1bfcfb5e7caff00ffff0af05fff0af3c32efec37f3f1353fe07fba4f9fb36dd7f3ff6e2f5354389a8b36cce7674da9857978a28dafea79e03320b28fe288b77bd581cbe6f97db6cf0a122575a07e3ebbe8aa8723d0786fb5365f27aaebe7ac5f166bd7aceafbbb54edb5822caa57b1127d0ca29ce1ad7eedbe285f9bafe2bc97dd0fdf77b626fb0bff716d3b07fff0affc7d0efcc37fe607fff00fff7b9fa81efcc33ffcc3bfed358a877ff8f7b5e07f4be7e03fae6de7e01ffe95bfcf817ff8cffce01ffee17fef13d5837ff8877ff8b7bd46f1f00fffbe16fc6fe91cfcc7b5ed5cc6ffdbfffc9ffff34315b363d1bbbafcd1c6f94ba562bcd4053ca26883a3b16e7f33f58714185daf683faa0b16cd756b5679b3fb91e574efddd13a2bdeddfa111319e836be5b6f66beda075f1ffee1dff67934a69b03fffd7af0df13fcebb96e4df8afbd7cdc4df00fff237e08fef31cf8efd783ff9ee05fcf756bc27fede5e36e827ff81ff143f09fe7c07fbf1efcf704ff7aae5b13fe6b2f1f7713fcc3ff881f82ff3c07fefbf5e0bf27f8d773dd9af05f7bf9b89be01ffe47fcd0ef7b60ff000bad6be5a3f028ad5c94af5acb6aed67deff67137b75bee0ff1cc1fff562afce17fc9f23f8bf5eecd5f982ff7304ffd78bbd3a5ff07f8ee0ff7ab157e70bfecf11fc5f2ff6ea7cc1ff3982ffebc55e9d2ff83f47f07fbdd8abf305ffe708feaf177b75be56f7f4bd1374f4b06c7ee6b55ae7ab2fd3ad7ed54367dddd7deacc57b159add9fd3c63ffd51e747256f25e4157ae6bd61bfeebfaf07f4cf07f2ff85ff3ff0ac13ffc9f2df85ff3ff0ac13ffc9f2df85ff3ff0ac13ffc9f2df85ff3ff0ac13ffc9f2df85ff3ff0ac13ffc9f2df85ff3ff0ac13ffc9f2df85ff3ff0ac13ffc9f2df85ff3ff0a1de1ff7d36c1ced9df28fe366eff27c4ec73e5ed0f2efa500ccfae663f489547a7befd0b39bf9691eff729fb3047f355afbea67f56b9eabc7d2f2a6f662cfb9fe053bd8c9cd9b3b7fed599cf80d589b5f1d5b8bf139dfa91971ab72c56b1554df8877f95373306fff09f79c2ff7d5df8877f5f3ff252e3f01f8fc1ff7d2efcf7bce05fc7c23ffc5763f00fff9927fcdfd7857ff8f7f5232f350efff118fcdfe7c27fcf0bfe752cfcc37f3506fff09f79c2ff7d5df8877f5f3ff252e3f01f8f3d92fff70cb0ccccfefa6715eb0fdcc7abf9e8dd6fbc3a2cef5d6d98ea2dda8fee616575b38b68f737db37f5e1f6f5fd7c14d3f9d0743f0ab67755273b0bff8fcfb6e51fe9d98fa85f87da231b57fdc360bd3af7b1e244adcffe17f9f9b564bdfa0f21fcc33ffcc33ffcef7b8efcd4b317fcc3bfcf837f1d0ffffb5ee07f93ebf07e7eccfbc1ffb65b23fceb3ebd6fe4a79ebde01ffe7d1efceb78f8dff702ff9b5c87f7f363de0ffeb7dd1ae15ff7e97d233ff5ec05fff0eff3e05fc7c3ffbe17f8dfe43abc9f1ff37ef0bfedd608ffba4fef1bf9a9672ff8877f9f07ff3afe47e5ffed7ffc8ffff1a10ecf6f5c76a0b660e7f0bbf25ed101d8dabe8fa89fcad35f081fe3bd559dea23ab72bd77e4a1d61ef5527965bdaa7e8e9c6ff6f1aef6b21aebbcdb3aaab7eacea8fe3b7bebef53e76e6e41cfdec7d6886a47ef7e0cfee15ff509fff71ebe6ff8877ff8877ff887ffc83b5a1bfc7faec3d6547d6463f00fffbe0ff887ffadd923fcc33ffcc33ffcc33ffc6f613ff0bfef1bfee11ffee11ffee13ff28ed606ff9febb035551fd918fcc3bfefe3d9f97f57172adad8089eee658d2e91f78e36a182e9f6ac3e5ebe86f7f41e2ac68f65b2bdd9dceac2d85af637bb0cc3dbaf213b2bb50f91fc45cb2e99daf38ebf8db76717ed97aaeff3b235aabbacee8caf61fbcdcec9d6ae3e006a1dbe477f66fece46f0ab71d563d44b94e305fff782ff6dcadfc6c33ffcc33ffcc33ffcc33ffcfb3ebce01ffeed3cfcc33ffcc33ffcc3bf9df739f00fff36c7d780fffb78f8bfef17feefe7b335a938f8877fef07fff06fe7e01ffe7d1f5ef00fff76fed5f8ffe9dffc9b7ff38b4ff605d5981f8f1ae900a93cfd055297ba3ac4eae267fd6497aa035454a3b31f55cd2caff29ced3ff250eb98e9b3ea6bf6c3957956f7aed3e7ec5aa2fea38f65145bd5e9f43223f887ff4e3ff09fc7c23ffc8ff8e81dfee3bee01ffee15fd784fffbbcca13fefb827ff8eff403ff792cfcc3ff888fdee13fee0bfee11ffe754df8bfcfab3ce1bf2ff887ff4e3ff09fc7c23ffc8ff8e81dfee3bee01ffe7f04fe7ffd7f41b87da79ad9ac333e4ccfa22bd6f2a8fd1917d402d6f9301fed6fe6c3885e43f0ffdc9e519d9be01f1d15fc3fb76754e726f8474705ffcfed19d5b909fed151c1ff737b46756e827f7454f0ffdc9e519d9be01f1d15fc3fb76754e726f8474705ffcfed19d5b909fed151c1ff737b46756e827f7454afc0ff7b27e88cc6663daeaa19f97ec51a8fe63dcaefa66fdfbe6d8fd0adf7d1bf7db6f3d5f36adda88f2cae1a5f8d7b26c1ff7abd2bf31ee57713fcebb86a7c35ee9904ffebf5aecc7b94df4df0afe3aaf1d5b86712fcafd7bb32ef517e37c1bf8eabc657e39e49f0bf5eefcabc47f9dd04ff3aae1a5f8d7b26c1ff7abd2bf31ee57713fcebb86a7c35ee9904ffebf5aecc7b94df4df0afe3aaf1d5b86712fcafd7bb32ef517e37c1bf8eabc657e39e49f0bf5eefcabc47f9dd04ff3aae1a5f8d7b26c1ff7abd4aef99d1b8849dbf0ef31756795475948f8d5379dd9ec6af5dcb98cffc2ae0ad87fa8b4d95377331aa7eaabdc92e5c94afce42adadd37316539db39ab3bfea4e457ed1fdb473d95ef81cf5ee7366ce74952ffbdc1df3cfaa96ad09fff133fcf7fbade6ba7772fcc2fffd7377cc3fab5ab626fcc7cff0dfefb79aebdec9f10bfff7cfdd31ffac6ad99af01f3fc37fbfdf6aae7b27c72ffcdf3f77c7fcb3aa656bc27ffc0cfffd7eabb9ee9d1cbff07fffdc1df3cfaa96ad09fff133fcf7fbade6ba7772fcc2fffd7377cc3fab5ab626fcc7cff0dfefb79aebdec9f10bfff7cfdd31ffac6ad99af01f3fc37fbfdf6aae7b27c72ffcdf3f77c7fcb3aa656bc27ffc0cfffd7eabb9ee9d1cbff07fffdc1df3cfaa96ad09fff1f351fe77ff2f087da376e33c8c9d9c31ef7dfca1dadc685e29ea29aba37a8ad6515d9e685dfe303afba83cfc1aaa7add1a56d93a673cd5deaa4badf628ab5dddc34eefde2b5b4b96371b93e555bd44e7dbf18c9ed5d9fbfc310efff03fe309fff00ffff00fffbd3cf887ffac67f8dfe43cfcc37fd5535647f514ad03fefb9ef00ffff00ffff0dfcb837ff8cf7a86ff4dcec33ffc573d6575544fd13ae0bfef09fff00ffff00fffbd3cf887ff9fed80df0cb5181f7f1b1bf1d921dbfc6881d1a1a81e6d8c5a981ff36bc9d6545d0c3fe7bd3300943a172dfba045f5a30f9d5f5375f1a33556e79dadc3f733de55df915ff6618ace36eb335bd3cabaed5e8ff74e9fd9c7a8f3718d623bf75dad23ead3af11fee13fcab173de07fee13fda3ff8bf1f877fbd56ff0efff01fe5c13ffc2b5ff8d7eb88faf46b847ff88f72ec9cf7817ff88ff60ffeefc7e15fafd5bfc33ffc4779f00fffca17fef53aa23efd1ae11ffea31c3be77de01ffea3fd83fffb71f8d76bf5eff00fff51de33f1ffb3351b8119b47ef1d97c145b01eff3461fdecb2fba93aba0559e6aa3ab8fe3f053b10a323f6f733360a27ea20f527649bcb27d1e6336ae73c9d4fab2baddb9ece3eb73fdfd8e7cedc7c0e767fdbf89f352f7a95aff9babfff6f6969ea3ca57eb8b3c547ff00fffb64eb60ef8bf5f1ffc6fa13ffcc33ffcc33ffcc37f56b73b07fff0ef7bac72e11ffee17f93fddbbee11ffee11ffee11ffeb375c0fffdfae07f0bfde11ffee11ffee11ffeb3badd39f8877fdf6395fb6cfcbffdf7fffedfc35d530be8ce65873123755067fa77ebfbe7acbfaedf993dde547d3cbbfda8cbb8e2ddadb7e27153d767a6e6cc5aa3b9d9359ecdcb4d67f8c13ffc77de677b85ff63f195d74df07f8ee01ffee11ffefd73d65fd7efcc1e6f82ffe335e1bfe705ff717f5dbf337bbc09fe8fd784ff9e17fcc7fd75fdceecf126f83f5e13fe7b5ef01ff7d7f53bb3c79be0ff784df8ef79c17fdc5fd7efcc1e6f82ffe335e1bfe705ff717f5dbf337bbc09fe8fd784ff9e17fcc7fd75fd7e7dcffe00cb6a14996d7a26ef517ae40166b27bf35517ec887c9fb7e7f7f7f72febbdbb6f67dcc9d53352795732623f0e476ac0fff982ff6bfb8904ffeb3e1dc17f4ff07f6d3f91e07fdda723f8ef09feafed2712fcaffb7404ff3dc1ffb5fd4482ff759f8ee0bf27f8bfb69f48f0bfeed311fcf704ffd7f61309fed77d3a82ff9ee0ffda7e22c1ffba4f47f0df13fc5fdb4f24f85ff7e908fe7bfa756fba7f80f5ea9addf4eec599bd60675cc8475eea2397f5489fafb64f550f57aee7193f72cf26f85f13fc1f13fc3f87e07f4df07f4cf0ff1c82ff35c1ff31c1ff7308fed704ffc704ffcf21f85f13fc1f13fc3f87e07f4df07f4cf0ff1c82ff35c1ff31c1ff7308fed704ffc704ffcfa167e5ff5d058cffc67b145315ea8c457e9d1ad653e5cf7a59750f6cc475fdede667fb11a9b3decaa38ab363dfbe7d6bc77a4517b373a7b24bddbd7b557d7bd78fec978a19ff8d1e3af7c4df59ff3cbcfc5c679dbeb74eef3eb7eb91e56535abb128d7fefaf1192f2bf8ff4df0af73ab18f887ff28def6a504fff00ffff09fcdc1ff56cec33ffccf7859c1ff6f827f9d5bc5c03ffc47f1b62f25f8877ff887ff6c0efeb7721efee17fc6cb0afe7f13fcebdc2a06fee13f8ab77d29c13ffcc3ff8fc1ff7b14941daa37f19b105d0af5f1c9362afb58f94bd9853dba5c472fa5af9fc1e4d7e9fd1468766efc97ad239a57ead6f7e3fe43e6d798f5a6eadbcb9c01e5d7e6733c14fe59c1a5f62bfb6064ef518ee5aaaa61fbf1bdd9f5f9b5a87df2fb9dfde336e6e17f9f03fff0afc6abfdb575e17fdbc52aaf28debfc33ffcc33ffcc3ffbd17fcc33ffcc37fe413f59ad587ff4d8e57fb6bebc2ffb68b555e51bc7f877ff8877ff887ff7b2ff8877ff887ffc827ea35ab0fff9b1caff6d7d685ff6d17abbca278ff0efff00ffff0ffaafcbf772f4276c8fe126480a88b536d5ee7b75ac798cb3e6ede4381ea7b8f3cb303f1174b41995d5035aff6dc03f8117c0c23a0d4985f7796dbe9b1b37f762dd1de641f19ff6c7bb7b5d53dcccec28f65fb127d4c220ea29ad5992a5eb3f98a1bf887ff0ff8dfd5fb80ffdfe7e11ffe556ea747f8877ff887ffadb147d1da7dbc7f87ff78bee206fee1ff03fe77f53ee0fff779f8877f95dbe911fee11ffee17f6bec51b4761fefdfe13f9eafb8817ff8ff80ff5dbd0ff8ff7d1efee15fe5767a847ff8ff91f9ffb9d340d57474b97cb3b601e5eb0f457d48d4fc788f161b7db0a29ebd54bfe3b97348ea03e0e5f7d1c7667b18cd596fa5e89c3bebf531632eaa97f5a82ea7edad5aabefd1e667f7f7a3f121f48aeee2b6b0c6ac6fdf67f50f858fcd3e4add0f1efcc7fdc23ffcc33ffcc33ffcc33ffcab3d81ff2dec01feb7b46fdf27fcc37fd413fcc3bf8f85ff4dc6c1ffb61b877ff8b771f0bf95fdc23ffcc33ffcc33ffcc33ffcab3d81ff2dec01feb7b46fdf27fc7f9ffcfff46fffedbffd2502546d8a9fcbf26615e50c08b3b1e8c3d6a9917df8aab115d9de3bfd6473ea039879abdc6edceafeaa98ca4b5df04e9f477bccc0c9fe21e8d4e9eecdecbdaafae9aeb79a877ff8af04ff711df8cf05ff5b3816cdc1ff9c6fe503fff13cfcc37f25f88febc07f2ef8dfc2b1680efee77c2b1ff88fe7e11ffe2bc17f5c07fe73c1ff168e4573f03fe75bf9c07f3c0ffff05f09fee33af09f0bfeb7702c9a83ff39dfca07fee379f8bf80fffff6dffedb4716d859d44a0367fa5ed5e7aa9ead9f476be6833de3f9bdede9ea9a1eb517f0bf26f887ff8ee0ff39fd8e0afee1bf23f87f4ebfa3827ff8ef08fe9fd3efa8e01ffe3b82ffe7f43b2af887ff8ee0ff39fd8e0afee1bf23f87f4ebfa3827ff8ef08fe9fd3efa8e01ffe3b82ffe7f43baa997edeb3c9d5455579d97c35a7e667fbbcfab01e7d19a27a2b7df89c558f9177d61d7a963d3dd3ef2abece12fcaf09fee1bfe307fff373f0dfaf07ffeb827ff85f15fcc37fc70ffee7e7e0bf5f0ffed705fff0bf2af887ff8e1ffccfcfc17fbf1efcaf0bfee17f55f00fff1d3ff89f9f83ff7e3df85f17fcc3ffaa66fcdfd545b7f0d8f9cef86a73637cfcf5a4aa63e7573f3ab7f9ea2fd4d4dafc7347d5be8cf5752f6cd48bdd33ebebc77d5ec76bec95eab3eacbfba97b13ad5ff59ef5adde67f3a3fad53a3b1f057f6f2396baaa3e46dd7f04e13ff7807ff887ffd8db8f77facec6e1bfe703fff00fff7d7f3b0ffff118fcefebc37fecedc73b7d67e3f0dff3817ff887ffbebf9d87ff780cfef7f5e13ff6f6e39dbeb371f8eff9c03ffcc37fdfdfcec37f3c06fffbfaf01f7bfbf14edfd938fcf77ce01ffee1bfef6fe79f89fff7eaa2db05dbc62d9876bc6a5e6d70b52936c6f795f9fb7cbfae0cfeac66067e55cbeed36d3c5a87aae33dbdacbfed35ebd7f767e73b1fdd0e7cea036ff7567d9c3abdf81ad93f38d59965b27daafd8d1852bdfbfbaf3ef4913a7b6d7d7ddfd1ddcc04fff0af72a35f25f8877ff8877fdf17fcc3bff250eb81ff7d8f5ef00fffd617feeff3fdbae07fdf9f9d87fffb9ee11ffe6d2d5fbb530bfee13fea19fee11ffee13f5267afad2ffcdfe7fb75c1ffbe3f3b0ffff73dc33ffcdb5abe76a716fcc37fd433fcff78fcff9c5d2065329e7d43d985f445ed98f5b131d985ad2e91eac1e7657d2a4f95d3dd3beb171d6cb48776cd59bf914fb6b6ca3bfae06430651f94ee1d527ba314dd934afe23e2c7ed5cf78319dd39fb6be3b2f357bd285f5fd79e59767ebe06fc6f694f510efcc37f34373cecaf8d837ff8f7bdc13ffc47bd285f5f17feb7b007f8877fdf17fcc3bfea5379aa1cf887ff686e78d85f1b07fff0ef7b837ff88f7a51bebe2efc6f610ff00fffbe2ff8877fd5a7f25439f00fffd1dcf0b0bf360efee1dff706fff01ff5a27c7d5df8dfc21e9e95ffb7fffa5fffeb876f503d471bac208ec6bab2b9d9474601335b23f28c2e4af4218b7cd5bc1f53de55bfca235a4ba6e8cc2a8f6cffb3339aa995ddc3e1a3eaad9eadefad5a67b61faace6a4c566b95cf8ee7117ff8877ff8877ff887ffd55af0af05fff0af3ca2b56482ffb8b76a9dd97ea83af0df13fcd7fd2a8f682d99e03feead5a67b61faa0efcf704ff75bfca235a4b26f88f7babd699ed87aa03ff3dc17fddaff288d69209fee3deaa7566fba1eac07f4ff05ff7ab3ca2b56482ffb8b76a9dd97ea83af0df13fcd7fd2a8f682d99e03feead5a67b61faa0efcf7f423f1ffd35ffcc55ffc1219578d5773be19e5ebc7b20bae629547d6bb8d1def7ec3bcbff251bdaa8f84cfcbf6aedaf768fffc9a3275f6bbbb9f95df8ccf889fb97bfefc3bf56640adceeba8b2fbb2b277ea1c3b6b807ff8cf6256fde0bfe70fffb53ffcc33ffcd75e7e0cfe630ff887ffd558f8afe7e07f9f07ffb917fc7fcead78f931f88f3de01ffe5763e1bf9e83ff7d1efce75ef0ff39b7e2e5c7e03ff6807ff85f8d85ff7a0efef779f09f7bc1ffe7dc8a971f83ffd8e319f8fffd7f01abd3bc351fcfead0ab0b3e03c1a375b4b7d5fc2aef99f7ecd1ca20bde9ac7dba7acfb30fd651afb305ffd7e6c37f5ff05f7b9d2df8bf361ffefb82ffdaeb6cc1ffb5f9f0df17fcd75e670bfeafcd87ffbee0bff63a5bf07f6d3efcf705ffb5d7d982ff6bf3e1bf2ff8afbdce16fc5f9b0fff7dc17fed75b6e0ffda7cf8ef0bfe6bafb305ffd7e4dffd0196dde419137b3057e9ab3e8c57ebd6d7b76fdf2edfbf199d7d9e8fb81f5fa9333fa41dffd5986e1df87f9ce0fff505fff0bf2af87f7dc13ffcaf0afe5f5ff00fffab82ffd717fcc3ffaae0fff505fff0bf2af87f7dc13ffcaf0afe5f5ff00fffab82ffd717fcc3ffaadeedcb68f0f63bf5575c66f3af5aa4f29de9d3c7d9b5aaf9ae4f1513c5dbbae372446b9cd1cc7aa298477e2c67ef5ae6732466f61ef89ce8cc577ca33a4763ba1ef0dff7a962e0bfee01fe7b758ec6743de0bfef53c5c07fdd03fcf7ea1c8de97ac07fdfa78a81ffba07f8efd5391ad3f580ffbe4f1503ff750ff0dfab7334a6eb01ff7d9f2a06feeb1ee0bf57e7684cd703fefb3e550cfcd73dc07fafced198ae07fcf77daa18f8af7b80ff5e9da3315d0ff8effb5431f05ff700ffbd3a4763ba1ef0dff7a9622afedfa34405a15a84ff4fe574362a8bb173bec7d1a7aaa37a1bbff62fe266fa8ef2a2038de2ed47d7c776f622ba50e3b9f3518f62a27de98cab6755273b2bf5ae340391dd13953fee91bd4f519f4a112ff6af4aa3beb2339d3d8bc8b3b3067f3fb37955a7ea2553e7ce8f67db23fc6fb217f8dfc7c2ff16e6f871f887ff9571f8d735e11ffe7deff00fffea8efab9f1ece7d478e4d95983bf9fd9bcaa53f592a973e7c7b3ed11fe37d90bfcef63e17f0b73fc38fcc3ffca38fceb9af00fffbe77f8877f7547fddc78f6736a3cf2ecacc1dfcf6c5ed5a97ac9d4b9f3e3d9f608ff9bec05fef7b1f0bf85397e1cfee17f651cfe754df8877fdffbedfd676f3e02ed018c777f289dcbe59bf09e593dfb1c6d92cd537d44bda9fe6dbf3eb7db6355cfe7ab3df2cf914755238b55bdfa73cefa8d7c54ee78f71fbca80795aff6df8f67bdd9df997b13ed7f75d6d17d89f2aab1a80febd9ddbba80ff8877ff8afd7a1faf1827ff8cf7a817ff857bed118fcc3bf7acf6ac03ffcab5ee05ff70ffff0df5987eac70bfee1df0bfee11ffef73dc13ffc5b3f950ffff0affce13fae01fff0af7a817fdd3ffcc37f671daa1f2ff887ffac9711fbb35d587498ea70d58646c5bcd4a6fa317be876ccd752717e3daa7e16e337b482dce6f867151b7975ea7baf4e6ea4ec522b983d50164a7f26436a7ef6e3d1e93b5ac7b6e90fc1cade4677d6e7a97b90c1ac7ab7f5aa738c7a8cf6577194c5c33ffcfbfee11ffe55cfd62b5b1ffcc33ffcc7bddb7af00ffff09f8f45ebd836f887ff7dfd2c06fee15f09fee11ffee15fd5f075fc3cfcdfc7c23ffcc37fdcbbad07fff00ffff958b48e6d837ff8dfd7cf62e01ffe95e01ffecfe6ff67db886a3033c92e986aba7bb11438d65bd58ae46b44b04730da3aca37dbb7e8826597425d4c9fef7bcc7ca38ba3ce26f2f51fda0852dfa7aa3b1313f5ab6a45ebf46bb15ebe96ba0ff6b9d393ef61668dd57df2357daf6fc187b6eb91d5f573f00ffff00ffff00ffff00ffff00fffbe4f55772626ea17fee1dfd78a04fff06fd762bd7c2df88febfa39f8877ff8877ff8877ff8877ff8877fdfa7aa3b1313f50bfff0ef6b45827ff8b76bb15ebe16fcc775fd1cfc9fc3ff4f7ff1177ff1cb7692545311d47eec4c55f58ff8656333bdd88f96ca89e050ef954756eb2b94c1fb95aa3e0855ee4ade8c67f591e8e4df14fd437454f0dfef05fee17fd613fefb7da867f53eeb978dcdf402fff03feb09fffd3ed4b37a9ff5cbc6667a817ff89ff584ff7e1fea59bdcffa656333bdc03ffccf7ac27fbf0ff5acde67fdb2b1995ee01ffe673de1bfdf877a56efb37ed9d84c2ff00fffb39ef0dfef433dabf759bf6c6ca617f887ff594ff8eff7a19ed5fbac5f3636d30bfcc3ffac27fcf7fb50cfea7dd62f1b9be96530b9fb03accaf82a58667dbb1f9595cdcb3e8c335a3decee059dfd80752ee3ea9e56f167fb3ef2a3ad209ab923ddf5fb1cf58fc1595afdc706fefb82ffcf78f887ff337de11ffe1f25f8efcf1fe967361efee1ff1182fffefc917e66e3e11ffe1f21f8efcf1fe967361efee1ff1182fffefc917e66e3e11ffe1f21f8efcf1fe967361efee1ff1182fffefc917e66e3e11ffe1f21f8efcf1fe96736fe7be0ffed1ffee11fe429cffca55d26e5739637eae92bf77ba5b6fa9f76fbdeefcb196b3c739fe0fffb11fc3fbfe01f5d25f87f7ec13fba4af0fffc827f7495e0fff905ffe82ac1fff30bfed15582ffe717fca3ab04ffcf2ff8475709fe9f5ff0ff63eadd0ff8bf8a1bef7e7c8cd9ff9447f41768ca2faad399f3f36a1d55cd2cbff25031ddf8ea79e5bd5bf30cf9fdabcedfc778657fa99bdd47353eb3ff55dccaf974fc57ce2edaebce9debf847f5fc1cfcc731ddf8ea79e5bd5bf30cc13ffcc3ff3ea61b5f3dafbc776b9e21f8877ff8dfc774e3abe795f76ecd3304fff00ffffb986e7cf5bcf2dead7986e01ffee17f1fd38daf9e57debb35cf10fcc33ffcef63baf1d5f3ca7bb7e619827ff887ff7d4c37be7a5e79efd63c43f00ffff0bf8fe9c657cf2befdd9a6708fee11ffef7319df8f7113814fde561b4916ade6ef0cc055f9953bdfa7ed498dd1c7521a231f531f03dccec45141b7d84a279b52e55c3d7f1b9f6b7e3ad94016663a2fafeb99be7ef40e43dd36bc446f68f847d8ffaf1feaa0ff561537cfabb5cd5b475fd33fc6fe918fcc3bf7fcedee11ffee15fc744f5fd73370ffee1dff6e005fffb3a3ed7fe76bc95e01ffee17fef03fff731517dffdccd837ff8b73d78c1ffbe8ecfb5bf1d6f25f8877ff8dffbc0ff7d4c54df3f77f3e01ffe6d0f5ef0bfafe373ed6fc75b09fee11ffef73ef07f1f13d5f7cfdd3cf8877fdb83d733f2ff6e27a3835487155d16eb612f888ff117c77bfbd8d1939d8ff2bcaacbd6f918f99e6c5f76cf948f3fcca85ea7a69a1fb5551fea9c7c7cb56f11a0d99892f2f36b18637e6ffdafbd3fbebebf7f9d5ea27bad00f267a1cec6c755b1515feaeef8fd51ac46e71edd5df8d739514f1ff09f8e297d7cc07f141bf505fff01fdd1b7f5eaa8f0ff86ff5f201fff02f72a29e3ee03f1d53faf880ff2836ea0bfee13fba37febc541f1ff0dfeae503fee15fe4443d7dc07f3aa6f4f101ff516cd417fcc37f746ffc79a93e3ee0bfd5cb07fcc3bfc8897afa80ff744ce9e303fea3d8a82ff887ffe8def8f3527d7cc07fab970ff8877f9113f5f47112ff6f7ffff77fff3192b30bab9af3c67ecc7f08d487c15fa46883b2395f4bf518c57befece365fd7c1f9dda7e2fa2bd531fa26d8b3f84d978759e6a8ddecbf6abd614bdab7d88c6a233d8c4fa541fb3f747ad3352759f67eeac3a73efe1e7a3738cf64fd58bd8abeeaa12fcc33ffcc33ffcc3bf9d8fd607fff7bdabdaf00ffff00fffaa37f887ff6d83ff284fd58cfacfd61909fef7757d7ea726fc6fe17aa25ee11ffee11ffeb70dfea33c5533ea3f5b6724f8dfd7f5f99d9af0bf85eb897a857ff8877ff8df36f88ff254cda8ff6c9d91e07f5fd7e7776ac2ffb6abf9fb1f604585b28bebe3b3063a17316bbcb319dd3554973eea67764d47e78ee6fab8eeba474cf7d256b58756ee42b69e6dd320461fe8a8a7aa4e273eeb49c55a65ffc0643d75fe6189fee1f3cf9ddc4cf00fff995f9407fff01ff505fff09fad67dbe01ffee1bf12fcc33ffcc33ffceb9eaa3af00ffff00fff593cfcc33ffcc77e511efcc37fd417fcc37fb69e6d837ff887ff4af00ffff0ffa4fcffc33ffcc347b5c1b317b6b3980a4c1f5b5de66a83aa1eab7e2b750e35ab93c558df2350cdd4897aec5ec099b54575abf195b51eddab958f77f521555ed947c8af41f5e3f729aaa1c655afaa46b4a66c1cfe759d2cc6fac23ffcc3ff7dccf0e8e4c07faf4ed423fcc37f1607fff0afdeb39a51dd6a1cfee11ffee17f46f0bfc9984cf00ffffe1dfee13f5a53360effba4e16637de11ffee1ff3e66787472e0bf5727ea11fee13f8b837ff857ef59cda86e350efff0ffeafcfffabf80e537a493d83d88d50fc26ccdd558a5d9fd3843b79aefefefcb976655577aa36b74f6fd84ff7bc13f7a66c1ff79b14af08f9e59f07f5eac12fca36716fc9f17ab04ffe89905ffe7c52ac13f7a66c1ff79b14af08f9e59f07f5eac12fca36716fc9f17ab04ffe89905ffe7c52a3d92fff76d41d5e2ec7c6723b2983137b3a1955fe5556dbeca5fe9d3d7f4b9473dbd4f34d7f59f897d069d756754acdf8bb3f7a5fac07da5e0ff6bf85ff1aed60bff6bb133fcaf9e5b247f3f1f7d26f00fffaf20f8bf46f00fffaf20f8bf46f00fffaf20f8bf46f00fffaf20f8bf46f00fffaf20f8bf46f00fffaf20f8bf46f00fffaf20f8bf46f07f1effef9d825113472fd678ceeacff6d6d9dc950b5c1deaf03cf2c731be86f28a2e77d45fe762747b567be7eb1cd151efcefe4579b37f61e9f7e24c68ed7df2e3994f169309fe5f837f7f4767fea183ff3cef4afe23efb3f89f8d51fdcd0afeefe7e01ffeb378f8ffccf3cff0af6bc0ff9c37fcc33ffcf7bcb33a591cfcc3ff6a9d91e79fe15fd780ff396ff8877ff8ef796775b238f887ffd53a23cf3fc3bfae01ff73def00ffff0dff3ceea6471f00fffab75469e7ffe91f97f571b19bddbdf08c69983a9808b7c543f59bcca8ddeedb89dcb605380653d4563c3c7fea772d4c5b53df8be6d9cf55575a25ad1bbad93adadfbe1e87e8cecbcef218bf1f52a55e0a977b596ea236c9fa3d8687d36de9e77e7ece01ffee13fefab3b07fff0bf053daa393f06fff09fc5c27fdc4f16af72a3773b0efff00ffff0ef7b85ffbde01ffe23bf48f07f3f06fff09fc5c27fdc4f16af72a3773b0efff00ffff0ef7b85ffbde01ffe23bf48f07f3f06fff09fc5c27fdc4f16af72a3773bfe08fed3ff17840af49946b34ba42edef01a319997aa195d8aacbebab0d105cb2eabefd5f65441ec3f326a0d9d438e8088e622651ff6eefedafa7e8f23a07dbebf231dd03e8a7fb8d4198e3566ff88a87105e2f855eb50f1d999f9f7ec83d3a9372bf8877ff8dfa4d7c853eff07f9f6f05ff9bf4847ff88feac03ffcc33ffcc3ff26dfe11ffe555ce401ff3dc13ffcc3ff26bd469e7a87fffb7c2bf8dfa427fcc37f5407fee11ffee11ffe37f90efff0afe2220ff8ef09feafe7ffedeffffeef3fac51d4b45f70f69e8d67973d8ab78b52b97edcc7673dabdcaa2fefdbc9f1ca7aaed61bf9290f55d3c6ab5a9db3acd697a9ba1759dfe33deadd8e47b5aa719b9ff5568d576b50fbae3c67ef58f72cab8f65d57bf49e8dc3ffbebeefb95a6fe4a73c544d1baf6ac13ffc777a8fdeb371f8dfd7f73d57eb8dfc9487aa69e3552df887ff4eefd17b360efffbfabee76abd919ff250356dbcaa05fff0dfe93d7acfc6e17f5fdff75cad37f2531eaaa68d57b5e01ffe3bbd47efd938fcefebfb9eabf5467eca43d5b4f1aa16fcc37fa7f7e83d1b87ff7d7ddf73b5dec84f79a89a365ed5827ff8eff41ebd67e3f0bfafef7baed61bf9290f55d3c6ab5af00fff9ddea3f76c1cfef7f57dcfd57a233fe5a16ada78550bfee1bfd37bf49e8dc3fffdfcbb6d7698f95fffec65f36dae6fd28edd7e7d5de563c77dee166c80df50f59c016dc7ab5c9fe77394fce1dbf5f875f831eb11f5a8f6dd5f085bd75f4275feea92aa33576b577b6873fdd956ebb1fdab7a0a3a554fdd7975b7d49dc8f646ed6516a3d61a293a2375663e27aa01fff00ffff00ffff0eff36c3cfcc3bf12fcc3bfeac5f7aa7a82ff4dae3512fcc3fff0887a847ff85739510df8877ff8877ff8877f9f67e3e11ffe95e01ffe552fbe57d513fc6f72ad91e01ffe8747d423fcc3bfca896ac0ffd7f1fff65ffecb7ff9f08712bdfb5f3bef9ba82ed24c5c6721caa73b3f9eed1a94879a5fe9377aafe267bcfd9c55767699bfca5f510641e77e457e2a66e6dcaa3d5c3dabea1e76fc67d55977c633fcef3dd4fc4abfd17b153fe3ede7ace03fee0bfee13ff250f32bfd46ef55fc8cb79fb382ffb82ff887ffc843cdaff41bbd57f133de7ece0afee3bee01ffe230f35bfd26ff45ec5cf78fb392bf88ffb827ff88f3cd4fc4abfd17b153fe3ede7ace03fee0bfee13ff250f32bfd46ef55fc8cb79fb382ffb82ff887ffc843cdaff41bbd57f133de7ece0afee3bee01ffe230f35bfd26ff45ec5cf78fb392bf88ffb827ff88f3cd4fc4abff6fda77ffdaffff52fdb17c97f183a71dd9c59a9c3c89eb3b1d9ba8fcc3dba9757ecc1159a5da73fff2bceb5dbc791f9a3fe8f14fcc3ff5582ff6bf2cf14fcc3ff5582ff6bf2cf14fcc3ff5582ff6bf2cf14fcc3ff5582ff6bf2cf14fcc3ff5582ff6bf2cf14fcc3ff5582ff6bf2cf14fcc3ff5582ff6bf2cf14fcc3ff5582ff6bf2cf14fc6fbffd2f606d0f90fd2b32f47c1a17d05ec459a933f67ecaff484dafc86be6fe55fd3ca2df2bbcceac352bf87f6ec17fedd19d9f11fca36710fcd71eddf919c13f7a06c17fedd19d9f11fca36710fcd71eddf919c13f7a06c17fedd19d9f11fca36710fcd71eddf919c13f7a06c17fedd19d9f11fca36710fcd71eddf919c1ff8fadf768a23aa8e892abb90a6c15ff0c5ae9e3cadebbde59dc6dcecefb333b52430116d5b3faf6eddb365347cd55fddbfb97ddc34ebfb37be43dad56d7ae628eec73e6db9d877ff8efd6b382ff3cbf53237beed4ca7cbbf3f00fffdd7a56f09fe7776a64cf9d5a996f771efee1bf5bcf0afef3fc4e8decb9532bf3edcec33ffc77eb59c17f9edfa9913d776a65bedd79f887ff6e3d2bf8cff33b35b2e74eadccb73b0ffff0dfad6705ff797ea746f6dca995f976e7e11ffebbf5ace03fcfefd4c89e3bb532dfee3cfcc37fb79e15fce7f99d1ad973a756e6db9d87fff99cf768233d4c3e4ec136c63a7fe966fd3c1c23df5edcec72ad5c966c7ed4b57d6439b39bbe0275f5d1b0ca2efb6daefb9788d91aa3bd893eccea433f9ea37ed43dcce2a2fbe2dffdfdb27d647fc11b7947756c6fd13f4e555e25750ed107afeac1e76771f00fff6a1cfee11ffeb7a97ad158671efee15f79c1fffa3cfcef6bc23ffc575ef0df13fcc3bf8f857ff89fcd19e3f00ffff0dfabd59987ff7d4df887ffca0bfe7b827ff8f7b1f00fffb339631cfee11ffe7bb53af38fe4ff3ddb487f3915ecaa117fb17d9caa31fc6d9de8c2740e4cbdab3e54ec00c6af3df2549735db0b1b6f6bd877af08b42da8afde554fde57ad6fd4e89c7d049fcf8f7af6e7e063320fe5e563fdbda93e16d97d8b6a55f7d7cefb7da978ebac2ffa07b8fb91877ff857f5e11ffe3b7700fee1bfca877ff8b7b1f00fffbe3efcc3bf5a03fceb5c3feedfe11ffed5dabce01ffe557df887ffce1d807ff8aff2e11ffe6d2cfcc3bfaf0ffff0afd600ff3ad78ffb77f8877fb5362ff87f1cffef3ed12fd437e20fc2cfdb385f505ddc487e51d1a5aa0e4cf958d9bca846941ff563f7c9ee45141fc91f6454cbc7abfdcf2eb4f2ecc4671fb0ce5ad525571fa54afedef933b0fdf8b38e3e58110b596dfbabbc7c0fde43f515e5571f6675e7baff08fa35c3bfce877ff8877ff8f7f3f00fffca0bfee13ff3f382ff9ee07f5f07fe631f2bf8877ff8df76b1f05ff705fff00ffff0efe7e11ffe9517fcc37fe6e705ff3dc1ffbe0efcc73e56f00ffff0bfed62e1bfeeeb14feffeeeffeae75d3ad617491a2cb5c5d06bf28554be5557d5a4fb586cc2bdab4eec721eb43c58ef96acd337b929d4fb5f6ee19fb7d8ae63af5aa3d8fee6074be9987eac1f6ea9fb76d6bd58934139be5ced4eeb232d303fcc33ffcc3ff18b382ff5e1f2a76ccc33ffc67b9f00ffff00ffff05ff7693dd51ae01ffeedbcedd53f6f1bfcc33ffcc33ffcc33ffcc33ffcc3ffb6d4878a1df3f00fff592efcc33ffcc33ffcd77d5a4fb58667e0ff7d04fbcd1cef76dc9ada06c77f6a53c75875e95583d66bfc171dbecdb135ada7ada3d6e27dfd85b1b991bc9fdf1f159f79795fdf93df93a87ea5ecc2fa31abea02db6775a7ac87da6fd543a50c1a753ffd1d5575ec5de9acd98e456c759fbbebce7a56fff97ba37a84ff7daff0bfed62abded5aff7807ff8f7fddbfee01ffe3b82ffcf3cf8877ff8877ff887ff4cf00fffaa47f8dff70affdb2eb6ea5dfd7a0ff8877fdfbfed0ffee1bf23f8ffcc837ff8877ff8877ff8cf04fff0af7a84ff7daf67f2ffd39ffff99fffa2c05697382b1ac574e67c5c75c1a383887a54974b6d6e479d4bd05d4356e3486ef6b1f7a0441e9d755a4fff1ccd7bbfea7e441fe56c5ef99cad6a0f57f23cbc51cc6a6e541ffee1dfc7c17f2ef8dfcfc1ff3e46cd65b9956f2717fef73e670bfef773f0bf8f5173596ee5dbc985ffbdcfd982fffd1cfcef63d45c965bf97672e17fef73b6e07f3f07fffb183597e556be9d5cf8dffb9c2df8dfcfc1ff3e46cd65b9956f2717fef73e670bfef773f0bf8f5173596ee5dbc985ffbdcfd982fffd1cfcef63d45c965bf97672e17fef73b6e07f3f07fffb1835677f7ffd03acd9e4aca882a4bbc0eea59ef15b953a34ffab2e6975d1567b887cabfdf51e9d0bdb9defa8fbf1eec6abb10edcd179cec628efacf6cc5c55ab53bbf21c8c1ef97854de3616fee13f9b83ff3c0efe755cd76f55f00ffff0bf9feb78db58f887ff6c0efef338f8d7715dbf55c13ffcc3ff7eaee36d63e11ffeb339f8cfe3e05fc775fd5605fff00ffffbb98eb78d857ff8cfe6e03f8f837f1dd7f55b15fcc33ffcefe73ade3616fe8b7dfbbbbffbbbf93fe3bb58f6209fc1e7117aa55eafd4a3f721abf7aa67f2ea7709fe7f5cc1ff71c1ffb93e8f10fcff26f83f2ef83fd7e71182ffdf04ffc705ffe7fa3c42f0ff9be0ffb8e0ff5c9f4708fe7f13fc1f17fc9febf308c1ff6f82ffe382ff737d1e21f8ff4df07f5cf07faecf2304ffbf09fe8f0bfecff5b952ef7ec0fe55d778b66351ec4c5ca5d94dbbf9fbbeede6577dfa7c1fd75d67e597e5666baecee1481f33679cd59f9d8b7ced5f16aeacb73337f64501ea7bc97cd51eae32b0ba56f56bd7b47277a31a59ec4c5c25f8eff55109fee37af09ffb2aef2c7626ae12fcf7faa804ff713df8cf7d9577163b135709fe7b7d5482ffb81efce7beca3b8b9d89ab04ffbd3e2ac17f5c0ffe735fe59dc5cec45582ff5e1f95e03fae07ffb9aff2ce6267e22ac17faf8f4af01fd783ffdc577967b1337195e0bfd74725f88febc17feeabbcb3d899b84af0dfeba312fcc7f5e03ff755de59ec4c5ca51f89fff47f014b5dd099f9ae22103a6333be9df5dc74c69aa27ab335b27875c9c607ac8aaf6acdee5d47bedfabea9ca1ac8fa8f7ca43adbf1b9fc55d25f83f26f8cffb857ff88f7ce0bf8e87ffeb04fff07f54f09ff70bfff01ff9c07f1d0fffd709fee1ffa8e03fef17fee13ff281ff3a1efeaf13fcc3ff51c17fde2ffcc37fe403ff753cfc5f27f887ffa3eaf0ff6e07fd737601edbccdb1317e7ca68edfb8a8876c7c40a23c95fc7afcef78aed63b9ea37a1154de238a55fdde9ea38f427416be5fef6de7fd5e5667a2e6d5c7a73a67e59fad479d9d9fabbc475f51bcdfe72c4ee57594ddef6ccfd41e44f3ca03fee17f78c33ffc473dd879f8877f3f06fff00ffff09ff56bc7a21ce50ffff09fed8ff5e8acc7cfc33ffcc33ffcc33ffc2bc13ffcabbaf00fff3e4ee57504fff01fc5aa7ee11ffec73bfc7f7a74d6e3e7e11ffee11ffe1fc1ffbbbf480ab4ea722ae0fdb8f71c1e0a9ce812451f161b6fd7f21100ab9eeda6a98f4f67adf6a3a2d61e1d8aad1bd55135231fbf27de539db99d57e3fe6ea87db6b5fd1eaa73547722dab7a84fefe33f8eea4c2288bcec7a541d15177d74a2fbefe3ecbcf2e87c14d579f93d549ef00fffde6b8cc33ffcc33ffcc3ff76e7ebe3e01ffee11ffed51afc5ae11ffeed38fcc3bf9d571ef0bfed7a83ff7b1ff8877ff8877ff8877ff8dfe0df79aa33b7f36a1cfee11ffee11ffeb73b5f1f07fff03fc3ffdbdffeeddf7e58f3e8f015b0f65dc1ae0e4cc5aa3cd58baaa72ec3470157b79fe88315e575f6adaaebd7fd517cb832afc853f9661fb1a89fa84e3417e5cf9c41f4c188fe71a8ceadb3df51df519fd19cf2eadee5a8bfea0e75b9f479be57f8877f55bbdb07fc6fd20bfef3dee11ffe6d0ff00ffff00fff6a0cfef76b82ffb846150ffff03f04fff03f9ea33cf8877ff8cfebc03ffcafe6c13ffcc33ffcc3ff26e3d59ae03fae51c5c33ffc0fc13ffc8fe7286f85fff7a8a8972de60bdb066fbfbe39df4c24eb11d55373a3a6f751be51be928febe4a9bdf13da871e5ab62a20fb2df03d583ff8d642f4b049895df577f160a12dbafefdfaf57f51dada5ea3bfae0d83a1948ea9ea9f38aeeb2cfad7acbfca3fdb67be3ef45f5e181ff4dd6857ff8576bb3bff00ffff00ffff00ffffe1dfee1dff7e905fff00ffff0eff3e01ffe953ffcc3bfea09fee15ff500fff00ffff00ffff7b1f09ff7e57b837ff8b7827ff887ff7d8e7d0fefcaed7f012b3accea80ece5cd20f5453b5297aecaefc4cce4753e3caabf993ed4de1c59c75007b4ec63b422ff41ed7ebca35e556e27a69b5badc36ad5cbc7671f563f76c51965bd8de721f887ff95faf0bf85fdc07fbf279b03ff7d2feb07ff5b19ab72bde07f93f3f00fffbe3efc6f613ff0dfefc9e6c07fdfcbfac1ff56c6aa5c2ff8dfe43cfcc3bfaf0fff5bd80ffcf77bb239f0dff7b27ef0bf95b12ad70bfe37390ffff0efebc3ff16f603fffd9e6c0efcf7bdac1ffc6f65accaf582ff4dcec3ff31fe7ffaf33ffff35face14deab24585d5fc99ba79db43be5247fcd51e65e3e3395b57961bc5471fa795b5456bc86255bdce5e74fba980e87864e367fc0334eeecaaaebee7555df83f9e0bfff0bf2af8bfaf05fffa5dc5c37fed918dc33ffc9f910bfff0bf2af8bfaf05fffa5dc5c37fed918dc33ffc9f910bfff0bf2af8bfaf05fffa5dc5c37fed918dc33ffc9f910bfff0bf2af8bfaf05fffa5dc5c37fed918dc33ffc9f91dbe5ffd7ff05aced041db9b8955fb4f967d78cfa183dbcb256f6aaf3c1eb7aaad82cffe887f0acf37ac41dfb8a5a670bfe9f5bf0bf26f8ef09fe9f5bf0bf26f8ef09fe9f5bf0bf26f8ef09fe9f5bf0bf26f8ef09fe9f5bf0bf26f8ef09fe9f5bf0bf26f8ef09fe9f5bf0bf26f8ef09fe9f5bf0bf26f8ef09fed7f4be2d483563c7aaf96e8d91136dc2199b52797cfbf6eda9a0b0fb52c565efdd5aead7cf777c66efc4ca7d19ba9dd959aaeef5ac871a57fbdb3de7a33d9ce505ff8f11fcd7f1f01fd7baca0bfe1f23f8afe3e13fae759517fc3f46f05fc7c37f5ceb2a2ff87f8ce0bf8e87ffb8d6555ef0ff18c17f1d0fff71adabbce0ff3182ff3a1efee35a5779c1ff6304ff753cfcc7b5aef282ffc708feeb78f88f6b5de505ff6b7a5fb960eaafd1d4bcf5ab727cbccfb9cdadfcc55b7419fce146799dbea3dc4e4cd59fd7ca3e1ed5a859d58ed6149d5be783b3722757ce2cd2cc3f3291fc3df6e3bedf71d7cf5c47d403fcc37f25f83f1607fff01fc5c0ffe733fc7fc6c03ffc2bc17fdea37f3e2af8cfbdb279f8af63e0fff319fe7f13fcef73e07f6bf703fff0dfed01fee1bf12fc1f8b837ff88f62e0fff319fe3f63e01ffe95e03fefd13f1f15fce75ed93cfcd731ef76b3d44550ef1138d96156316a83b3cb5a2dd2f795c1add6a32e6cf661b471ead046ae8db16bb635a2fce85ded63f6d1f3bd446b9a85d3afc9f754edb7da737bdeaa77d5db0c00558c3d9f950f80f552636a7cf663559d7326f8877ff88f63e07fff0eff714ddba3ea07fee1df8ec1ff76571ffee1dfc6c07fed07ff7bc13ffc6782fffb5af07f9f07fff00ffff0efe3e03f16fce75e6a0cfeb7bbfaf00fff3606fe6b3ff8df0bfee13f13fcdfd782fffbbc47f1ff3e26a3cbaf36c1ca82e337c7c6abcb1b815a2982c1f7ea7bb37d45c08f793f6e7b8b3e0016d668de5ff8e8e3e3f76116fe08285fcbf7adf2d5fdc8f67e78fae7ecfef88f6b7536fe3cabf3f37d67ffc845f757e57d241f14b54fcab3da4beba7e2a27be7eba9bd1de3f00ffff00fffca5779d931f8877ff8877ff8877febe99fe11ffe7d2efcc33ffcc3bfea2312fcd782ff7d2efcc3bf9a53b97e1cfee15fe5c13ffcc33ffc47b5e07f935e6a0cfeb75d2ddb13fcc3bf8a83ffcf71dfd7b3f3ffb3da7cdfac5aa42f6a7fedb892f78e36befae8a84df51b195de6e8a0a30f61751155bcda43b56f6a3cfb08d8b8a80fdb8fda6f1febd790ed5db66e1f13ad338228f28c00f6b1fe2efafabe86f58aee6ec487f7571f40d57fd44bd66bd443970fef13d5837ff8877ff8877ff887fffb3a953ffcc33ffcc3bf12fcef3de01ffee15ffbaa7c150bfff0affab0fdc03ffc7b1ff8877fff6c73e01ffe7ddd316f7fe17f0be3e11ffe55fff00ffff00fff6abdf09ff71fcdbf2affef5bb1289ba42eac9ff38079ef4cfe42651f21bff1aaa68258d5526ba940500712813bfcfce6fb5ad6475d7cd557d6a33fcb0846fbaec0ced615cd571fbfea2e44fbecf7cdc74635b30fa38ff11f1235ef3dd57dcc408dce429d933dfbec0319edb35a4324f8877ff8877feb07ff5b58df8ec1ffbd0ffcc3bff7ce04fff01fd5f075e01ffe6d1faa26fcc3ff10fc7f7a65827ff8877ff8b77ef0bf85f5ed18fcdffbc03ffc7bef4cf00fff510d5f07fee1dff6a16ac23ffc0fc1ffa75726f87f2cff6f7ffbb77ffbe19b8d0ea082d80350416bf3a2857adf4ecde812a9fe555db951496e3417f5d5e9afd377b4d7d15a865f770dea625731551fd158b5bf43fe3c66f2ba7b90dde1eafe549e1dafec1f8f8ad15965670cfff06f7b50f9f09f0bfeb7d0cbe6457d79df4e4df8d76b197edd35c03ffcfb77f8cf73d438fcc33ffcc33ffcc763f07f9f6305fff00fffdb5d7df887ffa826fcebb50cbfee1ae01ffefd3bfce7396a1cfee11ffee11ffee331f8bfcfb182ffef97ff9ffef8c73ffeb24dea6630fe1b8b8b1a1971f680559ebf7436bfdb8fdf745fb7eb5fc5dbf9686d997775586a4ef5a0e2a33ed4e5ed489d8d3d7fbff6ca3f3be795deb2b90e6c55bced2bda07e5a77af1f732f3f29e516e56bf3a97ce1e29d9f3bf09fee13f8a557edd3a9ddeb239f8877ff887ff4e3efcf765cfff26f887ff2856f975eb747acbe6e01ffee11ffe3bf9f0df973dff9be01ffea358e5d7add3e92d9b837ff8877ff8efe4c37f5ff6fc6f827ff88f62955fb74ea7b76c0efee11ffee1bf930fff7dd9f3bf09fed7f97ffbcffff93fcf9fc0a2fc26750e3f3ad4958b739656ea7f75cf67c9c373659dce07f48c3ecebc6367e4551eb3ec3ccbdd837ff89fa903ff756cc713fecf15fcc3ff8cd76c1efceb9c6cec91827ff89ff19acd837f9d938d3d52f00fff335eb379f0af73b2b1470afee17fc66b360ffe754e36f648c13ffccf78cde6c1bfcec9c61e29f887ff19afd93cf8d739d9d82305fff03fe3359b07ff3a271b7ba4cee47fe90fb0bcd938e4d5fc1f45dfcbba67cf7b46cfb0478ff8f8abfc957dbdf22c22c1ff9ae0bf16fcc3fff72af8af05fff0ffbd0afe6bc13ffc7faf82ff5af00fffdfabe0bf16fcc3fff72af8af05fff0ffbd0afe6bc13ffc7faf82ff5af00fffdfabd4badfb705a9bf48eb36a0f29f45575fe40ed4ea398bfb0a5d517feccdccddb8ea1e45be76ddabb587c7b76fdf765e2bfbfa157701fed704ffb1e01ffebf5af0df13fcc3ff4a0ff00fff91e01ffebf5af0df13fcc3ff4a0ff00fff91e01ffebf5af0df13fcc3ff4a0ff00fff91e01ffebf5af0df13fcc3ff4a0ff0bf5ff7d21f600db368233d4c115cd9bb9aab0e2ef2ebc2ade63bebacf267fb3ca3ee8ab23def9e47778f23883be7ec73945f563b8bcf7cec4733cba9eef14ddd7f6c676acde41d15fcc33ffceb1cf887ff9931f887ffaee03fefa9d34737bfaa0bfff0af72e01ffe67c6e01ffebb82ffbca74e1fddfcaa2efcc3bfca817ff89f19837ff8ef0afef39e3a7d74f3abbaf00fff2a07fee17f660cfee1bf2bf8cf7beaf4d1cdafea9ec1ff7b34d9d9007f71fce5b2bfaa110f90ba94d633baa8d106f9fabe8fce868dba59ffb6c76c8f562018fb9e5d22df87cf8f9ebbebb7bf76bc5aaff2cceec438a3ea8314e555a0d898a84f7f5e9d7f1cd5de4473deab33eed7302bbfae2ad6feda71f8d7fef0bf8f8f3ce11ffe553efc6bc1ff96fad93cf88f73e11ffee11ffe3bb1f6d78ec3bff687ff7d7ce409fff0aff2e15f0bfeb7d4cfe6c17f9c0bfff00ffff0df89b5bf761cfeb53ffcefe3234ff8877f950fff5af0bfa57e360ffee35cf87f4efedffda435c91a1c719dd811afe286477698be8e7fb61bac64e71484fe72d98f5e068592df3ffbacfaacf6d8fa463d47fd7b8fa89ead117d3caa8beb6bfbb3cffaf5f9de47c547fbaaee64a7be3d17d5bb7f563eaa76b62fd99df679d15df58a6a543dc23ffcc33ffcc33ffcfb9a3eaf73ff144351bf3edffb44bc8e5ff8dfd29ea279f8877fdf3bfcc37fb40615d7891df1593ff0bfedc6e01ffee11ffeed33fcefe3e07f4b7b8ae6e11ffe7deff00fffd11a545c2776c467fdc0ffb61b837ff8877ff8b7cff0bf8f83ff2ded299a87ffafe7ffdd4eda670b6335360a478b56f29b60c7b30b647394bfda340f8edfa4b18ee8c367374f5d20f5ac7c549cffe8a9c353b5fddeabf72a27eb3ffa50f8b5a8f828c6eea55ab7bf5bdec702179da9f7f5fda8da36df7bf97b1cfd23a5e2547ddf4bb4d6e1a972fdda7d6cb70f9b0bfff00ffff00ffff01f3dc33ffcbfc1ffae9e7a563e2a0efe3ff3e11ffee11ffee1ff73bd2af70dfe65aeca1f71f00fff760fe0ff3e17fee11ffee11ffee13f7a867ff87f83ff5d3df5ac7c541cfc7fe6c33ffcff68fcff6c076736515d026feedf3df4bef9ecc0140c592deba9ea653d5607e82fbb07d7c7f83e22681508d585f4fb12edb78ab3fed107c9af51f967fdabf9c8c3ef4fb636b5e77e2edb0fe59f7d84bd57b5ae4a1fc13f70d17aaa7d53fd461e598d6accd6877ff8b7827ff8877ff8576bf3359507fcc37f24f8dff7eafdd55abc17fcc33ffc6f698ff00ffff00fffd118fcdffb67fdab79f8877f3f07fff00ffff01f09fef7bd7a7fb516ef05fff00fff5bda23fcc33ffcc37f347626ffef9d4b936d8c8db117d93731de15e8918fcaf597d5c7f9fc687dd985b61efe03e8a53e3a51fde823177d30ac9ffacfc6a83ea3de1584fee2a875291fbf9e48eab2aa75dabe7d0f515defefefa2af3dc6ab0f5654c3df73bb069b6bf75ffd63d6b9e7598eea4fbdfbbdf1f52ac13ffcc33ffc5731f00fff91e01ffee11ffee11ffeb7a267f8df767eea3f1b03fff00ffff0ef7b837ff8877fdd2ffcc33ffcc3bfef03fee11ffe63df2806fee13f12fcc33ffcefd7fad31ffff8c75fba172102429947631df983ac2e9dea25dbdcea375a5b773dfe10ba527dcec646b5cf3a9f6c2fd4def9b9d53b617d327f5f2b823eeb537d4cbcef59fdab7f1c7c3d959bada1f251f3fe19fee37e3afdc3bf9e837ff8eff4ab7ea358f8877ff8cf6bc27fee0dff3dc13ffc8ff9c8231bebf6ab7ea358f8877ff8cf6bc27fee0dff3dc13ffc8ff9c8231bebf6ab7ea358f8877ff8cf6bc27fee0dff3dc13ffc8ff9c8231bebf6ab7ea358f8877ff8cf6bc27feefd55fcbfdffe8f2d787b1eef7ef3fdfc98cbc6557359e3b6eef08c2e67f4eee3ed3abcfc5a95544cb6c91518d907d30350f9f8cb919d838d9d01d8d71a75aca78ff5ef763d9dcba9fa8edebdfcc737ba0f516e3537bb6ff639fbf08f5f753ee37d85319513f102ff5b2b06fee1bf23f8cf7b5373f00ffff00ffff00fff915f26f8877f2bf8bf7f867ff887ff6d572b12fcc3bf9d837ff8877ff8ef08fef3ded41cfcc33ffcc33ffcc37fe49709fee1dfea99f9bffb5fc0eac034ab59d8ce8c7d06752f9cff4875f2af52f6f1eb8c571f99ec8276fed1511ff34e5e57d9c77bc5af9b17d553e3762cda836e8ff07f9de07f9f03ff79ddce38fcbf86e07f9f03ff79ddce38fcbf86e07f9f03ff79ddce38fcbf86e07f9f03ff79ddce38fcbf86e07f9f03ff79ddce38fcbf86e07f9f03ff79ddce38fcbf86e07f9f03ff79ddce38fcbf86e07f9f03ff79ddce38fcbf86aaf37efb4fffe93fe57fae855aca6045087ddf827f847e5cc13f423faee01fa11f57f08fd08f2bf847e8c715fc23f4e30afe11fa7105ff08fdb85ae5ff7d43a7486dfe991fe4477ddc5ff11f9157eb997fa8bf3fc1ffd709fed1570bfebf4ef08fbe5af0ff75827ff4d582ffaf13fca3af16fc7f9de01f7db5e0ffeb04ffe8ab05ff5f27f8475f2df8ff3ac13ffa6aadf2ffeb1f60cd24dbf1acc0983be3b2655eb7b14eff554ce4d395f2f2fff363761dd19aa23e23afa3f27dabffc9b44eaf9dfd8bee4ee45bddaf997a559caa79a48fccd3fe4f3176183aaaea8ec13ffc47756c0cfce7b95d4ff89f13fcc3bff29aa957c5c1ff568ef971f8cfbde03fafa59eb37a3e66a65e1507ff5b39e6c7e13ff782ffbc967aceeaf998997a551cfc6fe5981f87ffdc0bfef35aea39abe76366ea5571f0bf95637e1cfe732ff8cf6ba9e7ac9e8f99a957c5c1ff568ef971f8cfbde03fafa59eb37a3e66a65e1507ff5b39e6c7e13ff782ffbc967aceeaf998997a551cfc6fe5981f87ffdcabc3ffaf7f8055fdff98b473eaff6fa92aa28aaf2ed0fffffdb47dfdfaff47b1f1ffbb52adc7ffff3aed6c58b75fef63fbb7b554ef992a308e5c7cbfbfe3d9f66af742bd77fbf77befcfb4f288f26d8decd92a3a0fbfee2c57f1933dfb7d501feceedda8ce5ab1e0dfe11ffee11ffed518fcc3ff98877f9d0bfff7bdc0ffbd0ffc6f77def00fff5dc1ffbd57360efff0af7aacdee11ffee11ffed518fcc3ff98877f9d0bfff7bdc0ffbd0ffc6f77def00fff5dc1ffbd57360efff0af7aacdee1ff6bf87f57493e618c45733ea68a53b17e2c5b943d904eaeff6054717e2c3a1cf531f2fd46fbe87dfc5cd44bf4ae3e20fee2f89e6ddfd15afc07c2ef45f6918a00f66bf697738c47fbe6818ceafb7cf50f8cea53ed9bbf6bd9072bab61e3d59d51cca8338c7a5731de2beadfc7c03ffcc33ffcfb3af00fffaa2efcc37fe691c5fa312ff8877fef01fff00ffff7f5e07fdbf5e6fde01ffeb3383f06fff00ffff0efc7e11ffea3fad9598d78f8df7b47827ff8f71ef00ffff07f5f0ffeb75d6fde0ffee13f8bf36357f2ff6ecdac9441b6a12327ba4491a71d1f7391fca2a30d8beada1e558e1ff7b1d107255a47d48bf5893e68be8edab30802ebebf7dc7f84b28fa3ef5b7dc0aa3b3172d57b06b9af55d55031eac33e9ed51946e7a03cd55e643dab7d57fda98f4ae56bb9cbced9e677fe41b1ebf5cf4af00fff4af00fff91a71d1f7391e01ffe67eac03ffc7b7ff8877ff8877f9503fff01f79794ff8df0bfee15f09fee13ff2b4e3632e12fcc3ff4c1df8877fef0ffff00ffff0af72e01ffe232fef09ff7bc1ff3cff3f8fcd5020aa838f20b173599c5f8c6faef28efab3630aecc8c3d7f573d17ad45a6ced0ce068acca5760661f2b059cba64511fd1d9457da938ff9189f655adb1038aba7bd1f944ebf65eeabdf2ab7af31fda284fdd337557a3fed45ea83c1b0bfff0af7ce11ffe236ff8877f9fa7e2e01ffee17fdbc5aa31f8877ff8877ff8877ff887ff280efee13fea0ffee11ffee17fdbe0dfef13fcc3bfadadd601fff00ffff0efdfe11ffe23af23fcbffbc9f19fdf88e82055313b1e5d40fbeba151973152b431d11aa20f4ae7c3373cd4e1fb79bf8f56ea23e8f7c57a787fffab80f33d561f01bbfe0866d57fd69b5f8f1af3b1eaded8bdf4aa4052f1912268abbb5b81abf6dbaf59ad31825dc5f9fad93dce18807ff8877ff8b7f5d5daed3cfcc3bfeacdaf478dc13ffcc33ffcdbfee17fdf831d83fffd9ae01ffee11ffee11ffee11ffee11ffee17f0be37c1fd61bfe758de101fff06ffb857ff8f7f3f0bfedd6a3c6e01ffe7f64fe7ffae31ffff8cb2636c49a2965311e9eaed40556cf2acf3e773f001defac6695b7b27ee51f7d6cabb3e9d4531f882836caefc667f39157f481efc47755ad43dde5cedef9733b634faab577ce29da53ef01ff79aff00ffff00fff590cfcf7fce01ffee11ffeb338f887ff28cf3ec33ffccf08fee17f56f00ffff00fff550cfcf7fce01ffee11ffeb338f887ff28cf3ec33ffccf08fee17f5667f1ff3e02ec6f07aae8525a8d18b560f581515ed166598f6c91f683a5fa5287171da25234a7fa57eb567be0cfc0c21bf5157da43365906467a362fd9ef99ea3bbe16b653ebe9fea5cb2bd521fc7cccfd7abf6ccee6bf531cad6149d43d46b7457220ff8877ff8877ff887ffac7ff8877ff8df0bfee1dfd780fffb18f8dffbdb771b07fff01f79dabaf00ffff00fff599ff00fff99e01ffee11ffee11ffee11ffeb3fee11ffee17fafc3fcffcddffccd47665e41eac7d507401d78e491d5b25e8f945f93dd93ea023c42597f3ee611fd9e5163d5e3ccf555f7feabcefe5175e1ffb336fcaff5f3680ff83fbf0efcc3ff6a3f8ff680fff3ebc03ffcaff6f3680ff83fbf0efcc3ff6a3f8ff680fff3ebc03ffcaff6f3680ff83fbf0efcc3ff6a3f8ff680fff3ebc03ffcaff6f3680ff83fbf0efcc3ff6a3f8ff680fff3ebc0ff75fcfff4677ff667bf6c27296a466dfa19be6768a5972375a2fc6abe9abbc95fe499dc5752672d57adf719fec18d54ddad47d487ff3c0ffe8f0bfeb5e07f5ef0ff7a827f2df89f17fcbf9ee05f0bfee705ffaf27f8d782ff79c1ffeb09feb5e07f5ef0ff7a827f2df89f17fcbf9ee05f0bfee705ffaf27f8d782ff797d35ffe9ff02d6b3e9592fff6c5fcf0cf1a334fe7af0d9a5ce8af3fb1ac1fff723f847b382ffef47f08f6605ffdf8fe01fcd0afebf1fc13f9a15fc7f3f827f342bf8ff7e04ff6856f0fffd08fed1ace0fffb11fca3597d8ffcbf8fc11983afda84951e7dce59e05bff4e5f36c63fcff613adcdcf7fb53a7fcd7b64efbaf967c71fd9df33cfe6abbce01ffe3b82ff9edfab79c13ffc7704ff3dbf57f3827ff8ef08fe7b7eafe605fff0df11fcf7fc5ecd0bfee1bf23f8eff9bd9a17fcc37f47f0dff37b352ff887ff8ee0bfe7f76a5ef00fff1dc17fcfefd5bce0ff3cfe7fff032cdb5475e9b205640d5d71709167f63fc7360b959a1b3edeafd38fdfcb9903b417285b63e67144eae265e75dd5acee9d8d51f0cc7cc86dfc8cb2755675fd7c75de336734bbf66c1df00fff1dc1bf9eab726d7df83f4ff09fd7521e4704ff7aaecab5f5e1ff3cc17f5e4b791c11fcebb92ad7d687fff304ff792de57144f0afe7aa5c5b1ffecf13fce7b594c711c1bf9eab726d7df83f4ff09fd7521e4704ff7aaecab5f5e1ff3cc17f5e4b791c11fcebb92ad7d687fff304ff792de57144f0afe7aa5c5b1ffecfd3abf3ff5e05fbb1aad8ca058fe4fdd505520beb5c161f9bc5559733cbf5fb15ad29eb6dd4e8c0ae6a5a8f28277af7e3eae39f7d08a29a2a5e5d66f50f8ff252fba3f643d5559ed51debe42b40a37855cbcf45caf625f398f978c1ff7d9e15fcc37f5417fed705fff00ffff0efdfe17f4bebc1ff7d3efcc37fe40dfff0af3ce15f8fc13ffc7b2ff8dfc2baf0bf2ef8877ff8877fff0eff5b5a0ffeeff3e11ffe236ff8877fe509ff7a0cfecfe55ffe019637f1cd641f8dd1902a5e5d142f1fef37220225bbc47ec3ed7abca7ead97f78aafeabcb6c9f556fca2ffaa8f87ad19e8f39b50ed5737449abf71955173bf2ae62b23d8beafb9c6c6f7cee881bbffe8ced7c25757e0a68ff31f179fe3e75fe5188eac13ffcdbbcec7d46f0af7b827ffd0efff0df89817ff887ffbde03fae0fff713df8877f9b97bdcf08fe754ff0afdfe11ffe3b31f00ffff0bf17fcc7f5e13fae07fff06ff3b2f719c1bfee09fef53bfcc37f2706fee1ff99f97f8f9a1cffa982d9058a3e0ed19c9ff763fe592d4cf5eee3954ff66151e044bd441f33bf36dfb7fff0f9dea2fdb21f08ebed9fab0be281aace2193024e8d8fbeec6f3496411bf5acd6606b44f72fba4ff6ccd43d53f3eaa3a0fab71e7e4ce5a8f5d875657cfaf3e83004fff09fe5ab7e7d1efcc37f7617fcbc1ff3cfa30ff8877f5f47fdfabaf00fff3e0ffee11ffee1dfc6c3ffb67bce04fff06f73b3da596f7ecc3f8f3ee01ffe7d1df5ebebc23ffcfb3cf8877ff8877f1b0fffdbee3913fcc3bfcdcd6a67bdf931ff3cfa807ff8f775d4afaf0bfff0eff3be92ffb7bffeebbffe888a57876a8d37b720ff1c5d0af5e119792ab65343c54635aab5aaf1ea12aa750e55fbe07bcefaf16b50f1591fd17be5d55d7fb4ee0c90ea79a566d7378bcbfa8da0ec7c10a37b1ad5af38cceaaaf3f5827ff8877ff887ff7d5df8dfca1a593d351ed5837ff88fd657d5807ff85ff1857ff8877ff857cf59bff00fff99e01ffea33ee11ffee11ffead6f561ffee11ffee76ac2ffbea6f7cd7a877ff88ffacd04fff00ffff9f9bdfdcddffccd4796e49b53aa363c03bf52b4e9ddbccec748d5521e11687eccc7664074f657f9541f53db43f6f1f3f32a2ffbd064be51efdd5e3a1fb60810ef653da37c5bc33f576beaacdfd7575ecadbe68d6795d76134fbd858c13ffcc33ffcc33ffcc37fbd26f887ff31e663e11ffee17f2b7bec7acfe4c13ffcfbb5c13ffcc3ffb68bf731f00ffff00ffff05faf09fee17f8cf958f8877ff8dfca1ebbde3379f00fff7e6df00fffcfc2ffddff0bc26c71fe52fb0dbebdab86ed9c5ab0facfced967b5589597c9ae23faf06c627d7e037d6fca2f82ced6a97c7d2f519fd1feda787b16d19944973dfaa86550faded4058f7af579d1b9fa1ad51df6e790edbd5ab35fcb5b01bafa10d9787f47540dff6cbda37f38ec3a949ffa48c23ffcc33ffc2bc13ffc57bdf8daf00ffff00ffff0ff59dfd7857ff8cf7c7d2f519fd1feda78f8877ff887ffa85f1b0bfff06f05fff05ff5e26bc33ffcc33ffcc3ff677d5f17fee13ff3f5bd447d46fb6be3e11ffee1ffb9f8ffe90f7ff8c32fd1e6a862dd4be72f54b4383fdfb9c89df96e6fab313edeaafac854756dacdafbf1eecf238af3356c9f51cdecc3ea9f3b7b19cd55e7e4c7549ff6a2677d54eaf854bd45ef51ad6aef2a299f8caff1ee3f0e91afaf01ff3ade0afef59ad41cfcc33ffcdfd7b5b1f07f3fa6fa847ff8877ff887ff2dec05fe756fab313ede0afef59ad41cfcc33ffcdfd7b5b1f07f3fa6fa847ff8877ff887ff2dec05fe756fab313ede0afef59ad41cfcc33ffcdfd7b5b1f07f3fa6fa847ff87f55fe7ffab33ffbb35fa249d5941fcbde7d9efd68a8f7c8b37b41547e3456491d42d49bff90298fce25cbe6d487d7c7fbe7ecc33a7bd115e859eeea9eab7ad9bd8af63deb577dc8a3b9d5ded5475c9d83cfad6a543c44e7ee632b96953ffcc33ffcf77a87ff4df693bd479ed97ab3e76cac12fcc7f53bbdafeeb9aa07ff9f71150ff00fffb6b6cf857ff887ff5af01fd7eff4bebae7aa1efc7fc6553cc07f1edb15fcc7f53bbdafeeb9aa07ff9f71150ff00fffb6b6cf857ff887ff5af01fd7eff4bebae7aa1efc7fc6553cc03ffcdbda3e17fee11ffe6b3d8affdfff00abd34cb7f1ea20aa7cffeb3762551970fe59e556fdfbb5772e75e7c2ccee7fe411c546f38fd015753b1fc099bc28b6b37fea8c23908f9e47d5ffea5ec33ffc5f25f8877f1fe77fe11ffe673de15fcfaf08fee1ff2ac13ffcfb38ff0bfff03feb09ff7a7e45f00fff5709fee1dfc7f95ff887ff594ff8d7f32b827ff8bf4af00fff3eceffc23ffccf7ac2bf9e5f11fc5fc77ff807589de42afe8a038f2ef4951f90ea608e78cd5c802a26cb3ba2abf7b753ff19e4cfaa0364768fafe84bbd9fe1b95213feeb78f8efd57f06c1ff5c4df8afe3e1bf57ff1904ff7335e1bf8e87ff5efd6710fccfd584ff3a1efe7bf59f41f03f5713feeb78f8efd57f06c1ff5c4df8afe3e1bf57ff1904ff7335e1bf8e87ff5efd6710fccfd584ff3a1efe7bf59f41f03f5713feebf8b7bffeebbfeefd699b30eafe551c425e2bf7e791778efb9d8bfd414704ffaf2df6071d11fcbfb6d81f7444f0ffda627fd011c1ff6b8bfd414704ffaf2df6071d11fcbfb6d81f7444f0ffda627fd011c1ff6b8bfd3947efdba27ed4cd7fd67577fb7a44ffbe86aab9d2c74cced175f271c905ffcf25f85f8fbd22ff7b17fc3f97e07f3df68afcef5df0ff5c82fff5d82bf2bf77c1ff7309fed763afc8ffde05ffcf25f85f8fbd22ff7b17fc3f97e07f3df68afcef5df0ff5c82fff5d82bf2bf77c1ff7309fed763afc8ffde05ffe768f90fb0bab20ddf9efdfbd5757dcd33fda30f4be703b45a337a577f91a8f640fd4fa645fdad9e55f77f12efcaf3cf6a9ef50f42c7378a3d5399df577f28e11ffee17f1f7ba6e03fae0bfff0ef6bc2ffe304fff00ffffbd83305ff715df8877f5f13fe1f27f8877ff8dfc79e29f88febc23ffcfb9af0ff38c13ffcc3ff3ef64cc17f5c17fee1dfd784ffc709fef375be6705b2f7ee9cffff9be9df3bea6ca03f58fbff33d5d7b187335bdffbfa838efe7f541e055f9d87ada37c465ddb5314a7949d55d577675ef574d5c771489dcf189ff1f2e759edb1cdaffe41caf660e6fcb25a95e05f7bc37f5c6f761efe63ef9939f8877ff8cf6b0dc17f4ff0afbde13fae373b0fffb1f7cc1cfcc33ffce7b586e0bf27f8d7def01fd79b9d87ffd87b660efee11ffef35a43f0df13fc6b6ff88febcdcec37fec3d3307fff00fff79ad21f8ef09feb5f7d9fc877f806593aa83b48da8a6fdbb7ff68b882e6077e33a1f26fb1b4165e36d6f514cf591cbe6fdda2ad0fce1cf8016ad41c5cd785a75ff71b0b1d13a9457764fa2fce80e757dbca7ef2bcaf10047f7bd62ccafa5a3990fa9af07ff9ff1f03f170bffda0bfee11ffee1dfe6c03ffcc33ffcc33ffcc33ffc47f5fc5a3a827ff857b5e01ffe33c1ff7d3cfccfc5c2bff6827ff8877ff8b739f00ffff00ffff0ffe3f1ff1e25f84be3e7d48664975b1de038583f97413872bac0cf7c987c9df16bfbf41b690fc77b281f3fef2f83ba1c336b567dfbdec67f1114ca3f5b83afe37d958ff2f0b991a27be3bd543db5bfeaccbbf7497d5cd5c74b796735aa8f90eaa1b3871187d9d88fc6bffa4701fee1dfe64573f0fffafc5b0ff8dfee62e01ffee11ffee15fcfc13ffcc33ffcc3ffbd0ffcc3bfaf05fff00ffff0affacf04ff9bec01feef6b465e99e05ffb676b80ffcf39f8877ff8877ff8bff781ff80ff3ffde94f1f63505dbcec3d926ac617b7cd2b1854cd23f3333e55be9db36beaf413f9fb5ade57c567bd46f92a6ec4666b551e590d5b27caf575956f049c925a83f5cb3eac3646d5f739ddfba3eaf8fce88efa3e228fee7d567b51ed0ffcc37fe491d5b075a25c5f57f9c23ffcc3bf8ecf7a8df255dc88857ff887ffadccb773764d9d7e227f5fcbfbaaf8acd7285fc58d58f8877ff8dfca7c3b67d7d4e927f2f7b5bcaf8acf7a8df255dc88857ff887ffadccb773764d9d7e227f5fcbfbaaf8acd7285fc58d58f8877ff8dfca7c3b67d7d4e927f2f7b5bcaf8acf7a8df255dc88857ff887ffadccb773764d9d7e227f5fcbfbaaf8acd7285fc58d58f8877ff8dfca7c3b67d7d4e927f2f7b5bcaf8acf7a8df255dc88857ff887ffadccb773764d9d7ebc7e5697bb73f8fee086464e346e7323003af04597d52f5a6d40f4b1e93cab7a4ad5b8dd27dfebf0ad3ea8d9b9d8beab3df1bd44907c343e82d985f5f3b6be8def40e4e7edaff5dcb6fdbe461f9bee59465c54f7240275e68c321f55537955e70afff0aff2e17fbbf3837ff887ff7d7df8bfcf837ff857b9f6d78fc1ff9ca7fdb59edb06fff0bf8575e01ffe472efcef3dec7b1403ff9bec2f1b877ff8877ff8877ff887ffed2e07fee17fd6d3fe5acf6d837ff8dfc23af00fff2317fef71ef63d8a81ff4df6978d0fbff7ccc806db66464376b3edbbcdb5bf3317d95f50e56d3d2298fc07c9e6451720faf0f9fd501f84a8675fdfbeab1e554d2fb5effe5c7cbde89c2328a2bdf0fd29ef3166637caf6abf6daedf6b5f4fdd51bf47de4fc16b636d0f6aced7556b8ef2a258d5bbf2f35e760f7d4dcfadcf8f3e96aa06fcc3bf5f9f5f03fcc3bfcf85ff4dd6b7eff00ffff0bf5f33fcc33ffcc33ffcc33ffcefc7471efcc3bf1f5339f00fff361ffee1df8fc3ff7dff6a0efee11ffee13fca817ff8877ff85779512cfcefd7fc23f0ffae2e4076697ca33ece1e4ed44455cb2e50d5f73df867ebeb37c41f4c7609fdc5f331d1a16f9bbe80d97ef84b135d16b50615e7fb507be0cf28da1b95ab3e62fe62aabd55eb50395ed9babdb28fd31857f12b526b553d47caee908af19ed13df6ff8db968edf00ffff03f2ff8bf8f83ff6df3fbe1f745ed07fcc33ffcc3bff2827ff88f3ce11ffe559cef43ed01fc6f2dbfaa16fcc33ffcc3bff7867ff8877ff8cf04fff00ffff03fbce01ffee17f2ff8d79ef0ffdafcbffde94f7f4a6f882fa2a0b3ef36afdabc55a98d88ea471f2f1b97f55f5dc6d91e23bf6c4dd55e7aaf4efdce5a56cea9bbbeaa96bff8955f068a9ff71eea4e77ef4ef681f2b5b31ad13f48ddfdcff6c3c7f95e2a5f1b0fffc77a84ff5e2df887ffac27f8ff8cf35e9dfaf00ffff00fff336be808fee15ff5d39deb08feefeb78af4e7df8877ff887ff99357404fff0affae9ce7504fff775bc57a73efcc33ffcc3ffcc1a3a827ff857fd74e73a82fffb3adeab531ffee11ffebf9eff9fab667d516bd205233bacecb2458baffc6d9f1960decbab3ae8ce26ab758d18dbd7ccc73702dabefb3e7ced8e54eccc7ea979ef63ef995a43956f3d2298a278ffecdfed1d8af6a2fa08fbdeb21e7dafea6ca37efd9c8fc96ac2bfd61bfcc33ffcef7a7b83fff41dfee379ef03ff5bd82bfcc33ffce7f9d603fee1dff709ff9bacdd11fcc33ffcc3bf1f8b04ff9bec09fee1df0afee11ffee3da9bebdbe7c03ffcc3ff7d1df8877ffb0efffbb5f878ffecdfe11ffebb6bf36b89626c6fb7ff7efac31ffef08b0dce14cddb03519b9135d351f783b1e2b3da6705deaaaedea395bd54172d8a9bf551d0752f7257d9b91f39ab4edd59ff9573cc72a23d56ffa0c0ffe718fcdfe7c0ff5a5df8cf7be908fe7b7967c6f91cf85fab0bff792f1dc17f2fefcc389f03ff6b75e13fefa523f8efe59d19e773e07fad2efce7bd7404ffbdbc33e37c0efcafd585ffbc978ee0bf9777669ccf81ffb5baf09ff7d211fcf7f2ce8cf339f0bf5617fef35e3a82ff5ede99713e07fed7eac27fde4b47df33ffbffe0196ddc0595d75c9ba9b35abeec59df1998d5bdd67fb9bc5ac6a7cfcaa5a3375149cab5e555ef441b1eb3aeba3d3993bfa713fd2db4a1df85ff3998d83ff35af2a0ffe8fd581ff359fd938f85ff3aaf2e0ff581df85ff3998d83ff35af2a0ffe8fd581ff359fd938f85ff3aaf2e0ff581df85ff3998d83ff35af2a0ffe8fd581ff359fd938f85ff3aaf2e0ff581df85ff3998d83ff35af2a0ffe8fd581ff359fd938f85ff3aaf2e0ff581df85ff3998db363777f80359318c574d5dd88b32e59e57964431fa9eca398fdf5e18cdfa3f6bc3337e31ffd756de72371c63f3636dedfef33ff515be9673606fefbb98f14fcc71ef0bfe60bfff05f8d1d15fcc37f940bff7dc17fec01ff6bbef00fffd5d851c13ffc47b9f0df17fcc71ef0bfe60bfff05f8d1d15fcc37f940bff7dc17fec01ff6bbef00fffd5d851c13ffc47b93f32ffe5ff0bc23301b597d0c3711684d9ef515fff1ccd1fdd9fb394ad7ff419fd55ea597b37ab33217fb43a1ffa192fff7ce65e1c611dfef379f85f17fc7f7af967f8effbabdfa3befe399a87ff75c1ffa7977f86ffbebffa3deaeb9fa379f85f17fc7f7af967f8effbabdfa3befe399a87ff75c1ffa7977f86ffbebffa3deaeb9fa379f85f17fc7f7af967f8effbabdfa3befe399a87ff75c1ffa7977f86ffbebffa3deaeb9fa379f85f17fc7f7af967f8effbabdfa3befe399a87ff75c1ffa7977f86ffbebffa3deaeb9fa3f9997a6f7ffad39f3ebcd15997e80a1d39acd9dc28fee88539ebc23da3ce58dbabee8fed7b3c9fb196e8c3def5eec6c17f2f1efe63c13ffc5f29f87f6ec13ffc5f29f87f6ec13ffc5f29f87f6ec13ffc5f29f87f6ec13ffc5f29f87f6ec13ffc5f29f87f6ec13ffc5f29f87f6ec13ffc5fa9ef8dff773f3136df3ffb38ff9c8d59afa8a1aae11133bb815636d7f7a4d611d5f2e3675dd8159fec6cd458a74674ee2aceefe3ec1e2bcf95cbddb9ab7e3cbaefca4ff966eb196be8aea5cb8762a13a271f57ed897af671fe19fe8f0bfee1bf9a837ff8cf72e01ffea35ee01ffee1ffbe37f8dfa437fcc33ffcc3bf8a877ff81ff3f00fffd518fcf704fff05fcdc13ffc6739f00fff512ff00ffff07fdf1bfc6fd21bfeb787f07ff7bf8035ccfc5ff145f3b33a0257d6d318bf29f31fb9deafcaabfc5675f46353edc91179cfaa46f78ca2cb3bf3a1ba2276565def99bd38bbf6516ff8eff7b522f887ff2b6b1ff586ff7e5f2b827ff8bfb2f6516ff8eff7b522f887ff2b6b1ff586ff7e5f2b827ff8bfb2f6516ff8eff7b522f887ff2b6b1ff586ff7e5f2b827ff8bfb2f6516ff8eff7b522f887ff2b6b1ff586ff7e5f2b827ff8bfb2f6516ff8eff7b5a25bfebb1fb486cadc376ecdb27755bc333eb3c8ec60ecbc5f63b4a6a8bf9943cb3cbb75c7bcad5b9d53c74f8d476bf31f63ffbcda8f5f8bed41f539b3e7d57d5035b31ed45dea2af251ef55fecc7df5f9550efce7827ff88fbc3b3df967f55ee5c3bf16fcf704fffb31f8bff7f6cf556df88fe3ab7ee11ffe67f2e15f0bfe7b82fffd18fcdf7bfbe7aa36fcc7f155bff00fff33f9f0af05ff3dc1ff7e0cfeefbdfd73551bfee3f8aa5ff887ff997cf8d782ff9ee07f3f06fff7defeb9aa0dff717cd6efbb3f187f19bb8a8055fe6abee3d3f99845e39d038f3c7cccac677539572f58f79c3cfcd95ef9fd8e3eb6d1259cfd78aabdc8f643f5d3d9cfb196e8a332d6e4d7a5ce2fabeff72c82beca5be530caf17d445cc27f1d03fff01fd587ffad5507fef73d56827ff8af04ff5b98af04fff7fef00ffff09fc7c03ffc47f5e17f6bd581ff7d8f95e01ffe2bc1ff16e62bc1ffbd3ffcc33ffce731f00fff517df8df5a75e07fdf6325f887ff4af0bf85f94a5fcdffbb2dea2f68046054b48af5175dd5f39eb62fd59bbf386a7cd48e7cb3da6a9d763dbe1f1f3b6a5407add6a2e6fd47a2d3a7ef45c5787ff561551f5edfb7efe9230047ad41fd43118d659e6aedeafef858b5665bd3f7eac7abfb1f7d6c3a1f0f5fa3fab0f935f831555bdd8f6a4d33b1f0bfcf53f91ff00fff1bfcc3ff7e1efee17fdbe0ff03fee13ff084fffd1afc18fcc33ffcc33ffcc3bff2877ff8efac692616fef7792aff03fee17f837ff8dfcfc33ffc6f1bfc7fc03ffc079ef0bf5f831f83ffe7e0ffed4f7ffad3874db097ca9b7900d502a28f8bbfacd1a5b3f157cbf79fad39ea3f8b195287dfd9874e9fea5ca238df4fe4e5e33b39d9f947ebf17b51d5ca7cb20f895a5b764f67c73ade953a6cccc4d8f79b66cf05fee1df0afee331f8877ff887ffccbb12fcefe3af16fcc37f14333306fff00ffff09f795782ff7dfcd5827ff88f6266c6e01ffee11ffe33ef4af0bf8fbf5af00fff51cccc18fcc33ffcc37fe65d09fef7f157eb29f8bffd0156651c29db4c057005af8f9da9a7ea4735bda71a57f3d9c7a9daa7aa2fdfbbf79df9e846879ff5503dab5ad53aed1ab233887cabdefc9a541f2a467d34ba7ba1fc2378adb75a6ff5619ab9ebbeafe8be56f706feb7701efee15ff9c33ffcc33ffc47b5e01ffeabbe7cefde17fee13f12fc6fb23efcc3bf8a817ff8b79e6a5ccdc33ffc2b7ff8877ff887ffa816fcc37fd597efddfbc23ffc4782ff4dd6877ff85731f00fffd6538dabf92bf87ffbabbffaab8f6c41ea92448d8d77359fc5459b92f5e26ba8f7cc2b5a5bf6d18b6a28ad5efe0acaacb6faf8ce7e6c556fbe4fb5c6ac86f7fe687c9ca2deec7cd663f7839ef51af5a5d4d9976aade33dbb7f3e2feac1c6557b0ffff0bf6df06fdf95e01ffee11ffe95e07fdbad43e5ab38f8877fbf8ea8373b9ff508ff9bf483ff7c6df00ffff07fef09fff00ffff00ffff00fff5af0bfedd6a1f2551cfcc3bf5f47d49b9dcf7a84ff4dfac17fbe36f8877ff8bff784ffebf87f1f4136703c470d46504617dfceab4d508ddbbe6c7fbe87c86bc4f8dca8a6bf20be07a5113f6a6507667bf731b6071f977d7c6d4dbf06efe773fdb307a20b859d8b3e32116cfe2cb38f915d6fb51eef11dd17d5a7ea29fb20d85f55dbaf233a579f5bf5a462bb7ba9ce41bdbf02ffd11de8f2efd7a6fa7b45fed57ae13f17fcdfbfc33ffcdb5a91e0fffbe37ffc3e3bff9157977fbb1fdf13ff8a13f8cf05ffdb6e5df00fffaa0f3507fffbda7e1df09faf17fee11ffeeb9e542cfcc33ffcef6bc23ffc677dc03ffc8f1cf8877fef09fff01fc5db5f55dbaf03fef3f5fe68fccbff178451b26f56c5f96259f36a4e411779aa4b66e36c5eb787ec7267e3dd1e559f6aaf3a7d641ff3ac179593d5f5173402b37b6ebe5e75c1b33a59dec8edd4511f8fce9a7c9daaa7993564b57d4cc6a07a8f78fe9ef957de8fe4bfc3d14c1ff0bfa57923b75ba7ca7956fe157fb6967a877ff83f837f1f5379a97a591fbe1efce7ecfa75c03ffc5fc97fd5bfeac3d7837f1d3fc3bf12fc6fb2f7ac07f8877ff8877ff8877ff887ffee78a747f8877ff8d7ebebae1bfee11ffee1dfcec13ffc67b9591d1f9fe5c03ffc9fcdff4f7ff8c31f7e5109d1627c619f9b35a872549c1dcb2e58761851feed79fcd78160fcd7a93ff361887caa7cbb1e75a16ccccc474f9d53941341a2f62af252393e3fbb2fcab352b4465b57adb1db47d5833f1fef9d79aa7e545e7576594fbede4df00fff3e47d5b5f5235fdf67f6ec7dafe67fe815f95767e13de0ff7ebdf00fffea19fee15fc576eac33ffc57f5e11ffe339f2a1ffeb52af67c0cfcef7bf2f56e827ff8f739f05ffb47f3f00fff994f950fff5a157b3e06fef73df97a37c13ffcfb1cf8affda379f887ffcca7ca877fad8a3d1f03fffb9e7cbd9be0ff5afe7ffd03ac683232e840e6e78f42a42e6cb5d91ddfac96f75e55e763a82ec978eefaab71f57c54ea03d0f960753f0e477b5577c5fefae74eafd507328aa93e541d76b239fb8fadff08451fd7add97bd40bfccf09fef7793e3ff23fd21ffcc37fe59bd5f2deab82ff7d9ecf8ffc8ff407fff05ff966b5bcf7aae07f9fe7f323ff23fdc13ffc57be592defbd2af8dfe7f9fcc8ff487ff00fff956f56cb7baf0afef7793e3ff23fd21ffcc37fe59bd5f2deab82ff7d9ecf8ffc8ff407fff05ff966b5bcf7aae07f9fe7f323ff23fdc13ffc57be592defbd2af8dfe7f9fcc8ff487ff00fff956f56cb7baf2a5bff4fffea5ffdab5fa2a4c8e8a6230d55753a39ddfad9876f45331f465bdfee5d37478d671f82d933e980b4ba4f3335676a441f8d15c8aa0fb2ff8df677b6ff99fb38f3b1ebde81ce0711fe633ffbdbad0fff714df887ffd91cf8efe55475b2f768ec88e01ffeab3a9d1cf8efe55475b2f768ec88e01ffeab3a9d1cf8efe55475b2f768ec88e01ffeab3a9d1cf8efe55475b2f768ec88e01ffeab3a9d1cf8efe55475b2f768ec88e01ffeab3a9d1cf8efe55475b2f768ec88e01ffeab3a9d1cf8efe55475b2f768ec88e01ffeab3a9d9cef85ffdfff17b06cc2eaa6cc8c1fd1910fc515fd64b5662e6af541517e47d6b37ad69197f5ac2eeacc8765a67e34e69f8ffc35e615521f7c3f6ffbbeaa3ff83f4ff00fff5dc1ffb15af03f2ff89ff3bb52f07fac16fccf0bfee7fcae14fc1fab05fff382ff39bf2b05ffc76ac1ffbce07fceef4ac1ffb15af03f2ff89ff3bb52f07fac16fccf0bfee7fcae14fc1fab05fff382ff39bf2b05ffc76abd2affbb3fc0ea98aecc9d113f72563f6aeabd1a9f895db90467eee76ced6ecc6a1fb367e57357e6b2b8d9339ecd3ff21178f4077f55f00fff33b5e0bf1707fffd1cf85f17fcf7ebc3ff5ef00fff33b5e0bf1707fffd1cf85f17fcf7ebc3ff5ef00fff33b5e0bf1707fffd1cf85f17fcf7ebc3ff5ef00fff33b5e0bf1707fffd1cf85f17fcf7ebc3ff5ef07f32ff7ff5577f55aee608486768d4ffffd87b1b25c98ee44aef7601e492e252da959e748017c0bb76d3a495513fcbd5924b0e89d6248617ed79f21c77bf59d54037f0f95859de1be13fc723e28b9c192babbea2e3d7d6bcb5b7d2796eeaf64fd06d7e0334fde9b6b7fcf373bfc41eb9f3f34bd47eeb1abfd69986ffcf67f00fffbf56be2fbdaed687ff3ecfcde03fd781ffafabaed687ff3ecfcde03fd781ffafabaed687ff3ecfcde03fd781ffafabaed687ff3ecfcde03fd781ffafabaed687ff3ecfcde03fd781ffafabaed687ff3ecfcde03fd781ffafabaed687ff3ecfcde03fd781ffafabaed6ff2df2ffb271fab51b39eb5778b631cebf8bdfe4de5aca55c7afac6da7ed96e73ca4530f9b9ac9e799b3e0f4fc9280e8f9399f9fd9eb2b316fdd5fcab7d5f4ecd986ffe70cfeff6cf0ff3606fff09f0cfeb3c1ff35bf6ddd5fcae0bfcf77e6817f6ff07fcd6f5bf79732f8eff39d79e0df1bfc5ff3dbd6fda50cfefb7c671ef8f706ffd7fcb6757f2983ff3edf9907febdc1ff35bf6ddd5fcae0bfcf77e6817f6ff07fcd6f5bf79732f8eff39d79e0df1bfc5ff3dbd6fda5ecb7ccffea2f603d53f8d7de340cc37e1d837f0cfbfd1afc63d8efd7e01fc37ebf06ff18f6fb35f8c7b0dfafc13f86fd7e0dfe31ecf76bf08f61bf5f83ffbdadfe02d655fb3d2cfe97d6e32ffddba46fed7b35cf33b97f8d3dfb256b7e296712fe7f7983ffe7f27c6e83ffdfa6c1ffdbfa5ecd03ffbf6eadcee0ff9737f87f2ecfe736f8ff6d1afcbfadefd53cf0ffebd6ea0cfe7f7983ffe7f27c6e83ffdfa6c1ffdbfa5ecd03ffbf6eadcee0ff9737f87f2ecfe736f8ff6d1afcbfadefd53cf0ffebd6ea0cfef7f6b24dfa5b5ad4d7f472e5cfa0fd52f6ec9fd37bc6aefcf9c4fa9b90af59f3f4270c3b73f57e8d3fc1b7adf9167cbdf60cc0ff3e16fee17f63f0ffe519fc3f6ff07fcde0ffcb33f87fdee0ff9ac1ff9767f0ffbcc1ff3583ff2fcfe0ff7983ff6b06ff5f9ec1fff306ffd70cfebf3c83ffe70dfeaf19fc7f7906ffcf1bfc5f33f8fff2ec4be2ffee17b02a30b5d839e7846833e9b715d3788aa9714953b2a95ed7e7645dec9427e9d279fde9fcddf3c6ff35d6edb9ce9de766faf76bafd44bf3eebcea3a3aedd3999dcefe9575d5b5d21ad39e6b0e37f76c3cfccf06fff00ffff0ef62e11ffe2783ff3987c6c17ff677cf1bffd718fcc33ffc3fc6c23ffc4f06ff730e8d83ffecef9e37feaf31f8877ff87f8c857ff89f0cfee71c1a07ffd9df3d6ffc5f63f00ffff0ff180bfff03f19fccf3934eeb7ccffbbbffbbbbffb581d3a509ccf2626f96d639ff1ef7ccfb9abf5b736e5bdb2166fd5f3d55c57ecd933f19635cfb19bbd75ed69bf36fd6fcee36bf43c5bfbaa1ffcbfae769a87ffd7d53cc76e06fff00fffd7e79f35f887ff6772c3ff1c03ffbb9ae7d8cde01ffee1fffafcb306fff0ff4c6ef89f63e07f57f31cbb19fcc33ffc5f9f7fd6e01ffe9fc90dff730cfcef6a9e6337837ff887ffebf3cf1afcc3ff33b9e1bffc05acee02387f6ea63e6723a76f8d71b9ebb87bd7b1fa9cf4b93c5acfd5a93e4e73f7e9fca65ece773dc0a796d477a7cbe5ed74249d57e3dc99e8f6b6ab3b99ebdfd5bc8d9deb58eb690f696d556b3af33a566bd677f577eb532f83a4c3d5eee6a6787786f45c6a0ef8877ff83fee6ad567f887ffa997f31dfeaf19fc1f365777a6bb78f87ff4f908fff02f5ad399d731f887ff69adeb3bfc5f33f83f6caeee4c77f1f0ffe8f311fee15fb4a633af63f00fffd35ad777f8bf66f07fd85cdd99eee2e1ffd1e723fcc3bf684d675ec7e01ffea7b5aeeff07fcde0ffb0b9ba33ddc5c3ffa3cfc75f89ff9ffe02567729e87b3d3075f1dde268c1b4c969cee94af153be4d2eeded4a0d179b0e4fa72569e800717e9b75bfb29e6f69ee3c254d57cfcc34b739afa7b9f5736c5c392b9b735ec7cf5addf99ad6603a93f07f3f06ff9fd7e01ffe3bbf3406fff0af79e01ffe37f9e01ffecff1b3768a3b0dfee17f5b03fef706fff0dff9a531f8877fcd03fff0bfc907fff07f8e9fb553dc69f00fffdb1af0bf37f887ffce2f8dc13ffc6b1ef887ff4dbe2f95ff97d3a97ed60429a1263e7d35572d5afd34d7e6503a1d5ad7e59e16b4bb10d427998bbd3d9f3f2e9fd3e97cd261497155cff9dc69d63c49477a76efa9b7b397ee0b211d5ca7bfabd1699acecdf485b5ed7ba355c7d39e9f795c8dab675e59827ff8877ff8877ff84f3ad233fcc3bfae87fa4f8cc23ffcc33ffc77fae1dffbc2bfafd169827ff8576df00fff5d5df887ff64f00fffaeef8d561d877ff8af3ef07f44fd5d8d4e13fcc3bf6a837ff8efeac23ffc277b96ff974e9816d064dd4139635c7c77186b4eb7a929af3ed7987a68a74334f5e20eaabe6f00a93955abf6b65d8bf33d1dfcd4573a805d9d7a91baf557cd755cfb4d3eb556671dcc4e9fd3ed3e6b0e5d8baec6465b3ab3ba7e3aaeb9a6cb42d94af526837ff8d77ee0dfd7d86883ff236a84fffbdee01ffee11ffee1df1bfc1fb126fcc37faaaffef00fffc9e01ffeb51ff8f73536dae0ff881ae1ffbe37f8877ff8877ff8f706ff47ac09fff09feaab3ffcc37f32f8ff3cfcbf1c0bab0b336d645dbc3aaef9d2a1739f9a5f73e967bab89c9eb4a0297fb741dd81ec7ad183a96ba831da5b7d76b1559feb2d59077d97ef9c53bf0ea273befeb879ade5cc9d571d77392688dcbee9bcbbf052adf4eecebffa391e6b7cbafc347e63f00fff750cfee11ffeef0dfee11ffee11ffee1bfd6827ff83f0ef8d77af00fffa95ff8877ff8877ff8877f9dd75acee01ffee11ffee1fffe1dfee1dff5960cfe1f7bd73e751efee1ff0aff2f0e9464bae89aacbe4ff912046e2c2d922eac6bde69aefe9da67a983af8ba0d3ee7157ebd70ab9fe6d2b8ae37bdfcba7da8be535fe9e24a7a55a7eb51f374c0753955735a976ecd1db0ba1f1384ee1248e7db694eb5f4ecd49eea67da97c4897bee0cfee1df8d3bbdf00ffff0ff691efee15f35ea3bfc1f760cfee11ffee11ffee1dfe5847ff887fffbbee01ffedd33fcdffb779ae01ffee1ffbe1efcc33ffcc37f1d53bdb54e32f8877ff8bf7f87ffc38efd96f97ff7777ff7771f6bb1ab3689737ecf985b8c6e5ce7b763693127fd2967cd33bd5fb1670fc2b416db7dd20b62cae1409fe2366b7e6a7073d345f99af378da2dc78f3ffef8f373cdefc6aed6dc9cebd7acd354e78a3ef887ffa90efcc3ff3306fff706fff00ffff00ffff00ffff05fc7747cd208ffcf1bfcc37f97b3e6817ff887ffe70cfe1feb398d4efba411fe9f37f887ff2e67cd03fff00fffcf19fc3fd6731a9df64923fc3f6ff00fff5dce9ac7bdbf2848e78f2b707ebaf9338736919aabb55c4e1d73174d37af7da9bfd6d21eba7aae0fb739354fbd80eabcd3d26de604b7bbe45c3db7efae56d77bd25f63dda19b0ea6cbe9f6ae5b3fe753d7f65df8e272793a26f48bc59de34eeb34a679ddbee97ebb1ce93cc33ffcc33ffcc3ff7d0ff00fffd39987ffc3d681fffb5c750de01ffea7ba87f408fff00ffff0aff9e01ffeeb18fcc33ffcc33ffcc3bfe6807ff8877ff8af39e0ffb079e01ffee1dfeb4af3b50efcdfe7aa6ba0fcfff417b012f0a7f8da842654ff2a3001e516a0f34db9f579aaa73de9dc996baa9fd660aaab63aede662d34ceedc5699bba6eaf75bd5dae2bbab5f67446b66b9b72a77e9c7f577f73de36fc38bf2be7be8e9db63defa927d74bd20ffff00ffff00ffff09f74c1bf8f87ffe392ce6e1efee17f931bfee13fe5817ff887ffc3f692f4c33ffcc33ffcc33ffc275df0efe3e1ffb8a4b39b877ff8dfe4867ff84f79e01ffee1ffb0bd24fdf0ff99f83f7f01cbd9b401e93d1d92ce6703673a044e7fcaa97db9bca95e1d73f5dcfc74706a1fb5c66b0e7d3a185dbecda5e5b4d53e533ef5a91a9cb674f9b9b9943b5d805a3be9737d6d00def4dac56c736e7bd6be37da353ffcc37fd256fb84ffacc719fce73e3436d5847ff877b9e11ffee1ff68fb817ff87775343ffcc37fd256fb84ffacc719fce73e3436d5847ff877b9e11ffee1ff68fb817ff87775343ffcc37fd256fb84ffacc719fce73e3436d5847ff877b9e11ffebf06fe1ffe02d604a00aec363c09e9c0eec6ddc66e2eabe932d8c46d204a97a3f3eff26df2b8f9a966b50e9494777b114d67628274a325d5eac09ad676bbdfddba4c3d769aa7f5dd5cb6aeaf2be3f00fff292ffcc3bf8b83fffb1c9ac7f9c33ffcc3bfd7d0694bf9e01ffedd7ccd0ffff05fe3e11ffee11ffe3771f00fff9b75997a847ff857dfd46f97c7cdc33ffcbbbeae8cc33ffca7bcf00fff2e0efeef73681ee70ffff00fff5e43a72de5fb5af9ff362573013a7f26d105387f6e63e7788d5511a9be5b949ab7c6bb71d74fad5575277d13d04eabc6a643e2726acd4dedd49f6aab7d6f63d3a1ad9f570e6957b39e1bb7b7ea7b18ad355ef56c80e87aefb4a44ba73b5fa9cf5443cf50ba5834b7ae477736d4e01ffe6b1ef887ff6e4de01ffebb7c5d4df8877f971ffee11ffee1bfd6827ff83f0ef887ff238ec3ff61ebc1ff7ddd3a07ff797de01ffee11ffebb35817ff8eff27535e11ffe5d7ef8877ff887ff5aebb7c2ff4f7f014b0babd306dad4601a9b2ea3a97177185de3d536006afecd3a383fd5b5d171b5565a87e9806ce33547cd950e5577e8dda1743a8e633e03ddd8d46bf7a9d6edadfa74fbb0dddb699dd2fa244bb5139f357fcab131f887ff290efe1f6b4deb94d62719fc1fb627575375c23ffcc33ffcebf8d42bfc7b837f9fd3e5867ff8877ff8877ff84f7ef00ffff00ffff00fffc9e0ffb03db99aaa13fee11ffee15fc7a75ee1df1bfcfb9c2e37fcff3ef8ffe6bbefbefb212dae83420bba2637106e2c41793e4fb153be6efc36d6d5e836bdc6698eee10b85c6e5c63ba5a9b75baa2d7ed41b7ae691f92167dbe7a5edc58ba58eafcf6cb64b356a98f3367d793eb7bc3d499dbbd27fff45ec7e17fd606fff0af9ad318fcc3bf1afc3f8ec33ffcc33ffcc33ffcbb77f887ff4d5ef8cffee9bd8ec3ffac0dfee15f35a731f8877f35f87f1c877ff8877ff8877ff877eff00fff9bbcf09ffdd37b1d87ff59db6bf8ff366db426b98d57916911bb0d3b73b8f17431554d298ffabbd8e940bb03e50e5a3a9469b3ba1ebbc39b0eda99c75d1ce95db5761ab7bd757535b78bd1bc53bf5d4ed5957c3b736bb78da9753b68a73377e582d1f8672e0bd505fff00ffff05f73d771f887ffa415fee15fc7e11ffee1ffdedf69857ff8d7dcf07fafadcba9ba926f67f00fffaa1ffee17f1353ebc23ffca71ee01ffe9d56f8877fcd0dfff7daba9caa2bf97606fff0affae11ffe3731b52efcc37feac1eafdf0e1c3c7ada064093e2dac6236704ec06eb4a53cee204e7d4fb19bdea6bcdbb80dc01d9c0eb66ecce5edbe44eafb660d5c9ecde5d1d5486bb439b35d6f13985abfda95b37b65cd52dce61cc13ffc4f632e2ffcc33ffcc37fd20cfff00ffff00fffc79de68dc13ffcc33ffc5f35f83f5679e01ffe377575aecbebf2a9c13ffc7779b771f00fff9b38f8877f67f00fff691efee15fc75c5ef8877ff87f9e7ffb0b589a284190c09880d343990e7a77a16c6b3f03cd953e535ed757aaa163b58f6d1ee777f5904f17b1abe1744c39ceb9b49e29ef14d39de3947fb397092aed2769575853fe2ed7748975eb71f5cb06fee11ffee11ffee11ffee11ffee15fc73b1d538e730efee15f3577355c6ef887ffea07fff00fff3e3ffcc37fd206fff3dac03ffc6b5ced2769877fdf4bea17fee1dfe5857ff8877f9fc7f9c13ffcc3ff97cdffcbe97cfb4c0b5313d5b133a68e694ccaab794edf3ae66ae8a2b9cdabcf1ad72dbebb283aabda3a68eab3aba1f0b975d79ad5b6b5d305e2fa489a92d578f54f3db97393acbbc46a7ded47f3bbf3b101b25b974ebbf6edd6a67ed61af5b3bba45cde943bc5c33ffcc33ffc4ff1f07fef0fff9f72c07f8e83fffbbc9ae7f4ad63f00fffd517fe8f07edf00ffff00ffff07fdce5505ff887ff73ce69d61caebec6c33ffcc3ffa71cf09fe3e0ff3eafe6397deb18fcc37ff585ffe3413bfcc33ffcc33ffc1f7739d4f74be5ff9befbffffe87b39114d05d1a9b26ce1c9a6b6a5817a903c4c57679a61817d7e572bdd50334699dfcb69a55a383597daed8a431edb3d334e573f993e667f67b339fb44e70a5b3b4e16acbd4666ca3e566f00fff1b83fffc0cfff0af7e13d73516fee7daf07f6d6ca3e566f00fff1b83fffc0cfff0af7e13d73516fee7daf07f6d6ca3e566f00fff1b83fffc0cfff0af7e13d73516fee7daf07f6d6ca3e566f00fff1b83fffc0cfff0af7e13d73516fee7daf07f6d6ca3e566f0ffebf0ffd32f609d8e15902b07388953211d3c0ed8e4dbbd4f73eef05db5495bb796cfd67339b607e4ea5ed6f577e0389f9467aae3fc36eb9b346df24c3e57e39cae2b97a88bfb1ce7537d2a97f0bf37f8877ff8cf39bb3a290efefb7af00fff5b83ff3c07fff03ff9c03ffc7706fff00fff39675727c5c17f5f0ffee17f6bf09fe7e01ffe271ff887ffcee01ffee13fe7eceaa438f8efebc13ffc6f0dfef3dc97c6ff37df7df7dd0f9be4af15e636d4099ef2b9cd7a0b9b2e93694efdae6aeceaeb85e33eb797ddd5b9c9afdbdb67bf78d2e5f6ec975aca977ca7b3dad551b837715dbe4d9e2b97d2959a6f150bfff07fc5e0ff3e76ca03ffaf37f89f0dfee1ff9958f887ff2b06fff7b1531ef87fbdc1ff6cf00fffcfc4c23ffc5f31f8bf8f9df2c0ffeb0dfe67837ff87f2616fee1ff8ac1ff7dec9407fe5f6ff03f1bfcc3ff33b1f09fe35ed4f9b610f5a0a724dd62eb5cb7b8dbc5578d671dd5aa9755325df0334ff79b8b1df429df94537d6a6f3aef7eb3f5fc9c2ea46ebeaea1abdd1da8ee377353ae94b7e6acfb5b3f8fe318cfa75b3b3d2f694fbaf59f6a69eed4fb95cb5c9fdd7a4f7bdf5d64f00ffff00fff3517fce7ba57f34d39d507fee1ff9c837fff0efff0ef72b96797afe6d018f887ffaeeed57c534ef5817ff83fe7e0dfbfc33ffcbb5ceed9e5ab393406fee1bfab7b35df94537de01ffecf39f8f7eff00fff2e977b76f96a0e8d817ff8efea5ecd37e5541ff887ff730efefdfb6f89ff771f3e7cf8e8924d9b9a445460de3ace3597fc7efcf1c78703b4d194604f7ecfda6be35f93e3ca3e6df6e4b539ea7a3f5ba38bad7536feaf5dd32bf53639d3fb33f936bae01ffed5f7d9f98d0ffcf739d3fb33f936bae01ffed5f7d9f98d0ffcf739d3fb33f936bae01ffed5f7d9f98d0ffcf739d3fb33f936bae01ffed5f7d9f98d0ffcf739d3fb33f936bae01ffed5f7d9f98d0ffcf739d3fb33f936bae01ffed5f7d9f98d0ffcf739d3fb33f936bae01ffed5f7d9f98d0ffcf739d3fb33f936bae01ffed5f7d9f98d0ffcf739d3fb33f936bae0ff97e3ffe7bf80a500744074096f7655588a530ddd62747e7551b557d7fb9963ba34924d97c9339b77e6dceccbe6c23b3f5d5ebd44f4fd8caf75923e7789743da5bd70e66ad5fdac10a55e93d62beb7dd6b8d90dfceeecb87cd39a4cf19dd57c4e17fcc3bfe6857ff8877ff8579ff31dfe8fa811fee11ffeb316f8877f35f89f0dfe1f0dfe67837ff857837ff8aff3f00ffff00fff2eee4a1df8f77a740efe734ff00fff35a7f373b53b3ff8bf37f887ff3a0fffbf1effdf7cf7dd773f1cafb004cfd50dece25e73e8be464b87621b5b0f5bf271cfd5f4223aede5e5e57856d7c6e7cac5f51636c192be3c5e9bef6a3f572f35ad77bebff53ac2ffdb1bfc3f17ff8cc1ffeb0cfedfdee0ffb9f8670cfe5f67f0fff606ffcfc53f63f0ff3a83ffb737f87f2efe1983ffd719fcbfbdc1ff73f1cf18fcbfcee0ffed0dfe9f8b7fc6e0ff7506ff6f6ff0ff5cfc3306ffaf33f87f7b83ffe7e29f31f87f9dc1ffebec324dfa9b61e9b721f559176502df8d9d315b286a5dd5d5d97490a683ac7357203f8efcdb779a67fa2dbcf41b85f582d6bd74bdd7b12b1741177735f7d46bfd9cfccee7b4f6a7dd7e9b35cda5f1ee02a9eb7bf542dd7ca1769ca498ab06fff0bf35f8bf1f837ff877bef0ef7daac13ffcc33ffc7775e01ffe350efe1fe3e11ffee13f1bfcdffbc3ffe35c1a877ff887ff638c837ff84ff9d318fcc37f5707fee15fe3e0ff311efee11ffeb3fda2fc7ff8f0e16327b02ea03bac573672eb3f1d209767bbb8afa97b258f3b8cc9a71b3bf55c59e72b072d5d8c67ddb7aab589ebfabc52a7f37d76ae9b77baa73ddbd6babaf79bda534d3706ffd7f2c0fff538f87f9c87ff5d1ef89ff36c35d4ba6f556b1307ff8ff3f0bfcb03ff739ead865af7ad6a6de2e0ff711efe7779e07fceb3d550ebbe55ad4d1cfc3fcec3ff2e0ffccf79b61a6addb7aab58983ffc779f8dfe581ff39cf5643adfb56b53671f0ffc9e01ffe4f837ff8bf62f0ff9cc13ffcc33ffcff92fcdffd05acd3b106b8b1f3fdea2657ffcd22a7717749d4f1dbe774f853eecde2d7c5ede655df54cbc5d53a53dd1a3b5dae7a480ea377b3bfe7bf793ac59f7ba2ebe16a24d8353669ec2e8cee5c4d3d6f2eee7a0693afe672e755f56cf65c6b4ce66ad6cf3476bec33ffcc33ffc6f0dfefb5af0ffa8ade685ff63a50dfee11ffee17f32f87fcc0dfff09f0cfeef7de11ffee11ffeb706ff7d2df87fd456f3c2ffb1d206fff00ffff03f19fc3fe6867ff84f06fff7bef0ff7afe7ffa0b587a80d2017e2beb36cfd5d786b71751cdf75aab9a6eff0eaa1ef80d9c354faaa1b9379a3adbf63fe59af26cea5cdd8b67207f8bba9d254db7b1db97d156efd5bdd65a678eb730f8df6b86ffe7e6b73e57346d0dfee77cf0df1bfcc3ffcde0fff506fff0bf31f8f706fff03fe54af59fcd05ff7f36f87fbdc13ffc6f0cfebdc13ffc4fb952fd6773c1ff9f0dfe5f6ff00fff1b837f6ff00fff53ae54ffd95cf0ff6783ffd71bfc7fd2fda281b569d7e8a699cda590f26963f547c7d5dfe57a8d5ed5727ed6bcafbd94d43f41ecf27487f88c717b982e90cea6cb707bc9bb7adb4b4bcfcd95cb2ee5b96a29f6cae57b9a9ea1edb972e7faea394b71f07f1faf3ef09fe72783ff7b837ff8577ff8cf06ffc7839fe6da18fc673d9d96f313fefdfc64f07f6ff00fffea0fffd9e0ff78f0d35c1b83ffaca7d3727ec2bf9f9f0cfeef0dfee15ffde13f1bfc1f0f7e9a6b63f09ff5745ace4ff8f7f393c1ffbdc13ffcab3ffc6783ffe3c14f736d0cfeb39e4ecbf9f996fcfff417b0b4e8e6206d804c17c929ec99f89f444b63572fbac9aabe67c04ae60e74ad378dd53ce912bd7a20743fae5e6ccf5e8a5b7dd37c77493cabe5cad9733157cff66bd76d53631adfcebf360ffcc3ff157dd33cfcef6a4ce3dbf9d7e6817ff8bfa26f9a87ff5d8d697c3bffda3cf00fff57f44df3f0bfab318d6fe75f9b07fee1ff8abe691efe7735a6f1edfc6bf3c03ffc5fd137cdc3ffaec634be9d7f6d1ef887ff2bfaa679f8dfd598c6b7f3afcd03fff07f45df340fffbb1ad3f876feb579e01ffeafe89be6e17f57631adfcebf360ffcdf9fcb973a799ba887681259fd6a723d8caef8594be77f1656fecc5782ee59ab79134ce7e755103babeb5bedb6c94943cae3fcbb8b271dfaba1f29f6ccef72d6f99ac3d5d233d2f5a0f96bbcd6ac7eb7e7dbd9b9fd4cb1c9a68bc1bda7757563aaed5c8fa4a9b2b43973dd9741e21bfefd3cfc7fcaef72d679f8ef73c03ffcc33ffcc33ffc5783ffc71e347f8dd79af0ffa811feb3c1ff632ef8f77d398dee1dfee17f32f887ff9bc13ffc5783ffc71e347f8dd79af0ffa811feb3c1ff632ef8f77d398dee1dfee17f32f887ff9bc13ffcff3ca67f016b630e924eec349fe6dce1494deb067739ab9fcb39f5e2f24c3eb7e767ffcdcfd7d83307e5669bfeb67e57ce46ca57c715dcd7d4ee62a6f3e7b4d6589dabf3eed2ddacf5a697ee027e0b837ff8bfe207ff8fb9e17f9e83ffd71bfcc33ffc673ff8f706fff0dfe53f0dfee17ff283ffc7dcf03fcfc1ffeb0dfee11ffeb31ffc7b837ff8eff29f06fff03ff9c1ff636ef89fe7e0fff506fff00fffd9efd7e6ffe75fc0da36a07605f0d3ef14e216a3fb3750d3619e6a6e36e24bb32b6ba117c6e780ee2dac3becdd6536991eee14e72e6e77a9be66edaeec41b7c735dfe9fb390dfebf2c837ff83ff39dbe9fd3e0ffcb32f887ff33dfe9fb390dfebf2c837ff83ff39dbe9fd3e0ffcb32f887ff33dfe9fb390dfebf2c837ff83ff39dbe9fd3e0ffcb32f887ff33dfe9fb390dfebf2c837ff83ff39dbe9fd3e0ffcb32f887ff33dfe9fb390dfe7f1dbbfb5b68e7813c9f370beb7cea21d7dc0adbf95e3764635b5f055af5d4f7b4c975be8ebb3eb71aa618add5693cdfcf3f6dd7ad695ae7d4777adfe84deba53d9d9f694f9ce939d2bd75faea9f7a3c63ba3fff582fa3e4a3cfba179dd5cb3ae5d3bcdd6572ce6f584a35e01ffeb5be7bdfe885ffdee01ffe93c13ffc5783ffb9ae33f887ffb31efcfb981a0bfff0aff5ddfb462ffcf706fff09f0cfee1bf1afccf759dc13ffc9ff5e0dfc7d458f8877fadefde377ae1bf37f887ff64f00fffd5e07faeebec4be77ffc27089da8b7303de49b7acf68d9c4a44bcd5d4269a3b675afe4d8d85be4d9e4a8a0bce579d07c6fb1c66a5bedafe9ed6aaceb335d00dd39dae67ab637f87ff4837ff87f6d2cfcc33ffcc33ffcc3ff6b0cfee11ffefb3af0fffa1cf07fbc8916f5877ff87f8dc13ffcc37f5f07fe5f9f03fe8f37d1a2fef00fffaf31f8877ff8efebc0ffeb73c0fff1265ad41ffe7fdffcbf6891d3c91d582768b29a4bfd5f73d83bdd6edc7d3a1dd5479f4fbf7ac85d3ecd59736b0e9dbff2b9d592d6a5e670fd6a5e05dc59b7dfb5968e9db1dd01eff6b61b3b7fcb359de5da9bea9ece8ef6b0b57a0ea6bda91a753cf96adef33772756fe01ffee11ffeabc13ffcc3ffbd5fb7be9a7b63f00fff6efecae7560bfcc33ffcc37f32f887ff6af00ffff07fefd7adafe6de18fcc3bf9bbff2b9d502fff00ffff09f0cfee1bf1afcc33ffcdffb75ebabb93706fff0ffd3e7ed2f60b9859e16e7363ffdfb95576d02f0ad724cf96ff3f5df147579dd65d4d54bf3159494fbb56bb11d9ff26c2ec874094ceb3ded55cd339dd72beb57f3dff67c7ba65ddecdd8337b92f2deec8a866d6df8877fe70fff47acb91d83ff6cf00fff5ded330ffcc3ff6b72c03ffcbbfcf09f73a8c13ffc7779e17fd636c54e796f06ffd773c03ffcbbfcf09f73a8c13ffc7779e17fd636c54e796f06ffd773c03ffcbbfcf09f73a8c13ffc7779e17fd636c54e796f06ffd7737c49fcbf9c891c382aa07eba7f3bd315d5d8f4a38d3a3bc7379b53ff0dce9aff8caf7575fe66b7fe545b8dad1acef59b7c1d1835a66ebad3a4b10a8d5addd33abfbd60f46ce8a7d6d47ff734694cebe4acd6accf696f5c3ef5abb1faefe6d6dc4e535d17edc7ad8bae755abb748634c7742ed2977c8a877ff84ff9e11ffe350efee11ffee11ffee11ffee13f19fcc33ffcc3bfc6c2ff71a70bfee1bff607fff09f34692cfcc37fe7bb89877ff84ff9e11ffe350efee11ffee11ffedf86ff9ffe02d6d1d82de8ca6fba25e89e19ebf2a65a0a929bff1ce66adfec4afd3affac76d77b8aed2ebe2bf9afc49e75afd69bc6afe6d67dea62d35eb88bbedbb3cd9e6ced2d737506ff3b83ff5dec59f76abd691cfee11ffee11ffedfd6e07f67f0bf8b3deb5ead378dc33ffcc33ffcc3ffdb1afcef0cfe77b167ddabf5a671f8877ff8877ff87f5b83ff9dc1ff2ef6ac7bb5de340efff00ffff00fff6f6bf0bfb367f97f39271288a9f129a69ac69f87b71b3b733cb359498febe3fcecfa7131fa7cda59c301ea7ad05897df5d16359fd6a8fefaac3faeb7ee331d6c3597bf8b49ebeffad05ce77bb77e5ac7c53b5d9aab6a3a7f0bb46a4afbadbd74b65d9bcda5f59afce71cfc3fc6e8f369f0ff690cfee1dfe9837ff887ff6cf00fffaa0ffee13ff9c17f6ff00fff5a07fee7fce71cfc3fc6e8f369f0ff690cfee1dfe9837ff887ff6cf00fffaa0ffee13ff9c17f6ff00fff5a07fee7fce71cfc3fc6e8f36957f98f7f012bc1a63e67e2e457c7d37317f34c9d6eac1bd75a9396a9cf49d3edd3fd7661a7fbb414936aeabf69e9cce5e8d66acaa15aa74bb4fa24eddb4b472fd474f95f394b6e0db7e76eba042616a635bb9a7b3a13f00fff756c6bf00fff2e4ff7dcc53c53a71bebc6b5d6a465ea13feefb5c23ffcc33ffcc3ff3caeb9e01ffee11ffee11ffebbfaf00fff9bdcf00fff2e3ffcc3bfd6857ff8df1afcc3bfcbd33d7731cfd4e9c6ba71ad356999fa84ff7badf00ffff07fcfff4b123141779b4f0b5b3f35a63edf7cba03adfe3a96eae886a9ae69f155b3023b69d8683d7bd7fcaea6e675f96acddaab7b76e6d65df7b55b6bd53ec1e07a4c87ddf5e974b8783da735878eebb3eb2b7d114c1a4f1de9ac74756b7cf271fc4d31d3e5db19fce738f8877f170ffff05f9fe11ffee11ffee1dffbc03ffc6b2cfcfb7cf00fff350ffcc33ffcc33ffcc3bf3e3b83ffe3e1d9f505ffbe26fcdfc7d44fe707fff09fe2dd3cfcc33ffcc37ff281ffc7b1af817ffb4f10bac3e2162a1d70d7d8b4216933d4cfcd7787cd5d08d3a5e42076cf5d8c7bae1a6fbf7de862d5f400dde2dcc6ea5aa4cba8ebb95b97e9227675ea394ac07787fbf65cd749b5562d35367d09a44bbfebc5b1d15d1a0eb28f8b2f2fb7be496b62c55d5ec9af1b837ff8877ff8877ff8aff3694eb5c0ff633cfcc33ffcc37faa0dfff0ef0cfee15ffde1ff7e1efee11ffee11ffee1bfcec33ffcc3fff1300effbebed306ff479b07fe7d8c7b867ff84fb5e1fff7c9ff8b069fe6fe349e2bec8ae867b7490aabbb5c525dcd7bd3ec60d705acf30a7c7719a64b44e1711ba63d3950534d5763f24be30eaeeaab7bd15d901bbd69ccc1eb7a3bf7f4f4d15afac5916a55533f3d0fee7cb81a5547f24f631ae7e693e61aabba6b9e6e4c63d5e0ffb0f5e01ffee13f8fc13ffcd7cf3406fff05ffb807ff8877ff8877f9f1bfee11ffe8f8758f8f77e691cfee11ffee11ffee11ffee1dff507fff00fff87ad07fff00fff790cfeaff1ffeefdfbf71f1d18f5591355f1eee0694cb7284e9c6b34359ff2b9e65d1f9b43e6b4a7c39b6aa475d29a9dbe0ef2cdb8e64cfdab6eb79f2eef39967a767e4e7b3a3f355efb7317945e22693d554fb73ee90cbb9e3f0e5faa5dae2be375ce6948eb98d8867ff8877ff8877f5f1bfee1bffaa96ecde7f44eb9ae8cd739f8bfcfd38dc13ffc6b0ea745d74535ab6ecde7f44eb9ae8cd739f8bfcfd38dc13ffc6b0ea745d74535ab6ecde7f44eb9ae8cd739f8bfcfd38dc13ffc6b0ea745d74535ab6ecde7f44eb9ae8cd739f8bfcfd38dc13ffc6b0ea745d74535ab6ecde7f44eb9ae8cd739f8bfcfd38dc13ffc6b0ea745d74535ab6ecde7f44eb9ae8cd739f8bfcfd38dc13ffc6b0ea745d74535ab6ecde7f44eb9ae8cd739f8bfcfd38d7d09fcbffbf0e143eeeab82e7a827bbb09dd7cf2d385ea2e9bab3526df6ee1d3264d6b7bd52fe952fdcfae6f772175ebdc5d009a6b5ab3e45f3524ff8dbe9bddfe3cdc797977fd6dd671bac4de4af73bf36539e9ebcecda6aefad55af00ffffa0efff00ffff0dfe5827ff887ffe7fc922ed50ffff0dfd5dad655bf5a0bfee15fdfe11ffee11ffebb5cf00ffff0ff9c5fd2a5fae11ffebb5adbbaea576bc13ffceb3bfcc33ffcc37f970bfee1fff7c4ff4b1dacc5d45145a86f8da97e35be8a49794e1f8dd3854dd055ab736ef1a6c53f7dce1fd5a97e6e4da6babab6dd5a253fd5a539abfebabe4e43d2a97535d75937c5d4b3e4ce55ad97d632e973e765b3b7299fc6a75c690dddde684feedfb64db0761cd6dc7a5ea67dd639f83fac0ffc3fd6d55cf00fff751cfe3f8d7763f0ff580ffeefc7e01ffe532df8877ff83f8ea93ff887ff6d3ef8bfd703fff0af753517fcc37f1d87ff4fe3dd18fc3fd683fffb31f887ff540bfee11ffe8f63ea0ffee17f9b0ffeeff5c0ffe7e3ff450ba5c3a3c51d68baa813949db94b24d5ab31e9206deb6dc077b1b5cf2e47d5e8d62c5dba6a5b3fd5932ed1aacdd571f14ebbaea3ebabd3dc5dd0eaa7ef9b3571fdb81ce7bb3ea7f5a85f2e297f775968cfae9e5b934e57975f0dfee11ffee11ffee1dfd571f1f00fff1a5f63e13ffbc27fd60afff00ffff00ffff0afb9bb31f8cf356b1cfcfb77f8877ff887fffaaecff00fff2e16fee11ffee1ff7c9f7ce13f6b857ff8ffbdf0ffd2815203aaa542d325b2d9b4f3b95be054ff8c9936cbe5e8e0701a527ed590fada00939eaf5c92a9d774b96c2ef42bda2bd8aea65b9f1ae77c12d4ae7e8d5173ebd85d3c2e46e3d2b94d7a6abfdaff64e9cbf3f679fb138aaad1e5857ff887ff4f39e01ffee1bf37f83f621cfcc33ffcc37f1703ff9fdee1ff78a8efe2a6fafa0effc7438c1afcc33ffcc37fcd01fff00fffbdc1ff11e3e01ffee11ffebb18f8fff40effc7437d1737d5d777f83f1e62d4e01ffe7f6dfedf7df8f0e1a3167062bb4dd0c3d38d6fe7d5f766b73fdbe636dd6d941eec9aa7bb9092a64967eabfeb79b3a64e53a7d51da8676d73196df678aaa16b92626ee3e7bfcd7ac69ee3fa9ef2b875df98aeffd46fe7ef344c17cba646cdad9f959d6e5fe11ffe3557ca07ff3b1ff887ff3a06ff3907fcc3bff3bd19fcc33ffcc37fa757c7e0ff7983ff479dfabcf1817ff8af63f09f73c03ffc3bdf9bc13ffcc33ffc777a750cfe9f37f87fd4a9cf1b1ff887ff3a06ff3907fcc3bff3bdd96f91ff9794ecb4dac8f9a33ea9b1c99cdf6decfc4975aa4fade936e7fc4dc0d35717bbf69916d82daedb545d37d743355d5bd555354d70a6da937587aa5b8f34eef64663924fade9f6ba83a0ee6fcda339bb33a7f5aa8eeedcd73aee5c3a105dbcfabbb5709745f54d796e1c546dd379af3d9d9ff07f3cd482fffb71f8877ff8877ff887ffc9e01ffee1dff7e16ac23ffcc33ffcc33ffc6b0ef887ff2e0ffcc37fa7e1ca1cfc1f0fcfa70ffcc3bfbec33ffc6b3d17affef00ffff00ffff07fefff5be2ffdbbaa9dd266a605abcea533ff5f0d4b93aaf9bb8b9207411f4d3e9eb72543d53dd74205ddf13885daf5adbd9b4e13587fa260d49b3ce757d6acd345ff3e873aad1d54c17a4cba13a34e67ceeb4697ca7ebfc6d4ebd18527f696fd379b8e53f2f1fcd5fb5c13ffc771ae01ffed5e01ffe750efee11ffe0f3b0efff0afbae01ffee1ffb10efcc37fcd05ffbeb633f83fa256f5857ff84f39aa9ea92efc7f1a877ff8575df00fff5a13fee1bfced579f8ef0dfee15ff53983ffaf83ff77efdfbfffd81d72d79c26d934a33e2e3e35e8eadedeeb019bf46ca0763ad23a4ccfce5cae73dcf5e10eacd63b414b791d789acbd5d6f114e7ac3b4f6e3d9c5f5acb74f1d5da1d3c6e3dbbb573b537e7c9c524bbc2c9760d9dafe699ce1dfcc33ffc1f6d2ff00fffeed919fcc3bf33f8bf37f8877ff8877ff8877ff8bfd70dff9f7ce01ffee11ffee11ffe6b6df8877f97479fb586d301fff0dff502fff0ef9e9dc13ffc3b83ff7bfb3df1ffad135f45a64d70073fbdbb8dd03aee5deb541df5df02adb1e9c2e9acdb68ed295d442956d7abbe7787c1ad5fd25e3f55afd3a2f3536c7780537cdd8f34a77e097e3d8f4987faa4ba2eafe676fe497b3d13ee8bede3f0259434eb9cf349bc74ebe97a3f4cdf53cea4c3e9aaf99306f8877ff8cf73f00fff2e7ffd54bd4e8bcec3fff190dbf927edf00ffff07fd85af00ffff00fff6e4efde0ff68e7e01ffe5dfefaa97a9d169d87ffe321b7f34fdae11ffee1ffb0b5e01ffee11ffedd9cfac1ffd1cec13ffcbbfcf553f53a2d3a0fffc7436ee79fb4c33ffc3fcbff4b2ae412e8c13a3fbb439d16202decf9ee2e2877c0abaecda2688d7a494c174bd2a33e6e9db4b61bdfcc259b60aaef6eedf5b3fa3a28f480bb354cb5dc414ed02798a60b2dad91d6716bb4b9d8dd05a417713a576e2dcedfe6ecb4d7d8d4a73b934e5bd530d5827f9f0ffee15f739fcff07fb435e01ffeeb33fcc33ffcc3bfcec3ff616bc3ff71e7ab7d3a2dae06fcc33ffcf7b5e0dfe7837ff8d7dce733fc1f6d0df887fffa0cfff00ffff0aff3f07fd8daf07fdcf96a9f4e8bab01fff00fff7d2df8f7f9de82ff6f8f6611d245a05637a9dbecee606b6c5733e5d4df8a73da344f82d86d76a74b7b70795dac1e82a439e5ab39ebfcf96f8c763d6a4c82cd5d8209461d733a3767cbe99af6516d3a735dcd2b6b9d2ef4f3f99c4b7befccf1709afefbb167fef4a595ce97d63bf3381ff87fac9f72c13ffcc33ffcc3bfd709ff599fcec1ffa3c1bfcf09ffc7438ff00ffff00fff9d565713fee11ffe8f87daf00fff9d56f8877f7dd631f8cffa740efe1f0dfe7d4ef83f1e7a847ff8877ff8efb4ba9af00fffbf04ffef3e7cf8f0d18955e7eeb0541f1593fcae1ec694d71df6d3d7e577bd4df5b75a26ffae4ef24bbab77abb1a934fa775b357ae8f2b75a65edc2597beb49c8fc2b5d9a38e8967cfd6665f36795e1b07fff00ffff00fffd9e01ffebbf79a27f9b9bc2e6eaabf35f8877f5763f281ff5c33bdd73cc9cfe5757153fdadc13ffcbb1a930ffce79ae9bde6497e2eaf8b9bea6f0dfee1dfd5987ce03fd74cef354ff273795ddc547f6bf00fffaec6e403ffb9667aaf79929fcbebe2a6fa5b837ff87735261ff8cf35d37bcd93fc5c5e1737d5df1afcc3bfab31f9c07fae99de6b9ee4e7f2bab8a9fed6e0ffcbe5ffee9f204ca0bb84eef916f32c509b7a6e5ceb9c1a527e179336b1d3e9f24cfe5dbcea4e97ef16d66a35a6f679fed4f7ea73d6ab3ee7788d7135a60bcfd5719ad3fb33175aad573554cdba1e5dcde9124f175917533fd5dcb97536e54f06fff00ffff0dfc5c03ffcc3ff1175c33ffc27ff2e1efee11ffee1bfbec37f7e877ff8af71350ffcdf1bfcdfc7c0ff11f3c33ffcc33ffcc37fafd3e599fcbb78f8877ff887fffa0efff91dfee1bfc6d53c5f13ff2f6eb08a5180ea4175621c6ce7b89b4f1b5a7d1dacd382a5bccf6c4e7d76b5152a7729391f7781e9f319efd6d499f3d1d8e9b2743ea946da0fb73fdb0bc4996a7597793d2fae57b5abbdbaf174766e7f26b0eba31bdf5e069da6740ed457c7e11ffe9d4faa01fff0af9ae11ffee11ffe9d36f8bfcf05ff3b83ffd9e0ffb0b9e01ffee11ffeabceb42ef00fffce27c5c2fff1a019fee11ffee1df6983fffb5cf0bf33f89f0dfe0f9b0bfee11ffee1bfea4cebf24bf3ffa2459c30f7e992d6622a4637d0d5d5c3990e4c1ad34390343d6be922d44d739bace3ee6057736309de8d25c874dee9d0cb66d27ec6b95a2e9fab7bfe3ba79a33e94cda9d2948694fd37ebb98ed17aa8bd167a7d73da74b48f7247d69b99cda63fa84fffbf1eedd8dc33ffcc33ffcc3ff61ebc2ff31c6c33ffc5fd1dee5817ff8773da64ff8bf1fefdedd38fcc33ffcc33ffc1fb62efc1f633cfcc3ff15ed5d1ef8877fd763fa84fffbf1eedd8dc33ffcc33ffcbf837f5b17fe8f31fe1dfc3fc49fe35f0bff2f9b4d9f046d63ae6ca48bade09c9f7a08dd61acf1ceea864d07ea1cab9f7ab0d44f6bb90badce6bee04690220d57460a4dedd1ad75cdd214b39ddc5ab73a7fdf8e38ff112e9ce8af6e5ce47d25ee7f53c4d6bedf6327d49b82f27b7cee99c559f8fe18b3a9db3ee8b42d76132f8877f35f887ffce5f3fe11ffed5ef66f00fff1a0bfff00ffff00fff877d9f6ac23ffcc3ffa3c13ffcd778f8877fadab63f513fe0f1ba739e11ffef513fee15ffd6e06fff0afb1f00ffff00fffbf25fe5f3a619d889ad035a39bdec5e84676823bf09d4f7a5620bb8bce1d7acde3f2baf99acf5d8c4ebf833be94adaf480e945a9b63954e94c4cebefbe44d2e5e32e911ae762b48fc9d2e55ccff27406dc7be7377d896c8176312e775a47f8877f67f00fffb52efc1f760efe8fd85f7a867ff8d7fcf07f44fdf00fff37837ff87779522ef887ff3a5f73c03ffcc3ffbd0ffc7b837ff84f359dc13ffcc33ffca767f8877fcd0fff47d40ffff07f33f8ffedf1ffa2055232078ff34d80742252aed3af83de35dac151f3ba98f3dfeeacf315a6aa47813de7eaa6a6cbd3597779d6fada975bfb1a57b5a60b311d18b7f6b5b7aaadfe6ccd4155754f1784bb84ddb94d9f5583ee6b07fbc44ccd992e81ea9ff29ccfe92cba1ca99f941ffee11ffee13f69807ff87779e13feb817ff887ffd9e01ffe53aeda8b9b538dce07fee1ffcce33eab06f8f71ae01ffe5d5ef8cf7ae01ffee17f36f887ff94abf6e2e654a3f3817ff83ff3b8cfaa01febd06f8877f9717feb31ef887ff0dff2fa9210d72906a817480b599b410e922516da9c1e92272a0ab5ecdbd0144d7a7f3d5cbd46d8cbb686fefe7e1e8204f17660746ca932eb774f01ce46e6fd36154dd35ff9423e9a9f0a826a73369d1fe1d27ba76dd999fce58f5a95a5cad6ae7bf9debf6a9e69818d3dcf07f582d6e7d3a5ff8877ff8bfef13fe1ffde1ff5edf71c03ffcc3bff605fff73eaa07fee11ffee11ffeef73c03ffc574df00ffffa0efff0af9ae01ffee11ffee11ffee1fffe1dfee1fffc847ff877be0ffc7ff8f0e1a37348a2357112abcd1dc7e3023b816aa97e77b874e1a7de3a5de942d3cba03bf0dacb26ef14a36369af1250ce6fa35dc7bb79778636fd4d206abee90cba5ebab39dce91ab9be2de3517eba61f7dbffde8bf8babb53597d648f1f00ffff07fd89c5d1ff00ffff09fe7e11ffee11ffee1ff78a8ad63292ffcc3bf8bd131f8877fcda535e01ffe93c1ff6173767dc03ffcc37f9e877ff8877ff887ffe3a1b68ea5bcf00fff2e46c7e01ffe3597d680ff2f8fff77efdfbfffd86dca24b603bf1b9f36577d93960ddc6ebe3b301abbd1dae93f9f5dadb4c169acbb14b4b6fa25b8a7cb7dd367ed6d02b2be7739d4efea97d0950b7173cea6b3fbcc99562dd52f8d6fce8c3b77c9e01ffee11ffe55e73907fff03ff5597b83ff6cf00ffff00ffff00ffff0dfe7503ff8877ff887ffa413fe1f7de0ffb0f3f00fff492ffcc33ffcc33ffcc3bfabb335f887ffe403fff0ffd37cfd0b589b439644768553de6e6c5b334159fb398ec3d6544bf06aaea423cd259b60ea2e9ccd6531e5da68d0b174c9a57737bebd345c1f4ed3ede7f6db9ddaef699b73daf5e3f26ebe08366bdbd9d5cbb68b9d7ccfbcf07f583ff8877ff79ef4c33ffc4f06fff0df19fccf35e11ffee11ffebb7e6a1cfc5fd3a063f00ffff00fff5d3cfcc33ffccf06fff0df19fccf35e11ffee11ffebb7e6a1cfc5fd3a063f00ffff0ff86fcdffe02562db085b4bb0452fc7611af1c9429cfe97fc66c2faed3b68be934746ba235a7da1b5d53bde4b75dff94cbe9d95e001b40b7b6398f1d9c29e7f622bab2fe354e6be91aa84dbc6d72ba0b05fee1ff4a9ed3d7e981ff9d4e570bfee15f6bc3ff352df0dfe7847ff8dfbe4fb99c1ef8dfe974b5e01ffeb536fc5fd302ff7d4ef887ffedfb94cbe981ff9d4e570bfee15f6bc3ff352df0dfe7847ff8dfbe4fb99c1ef8dfe974b5e01ffeb536fc5fd302ff7d4ef8ff32f87f39c5d5624ec429de09d5319d9b4c45763a546bf5d3b8ea7ffe8664eda7e6a86335eecc577beb7ad29c2ec7f95e7bd1f7d3a7fad61fa723ed41351d4f3026dfdacb06580779cd91f65e9f6b4c67ddf938c7dde591f24f004e97f31433ed87eebbcb91b44f1780e6857ff877f53516fee779f83f6caea4c3bdc33ffc6b5ef87fcc91b4c33ffcabc13ffcd71cf0ff3806fff0aff9a67a6af07fd85c49877b877ff8d7bcf0ff982369877ff857837ff8af39e0ff710cfee15ff34df5d4e0ffb0b9920ef70efff0af79e1ff3147d20eff5f3eff3ffd138429d9b481ea939ea77c5d8eb3117741a8cf559b2e8a0ee2a4ed1ceb6ab9f73ae6d6234191b4baf8aebe3bc44e5fad9f7aecd6d559b78757f6377d89b8fe93e6cddaa51ccf8ce97cd5dee551bf4dfeab06fff0affa6afdd423fcc33ffce75aeebd8ec1ff6e2ef9567ff87fdee01ffe555fad9f7a847ff887ff5ccbbdd731f8dfcd25dfea0fffcf1bfcc3bfeaabf5538ff00ffff09f6bb9f73a06ffbbb9e45bfde1ff79837ff8577db57eea11fee11ffe732df75ec7e07f37977cab3ffc3f6ff0fff9f9ffe917b0a6624eec746853e19aff0a549b8d9d74a59a93feba169d9e4dfd8d866e1d3a2d57f26dea5c59d3cd4577faaabe7838df3dfedbae2e5fd7eb749653cce43bed678ddd9e7707f3b3e6f6cdd5abbe35b6f3817ff8afbef00ffff00ffff0eff3c03ffcc33ffcc3ff7d7ef8f71aba75e8b45cc9b7a903fff00ffff00ffff00ffff00fff7dde9407fee11ffee11ffeeff3c3bfd7d0ad43a7e54abe4d1df8877ff8ffbcfcbfe8c6d4022eb8da399f0ed93976fbaccf35b7fae998e64c3a5cd3dde1573daeb6d3aaeba235dc5ae99aea7a4c1748d5910e905bebce2fc59e3ad5a7f6bff173efba8e2946d7d7edcf955c6e0fd574cfebb35bb704a9f399cefb99bffe993ccdbbddd37a5e1c43ba1ef00ffff00fff5a07fee1bffad4fee11ffe3527fcc33ffc1fd607fee1bfc6c23ffceb27fcc3bf33f887ffae06fcc33ffcc3bfbec33ffc777e29f6d4a93eb57ff8877fcd09fff00fff87f5817ff8afb1753d5e8ed0500a4cc9ebbb2e9ec63b98d242b883942e1b7710b5af7a40359f33dd4007aaf3d3ba6e133547aa3df5a49f4eab9b73974d7d763d76f5d5b7fbb270f5ab8f7bbe62e98ca806d59ace888baf73dbcbb2d67017c4477339bb1c55b7d3ecd6b6fa75eb0aff8ffdc13ffc6b7c9d83ff4feff07f3cd485fffb1cae7ef571cf570cfee11ffee11ffee15f63e11ffee11ffe359f33f8877ff8877ff8877f8daf73f0ffe91dfe8f87baf07f9fc3d5af3eeef98ac13ffcc33ffcc33ffc6bec97c4ffb769613a419aac7ea698eadfbdd71aaea9a4d55d18dd82771bef3627d5abfecea78ea7de1d346a4eaf5bfb2e479d9fb4b9b3e02ea254abdbd7ce375ddab5e76edfa6fe93b6b34ef2d1fad53fedbfead7e71a5b6b7467f1ddf085d9b1d3d5ecf4a75e545f17e3eab977ed4773c33ffcc33ffcc33ffcc33ffc6b1ef8877ff887ff3a06ff8f79e01ffe6bde33cf64f0ffa81bfe0feb0bfff7faf5b9c6d61af00fff6af00ffff00ffff00fff2927fcc33ffcc33ffcdf8f7dc9fc7fdb25d6a42a2835adf3755c9f5333b57e5aa0740835d6e9bd02fc747174b95c8ccbe1347439dda1ec7c529cae4577f16efaecf6d9d5dd7c897479aa6eb74fd3c5b7bd1cf433e5ad734e6bcae5be5c92bed3afd6773a1cdb698fb586cb01ff7eade0dfe787ff47ad2917fcc3bf6ae872c23ffcd7faf00fff9d4f8a837ff8571ff8877f1703fff00fff8fbae1dfe787ff47ad2917fcc3bf6ae872c23ffcd7faf00fff9d4f8a837ff8571ff8877f1703fff00fff8fba5fc3ff4bb7696ed1dce729a23bace992499b7dc6a5ba69e3cf183d6cdd219d0e93ead3459f005460d4dc5a6a8c5b4bb75eddda24ffda5f5dbb3aae7ee93d5d34ee8cbcbcbcc45eea3aa45add17c30686d34fe33ab0f492ecd6597b706beb7aebce4fe7abe7d8f1b339036e0efee17ff30efff00ffff05f9fe11ffee11ffee11ffeb516fcc37fca09ff5e63f2857ff8efd606fe1f0dfee11ffe8fbb38f8877fd777fa847ff8ef9ee11ffee11ffee11ffeb5d6ef9dff6f35a00a514169f3d281d2053873680d353d04f540a5435a733b8d576c5a4077e8abce1f7ffcd1e674fab466aaa3b56a4e9daf07251dc2d4875e186ecfbb3de8f2d5b99b9deba47a5d5f2977d5a4da53ac5ada4fe793e653beb4afdaa35beb6e8f52dd8e81c433fc3fead677f83f1ec6e0df1bfcdfe7721ae0dfd7847ff8572da96f5717fee11ffee1ff68b4d77c4e2ffcc3bfcbaf63f0ef0dfeef73390df0ef6bc23ffcab96d4b7ab0bfff00ffff07f34da6b3ea717fee1dfe5d731f8f706fff7b99c06f8f735e11ffe554beadbd585ffaf8bff77efdfbfffe836400ba839089c25bf9a77932335d15d6add25960ebfd670daba9a9a2fd54e975257abdb0b8d73735dcfaaaf1b4fb5529fdbbd9ed6b5c6ab8e2bb5ba4bd85de69b33acefe98b70bb2ecfacd32666e207fee11ffee17f530bfebdc13ffcc37fef07fff09fe23a837ff887ffc3c6c23ffc4f31f00ffffa0efff05fd701fee1bf1b4fb5529ff00ffff00fffaa27c57506fff00fff878d857ff89f62e0ffcbe2ff450f4afded2dd7ac0aba7daa4ffd5461a9e97af035976b64e33b1da6aa5dfbd03cb586ae99d64eba5d1f9a576bba318dafeba776fb337f75d353ff6e6d9cb975726b9c2ea3b4ce099674e1ba5ebafd55e05c6cfd4997cbe6f2bd59e5c8ad87cbdbbdab7e67ae5767ca31fcc33ffcc33ffcc37faae5625447cd03fff09ff4695df8877ff8877ff8877f1d83ffc3ea7706ff8ff5e01ffe5523fcc37fadabcf2e4fad01fff00ffff05fb5a8be8dc1fff110a763f07f58fdcee0ffb11efcc3bf6a847ff8af75f5d9e5a935de92ff177730b401b7f05acc6d8c1ee46e316a4d7d76bad29c6ada5e3049e3f999e23b506aaceb37f5af2038d3dedc7c575b7deba73b70555bcdaf39745fd27a6a8eea9fceda69dd788232ad7577de3af0f4c24897f106c6eeecd57cdd85d4e9d44fbdf0e01ffeeb27fcc3bff608fff00ffff00fff8fdae01ffee11ffe35bec6c23ffc2783fffb9afa0cff8f06ff7e1cfee11ffee15f6bc33ffcc37fce553fe11ffeb547f8877ff8877ff87fd4f61afedf7df8f0e16375ee40757e7ac8eba26f725da993f26efcba03e462abafeb37d570f1aedef652ec724c7d5cc9b9d538e5de68dee678762faf9c410566dab3db6f143bc836f1ee597b4d71c97fb3ae29dfd5f9d4cf69f00fff1bcdf00fff1b837ff8877ff8afcff00ffff00fff57fde01ffe9dc1ff7e3ef5731afcc3ff4633fcc3ffc6e01ffee11ffeeb33fcc33ffcc3ff553ff8877f67f0bf9f4ffd9c06ffd7f97f990e8f06a722ae598d4defe9f9ace3c04af9aa9e73ec8c73fda5c35be39d36e7f3da83a7b9dc214f31aabdcb59d7e5fcec6aa43a6e3da673a0399d3e77a8b58ee6d1f974a6b46eade9f26ecede954bcd9d191def009e80d71cd537e973b9932ffc1fd107fee1dff508fff0bf31f887ff3326e5857ff87731f07f3f0ffff00ffff0efe6e1df8fc33ffc5fa903ff5e8fe6807ff8df18fcc3ff1993f2c23ffcbb18f8bf9f877ff8877ff877f3f0efc7e1ff97e7ffe12f60b9642ee966aee6ec7c9cb91c4ed75d3366319dce8dbea9977410dd61711a35bf0321d5d86a73bd549feeb73953fc79906f7f22b1f349fde898d6adfad4ae5c74c9476b241d09cca97e3a67ceafd6e9f2aadeae86d3bfed07fe773ae01ffe537df8877ff887ff140bffc7a823e570baaac13ffc7706fff0bfd537f502fff0efeac33ffcc33ffca758f83f461d2987d3550dfee1bf33f887ffadbea917f8877f571ffee11ffee13fc5c2ff31ea48399cae6a5f23ff77bf80e58abbc63b71d5576d7ba8af5c264edf74c8dc81d53c29af8e4de636386dd2bbe1b2ade31bbf6e4fdca19c34688d94cbf5791cfe22567335cf0b5febb99c9b7556cd1d444957c742f24b1ab586e367d29458d47cd3f9d19a9a03fee11ffee1bfab09ff87ed4535c0ffa3c13ffcc33ffceb18fcc3bff371fae01ffeb53efcc3bfab0dfff00fffdee01ffebb9e522ed7e771c03ffcc37fd500ff8f06fff00ffff0af63f00fffcec7e9fbaaf87ffffefdc7eed06a1227d835e8c47607305d44d53f2d40f577fa35df46db74694c174b3ab09ac3d5eaeaa7bd71f1d3c175bf01abf11da85b9f645d0f13182997d6dd409cceff9437e570b9ba8b3ed59d2ef52d231b8d49b3cb590dfee11ffee13f69837ff84f06fff00ffff00ffff00ffff09fc6ce71f8f71a931ef8877f35f8f73e750cfee11ffee17fca9b72b85cf00ffff07f6ff00ffff00ffff00fffbf57fe7ffe0b58eeb0a4c3372d5815d5c5a99f6b5cb569c309cccdc64f077e7b38d25a6da17c2b98ebfbe9f30cacdd3a697cd2aa7d5c598f5aa73bf85a23d5aee329afd650cdd31e6fce44e7d7f5bfb99c5c9d74f6277ff87fd402fff00ffff00ffff0afda537ef8efe3d4affac2ff1ca76b51dfe11ffee11ffee1ff7e3ce5d51aaa19fee13fe987ff47adda07fc1fad1ffcdffbc2ff1ca76b51dfe11ffee11ffee1ff7e3ce5d51aaa19fee13fe987ff47adda07fc1fad1ffcdffbfe5ef8ffb6164ac2eb5ce79f8a697cf2a97955435d80e9425153ff6ef15d8e3ad75d54554bda78ed291dacae87e9f2d5f8cd65e37ad6bacedfcd75eb92f6dbe5ef2e836e3d1ca09a2701a37be3e63457b73ea98f7a9652ff2eded54d175c8a73f9351efe0feb0ffff00ffff00ffff77e750efe8f981bfeb30ffcc33ffcc3bf6aaf73f00fffaa0bfee15fe35537fc7b83ffc75a4e27fcc33ffcc37fad0dfff05f7daa16f87fcc73bec33ffc3b1ff8877ff8877fd55ee77e6bfc7f5b276aa224520b76625deeba806e5c73b91c4e5307561a4fa04ebd74e0a483727b76ff8ee97441756be97ad35ed24170351d30499f5b13a75dd7ac3bbc6a694f27ad1a5f6b4cbd76e744c7f4dca6fd4c75b5befab9debafde92ea57449a916f8877fd5987c5d2ff00ffff00ffff00fffa937f8877ff8877ff887ffd3d719fc3ff606fff0efd6c169867ff8877ff8877ff8877ff8877ff887ff7b9daa19fee1bf1afcc3ffe7e4ffddfbf7ef3f6a3267aee8e4ef44ebe27507cf35587d9fd1bb194f87f4ea1ad4987439a95f8aad3a524cf7de6977fdbaf54817b29b73fb9c3e55c3b45e133c092817efe66a7f2ece9dd1748eddd9577f1dab795cedcd65a2f34e63f27706fff00ffff0bf35f8bfcf0bfff00ffff09ff2c03ffcc33ffca7d8aa23c5c03ffcc37fef03ff5e6335f887ffd4fbd6e0ff3e2ffcc33ffcc37fca03fff00ffff09f62ab8e1403fff00fffbdcf97c6ffbb0f1f3e7c7401fa9c129fcf6aa9c9e4930ed804df99c7c19d346d0ef7d453076ad78ff399724e8726e571dab7179bab55f324bbb23fd345d2d5db5e3aa7a5f579d77c01b8d854cf69de9c814d5faae5f673fb6dca2b9752d2b6a9bfd1a906fff00ffff0ef34381ff8877ff8877ff887ff4d3df8877ff8877f7daef3d5e01ffee11ffe750efee7fa1b9d6af00ffff00fff4e83f3817ff8877ff8877ff8dfd4837ff8ff2df0ff5293b9a2d3d859e02c529fd5f79cab3eda481d77e27511dd62d7b99a775a3437eee2a73e539cf339fbecd6395d2e9a5bd74cd7a0e6da1c74ade50e67d79333dd679dd37d727bebe29c0eed5d3538ff4e4fad9ff6cdad758d534bf1ba8fd5eae5dbad95cbdb69807ff8d71af00fffdd18fc7faa39c5c13ffcabc13ffcc3ff715703fee11ffee11ffee11ffee1dff5ee6aba5cf07fd81c4e03fcc3bfd6807ff8efc6e0ff53cd290efee15f0dfee11ffe8fbb1af00fffbf17fe7ffa0b58ae5137a676fad4cf5a7c9b4be334ef14f38cd634afcf9ddf26affa9c9a3b9da7b93a1bcd0ecad4739ddbf86ee73b6d57e7d5ea25ef2e6cb74ec7b24fd5d3cdb93cdd25a81aeaf3cbcb8bbd743667c669acf5ae9c1d37dfd5817ff877f3f07f44ed2e0ffcc3ffe97b1afcc33ffcc37fd204fff09f34d67af00ffff00fff5d1ff00ffff07f3ce8e9e6e01ffee1dfcfc3ff11b5bb3cf00fffa7ef69f00ffff00fff4913fcc37fd258ebc1ff97cbffbbf7efdf7fac839bcd4b62eab81672e3570ed7e6b06f0eee79d8f54fb8a59c2e878bd9f6305d185bc0a718cddfd599ea4eb1daa3eb4d7b771a3bdbe8d33cf5a27696d6452f435d53d5e5b4a8eeedfa6ebe989cf6ed85d49d153706fff00ffff03fe5d2f19a338dc33ffcc33ffcc33ffcc33ffc7706ff473b06fff09f7a9ad651b574eb9372e978cd99c6e11ffee11ffee11ffee11ffe3b83ffa31d837ff84f3d4deba85abaf549b974bce64ce3f00ffff00fffbf67fe7ffa0b58e7a4164c9b75faa4835ffdd382750756cdd5da5c02a95682a2f3db6abeb276d53ff5d0e5703113a4dd3e4d703b3d572ec5e9a07e1cbe98742eed8fd3a36be4c6d2fa759a261f5753e35cece6bc77fbd969ae36ed31fcc33ffcc33ffcc3bfd303fff00ffff00ffff0ef74c03ffcc33ffc6f6ab83aa916fcc33ffcc3bf33f8877ff887ff540bfe7d1df8877f1d837ff8877ff887ffcfc3ffb7a76377283579d774f551319a4f7ddc78574b759e31ba38daa37e4e17c3617a4cfd761791faa479cd5bf3274dc92608dd7ea97e9d731ace3add65e4723a1f07435acfd47fcd73fb6dc7732ced5d7dafbd6c00d47add6555d7acd33ce575bd389daa775adfaa0ffe738ff07faf01fee15f7de01ffee11ffee11ffe534ef8effde03fd7529df07fd818f8877ff8877f1703ffc7833ef887ffa947f8bfd700fff0af3ef00ffff00ffff00fff2927fcf77ef09f6ba9ceaf91ff972ab2feb8c570077f3acc6eb1dcb8aba307a8db648d715a6aaff5b96aea3623f558eb6b9de9823b7d354ec7bbf9f370a50b3ac1e7a0ebd646cdad8703cdf5e3ce925e36dd25a06b99f2d5cbd7d572355c7eb574a156d8754fd2994817665af3eeb2550edc7e54ffaa0bfee11ffe0f9b5f0dfee11ffee1bfbed771f8877ff83fee62e11ffee1df6bd2381defe6e11ffee11ffee11ffee11ffed51ffee11ffeef7badcff0ef73c3ffe373ed09fee1bf3ec33ffcc3ff6175d5bedcb8abf37be0ffc5355205d5a0f330ab88b4c16e61eb671599c0aa8b5f6bd50577b5ba85acf94f5fad99f4a6cb45d7437bacbee900e905967ad179b5dbbf71a9dadc65e32e46f7aee3e98ba1d671b9ba4b45e35cefdac3bbe6b2afcfdbcbc09da7b416aae7aca3e752fb74ba36d6ed955e429b4b5f0dfeef6b26bdf00fff3507fcc3bf5b0ff87fcc05fff05f3fe11ffeeb38fc1f6dadfae9fa807ff8bf19fcdf3fc33ffc3b837ff877dae11ffe935ef887ff9a03fee1dfad07fc3fe6827ff8af9ff00fff751cfe8fb656fd747dc0ff57c8ff870f1f3ebac4ea9812b8f17468f560743eee2025bf2b7954b79bd34d9f72d47a53dee4eb2e5cf799fad41add9ce6eab44f978582d1f593623b7854f333fbe27aeaa0ead63c9dbb498fea7231d3fe6efb486be77adbf4aadae1ffdeb7d69bf2265ff8877ff887ffae3efcc33ffc3ffa5dc9a3bae11ffee11ffedd38fcc33ffc3f6a87ff7bdf5a6fca9b7ce11ffee11ffebbfaf00ffff0ffe877258fea867ff8877ff877e3f00ffff0ffa81dfeef7d6bbd29affabeb8c62a509db985af40ba8daa31faa30de861ad7e6eaebed7f9f3791aabf1c94f73b83e5d5e0743d2a09b5e3fd3e1d7e70912fdac7ba3fe9de67af1aae604ac6a4c17b8eaef2e8baae98a75e75ccfb29eb1b4f7676cfdac71eedded5df24d3ab59fbaaefa5c63b506fcc33ffcc33ffcc33ffcc37f32f8877ff8877ff8877ff87f8cad9f35cebdc33ffcab7ef8bfd70ffff00ffff00fff5ea7f603fff73ae11ffe9d06f8bfaf01fff00ffff00fff9f9fff87bf80551b74cda6265d7c8a7135524d57471bd343a779359fda267f1a738730f5d7e54c36019c2ea6c9af8ebd56c7b406e7dc66dcd57235373a3767c0f96fcea5cbf3ecfa4ee7c4ad55a7c9edd59621f887ffab3ae01ffee11ffee11ffee11ffee11ffee11ffe750efeb366f8877f67f0ef6b6e74c2bf1f83ffc3ce75f9d318fcc37ff5817ff8877ff877793acdf00fffcee0dfd7dce8847f3f06ff879debf2a7b1d7f2ffd32f60a544b52115d82d763a749b45da8c750d3d7be9a50bcc1dce4d6ea769ca75e5106dfb743a6e3ff5dff37407f87cdeeadaccd59c4ef75443f555fdaef7a46f7316b456024e6b4cd67d2175eb7dee99ebfff4bd62535ff00ffff00ffff00ffff00fff5a07fe8fb56ef887ff941bfeefe7e11ffee11ffebb319717febd16f887ff49c7f40efff03fe9827ff8579fae26fcc3bffa4d35545fd57f33f8df1bfcfb79f8877ff887ff5f8aff9733e0dce473e27cae07c02d784d56e7d5a7e63c9fd54f7377635da375cee970b95ddf5ddd94efb47481d4397770752d6b7cd5e6f2bafeddf379f96abe1aabe34e978354d72de554736723d549e771dab7b4b6d5740d6b1df54b319d6fd599b4bb3573351c77a97677d6ea3bfcc3fff90cfff00fff475b3b19fc1f6d4e35f8877fa7338db9faf00ffff0ef6bc33ffc9f06fff00fff8fdab46ff8877ff887ffae7632f83fda9c6af00fff4e671a73f5e11ffee1dfd7867ff83f0dfee11ffe1fb569dfbf04ff2f6e01d2c2744076873681a78be50e71ba506a4cd5e62e8069e13b6dee724940e8dab935ab73d365aa3aabb60ac7f6205490dca17587319d07a759f77db35f5d3ed7878bd75edc05a7f9d37c67ba96eef9cca97bdfed637aaf3da535eb74d431ddcb6e7de01ffe6beff07f5fa77b3e73c23ffcc33ffc57cd2e0efeeffb9c7c5413fcc33ffc7b1d750cfe8f87d88d36f8877fadd33d9f39e11ffee11ffeab661707fff77d4e3eaa09fee11ffebd8e3a06ffc743ec461bfcc3bfd6e99ecf9cf00ffff00fff55b38b83fffb3e271fd5f4b5f3fff33f41e804a6c3703e3bc19bf8b4d84e43975b17d16de8a60fd753daf87468bafaaab1db7097c7d54e7da67a29d6d5eefcbb3c4e67cae9f676d2bed9e38fe1d24bf5b65a3bbf6e1d754d26cdee3d59b75e1d631bfee01ffe53ad94c7e984ff23cec1ff316ae872c33ffcc33ffcc33ffcc33ffcc33ffcc33ffc4fb5e0ff3e37fccf5ae11ffedd38fcc33ffcc33ffc3ff6d2e5847ff89f0cfee11ffeb3c1ffd7cfff4f7f01eb1ca89f1d1855ccf953633f868be6cca9b9d54ff33bdb2cbefaba0d4d0b38e5d258ed456bb8b8ba7eae6e0253e7dd3a77a0573fb716eec03a6d4e63ade5ce411dd7dcd35eb8339572b933e5b4b93c9b33a79fdd7ed7d85aebe3c7fc2594e2524f9d76ddab9a0bfee1dfe9522d4e1bfcc33ffcc3bf8b857ff887ffe34ea3c6c13ffc6b3df83f1ef23b837ff8575ff887ff4e3ffcc3bfd3a079e01ffea75c1aabbdc03ffcbbd85a0bfee11ffee11ffee1dfd585ffc3f6a55ae01ffebf56fe5f5213d59cc8a9c1e9e0a8bfcb539b720d4ce6e04c351408a7bf1e087758f530d65cba21e910bac3a13dd51caa69da4bade90e8ffaa50b5d75a683a6962e1f8d73206b6c5773fa224963693c8158d7b2ebcb694e9ce89aa6f3abb5d305389d25f8877ff8efc7e11ffee11ffe751efee1bfcec3fffdb81afcc3bffabb3cda9fea990cfefbb5817ff8af35e01ffe9d1ffc3fe6837ff8d71af00ffff00ffff07f3fae06fff0affe2e8ff6a77a2683ff7e6de01ffe6b0df8ffe5f97f7187211d905a5c0f9b5e06d53f2d82e64b8bd15d566eb3dc85a1796a4c35d5bc8951bf29c6f5abe3494fcd950eb6bbe01458cd3fe975b93b5d7a71b8dcdd45942e9a4963b77e7a1e5cacae5bd299d6a1f6edce9cceb9bd9acce5ee7a729faa37f506fff00ffff05f63e01ffe5d3cfcc33ffcc33ffcc3bfd6e834c03ffcc33ffcd7dee0dfeba9b9e01ffe6b7df887ffd317feeff5c13ffcc33ffcc33ffceb5a5683ffe3707a5dee4e17fcc33ffcfb98bb1edebf7fff5117c8bdeb4551c7b5a803d6e5d0b13aae79ba86b5aed648799de60982496fea53eb9cb5743ef9384b35a65a5dafdd3a4d3a9c8f8b4d7d271d5d5e97bbc6bb9e272d699d12dc9bb39cfaeb744debe0cec666efba5ae91dfe1ff5a51af0ef353a2dc9e01ffee11ffea7bc2e778d877ff84f79e11ffee11ffed39833f8cf3a9c0ffcc33ffcc33ffcfbfee01ffee1ff687574795dee1a0ffff09ff2c23ffcc33ffca73167f09f75381ff8877ff8cf6bf3eec3870f1f35d97410ba4bc38d4f174c577bdabcc94fe7bb5e37755dbea9f7ab9ab6d641e66cd2dbe5e8e0d3f904bdab95f2549f3af7ccd9d45cee6276393775eb7bd7d7cbcbcbf1e38f3f3ee49b746fce84eb617bbe27ddf00fff2916fee15f35c0bf5f4ff8877ff8bff775f9e11ffed5a7cec17fee4bf5c23ffc6b5f2ecee5dcc4c23ffcab06f8f7eb09fff00ffff7be2e3ffcc3bffad439f8cf7da95ef8877fedcbc5b99c9b58f8877fd500ff7e3de11ffee1ffded7e5ffbdf1ffad165707b78049e054f87cee2e8cb4589a231dcc6e9354df34ae35ea9aa836b5e922dbfaabae6e2ce94e716e8ddcc1aebe6e0db4fee95fdf9d8efa99e66bbeb49729b66a723d39ed295f7709ba7a9af376f9eadab97aee72d2b9b4c6ba575dae695fe01ffee1ff533ef83f1e7a827ff877f54ffffaee74d4cf340fff8f73f00ffff07fc478f8877ff887ffaa09fee15ff54de3f00fff673ef83f1e7a827ff877f54ffffaee74d4cf340fff8f73f00ffff07fc478f8877ff887ffaa09fee15ff54de3bf04ff2fceb98a700b99046f3626c1eb0e91135cfd3bf8d202abfeb450536d97dfd577fa1280dda14b6b977ad81c80b446dd9c5ed6752daa8f5ee6f5d3e548f1ced279ad71f5e7367efb0dd4eadf5da87aa655b7f630e54d975fdafbee3971a8beba16aabbc6c1ff116ba471f887ffeadbe585ffe3ae3efcc3bfe648f1cee01ffee1ff319fd3e1b4eb33fcfb35827ff84fe3f00fffd5b7cb0bffc75d7df8877fcd91e29dc13ffcc3ff633ea7c369d767f8f76b04fff09fc6e11ffeab6f9717fe8fbbfaf00fff9a23c53b837ff8ffadf0ffa2c55c63f502e88aa4454f82baf8eed03aaddd65a250bb43a331cedce5a7b59dcee49f743a73f069cfba1e296f02afd67297835e6a49f379d9298cee62d5cb58ad5b335de3b4f7ef8c5eb786eefc397ddd5e4c67a46a75eb79c5de990bb68ed77ecff1eaa7b9ce4ff8f706fff0afcff07fdf2bfcc33ffccffae11ffe934e67f00fff6af00ffffa09ff8f3dc23ffcc33ffcd738f87fcc757ec2bf37f8877f7d86fffb5ee11ffee17fd60ffff09f743a837ff85783ffaf9fff771f3e7cf8a8c9d34675096b412d9ec4689c9b737aba77b7d19d06d7579a773da69a697eaa39697035525c77c0dd7e6ff2ba3a678e6e7dd29998d6f18a6f77065d9e2e66d27b8e9dbd6f74279b743b9f5a577939e7dcde74f96bac8ed5718d817ff887ff597732f887ff64f00fffcea7d6857ff8dfccc13ffcc3bfd79f62527ef83f0ed507ffc758aff33b0ef8d739a707feef6ba438f887fffa0cfff00ffff00fff878d7731293ffc1f87ea83ff63acd7f91d07fceb9cd303fff735521cfcc37f7d86ff2f8fff175d644d70da99acfed47117d735981623e5718ddedeab2ed5e234e841eb0ea35bb86d1f5d7f53ef9adff5e5f2e8dc19a37b55df136cd52fada96a48cff53d9d9b691feafea7bedda5d3e571ba3abd47d3976a56bd9b3574eca43e6b9eba8fda9b6ad89e3df8877ff887ff9a0bfe8f751ff00ffff00fff2e1efee11ffee17fca5d35c23ffcbb9af00fff290efee11ffee11ffee11ffee11ffee11ffee11ffee1bfc63b83ff5f867ffb4f10aa931e6c07ae3bf0b5789d4b87d389ec2e09f5adf936bec9afeb5d2d1dc804748dab9f6e4cf3ba7a5a2bad619dd383eb6073f06a5e6749d739b6b960d2fa75e7abfa69cc466f8271fb25e7fcbb1e5583ae4dc797bedf3e7ffcf1c7873177314e5cc03ffcc3ff61f3b939f8877ff8f77dc13ffcab16f8ff14573fdd18fc1f7735e11ffe3bed351ffcc33ffcc37fe70bfff00fff9ff2b94fad05ff87ed0dfe8fa8bdcec1bfd700fff00fff478c857ff8877ff877b1f00fffaaf56be1ffa52be00eefb639cd919a3adfeba5e00eb13b186efef67efb3748a7c5723da78b2ce573dadda157ff74509d8ffa2b00da9fae6dba646badaa454171ebab9fae7707989e81aa519fb547775e3af86bcfa98f74be93b975ed804f674675a86f77e14c177efae274e7c2f9682df887ffc90ffee1dfc53b4d4e0bfc1fd107fe1f7de0dfd7807ff8877ff8877ff8773dc33ffc6b1ef887ffaa05fee15ff3e918fc1f513bfcdfe7857f6ff00ffff07fdce5aacff00fff677ccd0dfff00fffbe06fc7f7dfcbf7bfffefd479de80e8e83bcc6bbc54d106f6bd6ba29dee577e60e6cba9c5cad74a8539eb45e9d9ec9929fae913ebbc3bd0170baa8bafed436ebd39da5497bb7479aa73b87ddb99af668ba943bed9bbcdbbd48978bfa75716af00fffe75ced01fe8f07fdf00ffff07f6ff00ffff0dfeb990cfee1bf1afc1f36bf33f8877ff8877fed05fee1dfcd25ffa947f8cf79e1bfd73319fcc37f35f83f6c7e67f00ffff00fffda0bfcc3bf9b4bfe538ff09ff3c2ff277bd14293d502e9f2701baf714e543db4e7733ac4daa8dbccd34f75699ddb6fb8d577d5dd41a3b56bac7befd628bdabae0e40b7865a3bd5d0b9da975b17f57175cef7ba8e534e7796ae5c8267bc4251cf82f371406ff66b1aaff954a3c639ade7bb3b63557bba1cd4c7ad1dfcc3bfcec13ffc773dc0ff63ed1aebdee11ffe6b7d8d837ff8877ff8877ff87763f00fff3506fe3f8dc13ffcd778f8877ff8877ff887ff3a0efff00fffd7b4bb1ee0ffb1768d75eff00fffb5bec6c1ffef8bff17b7a04e4c02276d6a6adc09ea2e14d7fcb376f6a035d382d5bed3e1aee62e2c37df5d5eaa55f5b839cdab355597ae8103c91d9afaecea76178aeae934a68b5f2f2d77e81ddc696ddcbe6b4fa7cff96fac565fe5e29cbff9a65edd25d169acd6adbd5b0f575b7b867ff887ff23f604fff00ffff00ffff00ffff05f7de11ffeabc13ffc9fbea957f8877ff8877ff87ff4ab3aeb3cfcc33ffcc33ffcc33ffcc3ffb306fff05f0dfee1ff1c57bfeaff39f97ff7e1c3878f29b12edc76a1dc7c671df0e9b0399ff3d9e574b5d2819e8075174dcadf5d68aea7ae97697f524f4ed7b69fe49fce84cbebf4685fa97e77f037e7355d445ba05c6fce7feaadc69ecf93ee4ec3b6f6551ee01ffe5d4f4e4f9dab06fff0aff39dc13ffca7faf00ffff00ffffa9cde9d4ef8877f570bfee1dfe5d17c5d6ca705fee11ffe0fab47fb4af5e11ffee11ffef539bd3b9df00fffae16fcc3bfcba3f9bad84e0bfcc33ffc1f568ff695eac33ffcff96f97fa903ce418bd4b19bdfede7f627bc92b09adb3dbb315737e551d345d53a757c03eae9e3d6e1cc537faa0ecd59eb6e61acb5b427cdd3f5940e9df659759efbeaf4692f55ab7baed6f5e8f64cc76a6d8d49ebf6ec99d433a06b9cd8d178a76502b9e670ebb4f1777d4d9ae1ffb157f8bfd7a7bd54adeeb91afcc33ffcc33ffcc33ffcc37fd503ff87d5e0d669e3effa827ff887fffc0cfff0af3ae0ff5e9ff652b5bae76af00ffff00ffff00ffff00fff550ffc1f56835ba78dbfeb0bfee11ffef333fcc3bfeaf85cfcbf7bfffefdc7e378dcc0e95d9b3aff1d45bd2ca65c6a29aeb3e9003bdd9d3ecdd1c5259d57f54f3993ae49936ad7f8fa7e45abd6d73cdd9938e7536d973701b9d9bf2effa61737af5aba3d7b46eb666fd2ba776bac35a71ce95dfb827fdfc7563ffcfbf550ed690d6aacfa74f9e11ffee11ffee11ffe3517fc67ad5a5ff3c03ffcc33ffcc33ffcc3ffac1ffefd7aa8f6b40635567dbafcf00ffff00ffff00fff9a0bfeb356adaf79e01ffee11ffee11ffee13fdbcfff04e1e97cfb3cffbdcbe900a5a2570e768dbd1366003c3779aae53475cfa98fa4efca85d5d9f652d9e4e9faaeef0ed0a9b63b7435ae5bd7ee42e8cedad4c734972ecbeecb6973363b2ed2594f6bee2ce5747aba4b46f56eb83dfde11ffe5d2ef8877ff8877ff887ff2e0ffcef0cfee15f0dfeef75d767f8cf79e01ffe5d7ef887fffa0efff0ef62e0dfc7387df00fffe773b73ef00fff9dc13ffcabc1ffbdeefa0cff390ffcc3bfcb0fff5f36ff2f2aea5cfca9d0cdb7dbec74b9d478b59bcff973e651a01cc4673eadab0b999e6bdee9c06f40520d6eae3b90ae0f8d53ff4947cd79e6abfd74b15aa7ee53cdedfa4ef0b9b396d642f5a4b5abbe697f52bfba3e6ede9db994637bde35b79b7375742d756eaaefce3bfcc3bf8bd53af00ffff00ffff00fffaa0bfee15ffb837ff887fffb77f8877ff83f6c5ff00fff5a13febd16cd0dfff0ef6ac3ff6163ea38fcc33ffc3fc66a1df8877ff8877ff8877fd505ff17f9bffd05ac4da269015558d7c4f4aecfdb8d75393b585cbf9a6fe37765acce39fdb557e79bd6a3d3edf24ebd755a36b9939d8778fa2dcb6eed5c8dceb7e64df19b5ce96c6b7e7756a7f5ef78eaf67bc385d3e8de9d7fca07fff0dfe54e06fff03fbdc33ffcc33ffcc3ffa32ffc1fb606fcc33ffc1f6d1ff00ffff0ff1857e3e11ffee1ffd37cca07fff0dfe54e06fff03fbdc33ffcc33ffcc3ffa32ffc1fb606fcc3ff57c9fffbf7ef3fa66653e1e910bcc5a553fd4feb36b55bbc6ef3368bdc5d88c7e10fea6643dca14b79929eed8579652debfba4a5eb6f9a73e7a6eb67da87b436cebf8375ca5373693ed5e37c9375974da7656369adb507f8cff1aeee71c07fea6f9a83ff23ae07fcc3bf6a877ff8877f9f0ffee13fad8df3877ff887ffc36aa9ef9396aebf690efe8fb81ef00fffaa1dfee11ffe7d3ef887ffb436ce1ffee11ffe0faba5be4f5abafea639f83fe27ac03ffcab76f8877ff8f7f9be56fe7ffe0b5853c1b47949449aef16a53b74d5d24554e7d4a6cb6193bf83793ab0dde5e32e9aaa63d29b7438bf6d4c77d8d358b7c79b439eea3a9f2b975ad2a4b6d5edf24c7b7815fe1adbd9747e26ceb6671dfee15fc7e11ffee11ffee11ffee11ffee11ffea779f8f773f00fff497367f00fff4907fcc33ffc1f0fbe6e6e931ffee15f75c13ffcc33ffcc33ffca79af00ffff00fff6eeee53076069d3f4e5cf573b19a272d66b21adb59f239c7ea7c37d669389f6becb40e49531daf1ad206aad6eee24830d44f1defb4a53390fa48a66b57c7a7baeaa3fda4cb547575eba63ee9e2aafa74ffb54f8d49fbe034779772d5a031ba4687e9ab5bafda0ffcc33ffcc33ffcc33ffc1f0ffae01ffe9d9fd6efb4c23ffc3bcdf00fffd51ffee11ffebdc13ffcc33ffcbb719defc63a0de7738d857ff8d73cf00fff5abfd30afff0ef34c33ffc577ff8877ff8f7f6b5f3fff35fc0fa79c01cbe5a3c1d0ef5d14dd0e72aded9b6f66decfcf7445d0f5dae4e97d3d6e54c7ed5b7e6ed604f6b9f7a986255c366ad92b99c3ab6dd0b37afb993e64e835aaad38d77b5bbbd49ebd75daa536f3577fa74f552bfaeb65b0ff887ff6e4de01ffe5d5ef887ff293ffcc3bfd6d79c9bdaf00fffaa410dfe8f87fcf00ffff00ffff00ffff00fff6e3de01ffebb35817ff87779e11ffea7fcf00fff5a5f736e6ac33ffcab0635f83f1ef2c3ff97c1ffb7ce2125bc02df59c40157e36beedb4256919a3735a273ba099bc5e82ea2f3f9e5e5e521de5d6cba29db0b723af4da93ce7535eb98e67075d4aa9fd639c7ddfaa8ee2e7f77a1a5be5dfeeaaff135b7db731defd6c7ed55d253e7756d9c3eb77ee9dd8de97ee97e4fba741efee1bf7ed63ae738fcc33ffcc3bf1afc1f77f3ba364e9f5bbff4eec6e01ffee11ffee11ffee11ffee1ff5e77ca01fff00fff8ff3f00ffff5b3d639c7e11ffee11ffed5e0ffb89bd7b571fadcfaa5773706fff00ffff00fff5f36ff2fdda1389f9da82460129416a02e7ef54be0564de9525080b466024ced1c578de7dab843a57db84d49ef9ac71d6ce7ebd6c2f5ecfa4c9762bdb06bcfaac5adb54258d73ced6bba10ab9efa9cd64df5bab3d3593acb5a4bdfd33a3a2deef2757a3767a8d3ea72744cc3ff61f3c13ffc6b2d7d877ff8877ff84f73f00fffd33bfcc33ffcc33ffcc37f971bfee11ffee15ff5c03ffcbb1ee03ff79ad62bc5c0bfaf0bfff00fff479c837ff89fdee11ffee1fff7c1ff4b4d9216707b89249f6e53742e69d9e8ea0e4aaadf81e53647e7d301769790f6a436cdb97eb556d2a23da87697a3dbef0e7e7781de7e7b507dba7331c1e06a279feabb814ed7baae57ba409c86b4efe93cb9dad325e62e6df705e47aacf3f0ff5807fee11ffee11ffe8f760efee13f8dc13ffcc33ffceb3cfc3f1afcc33ffcc37f9a837ff8d718f8877ff8ef7daa2ffcdfe7807ff8afb5e11ffe6b0feaab3dbabedd3ae83cfc3f1afcc33ffcffbef87f398cd5c267925a301ddc2e971bd303a60d39e0a70d56bdd342ebe2bac553cdae7f5da3fadc1d0cd5d181e072d4387dd679edc381eb3e93b975d23539d7a0fef6a03b90ba66d39ebab5d2339400497d684fee127b375c6c9a4fcfb7ea4c1791f6eece6bf5d5f8b47e55fba41ffee1bf33f887ffc9e01ffee11ffedd18fcc33ffcc37fd50cfff00ffff07f8ee933fcc33ffcc37f1d877ff8d77a35d764f0eff5c33ffc7706fff03f19fcc33ffcc3bf1b83ffc3ff138449689ad345e8364a9bd471973f995e3a6e5ef5a485740754f3a8cece2fe5ad795c0e57c3f96d407097ab5a7759b80b3a1d76574701565dee127650d639a75df3b95c4973aa9dfad57354ebbb3dd7fc9b0bded5a839b4865ae2d2f9c13ffc57fff313fee1ff38e05fe7e01ffee1bfaf03fff0af3d3a5df00fff3aeef227837ff877f95caea419fee1bffa9ecff00ffff00fff3a07fff00fff7d1df8877fedd1e9827ff8d771973f19fcc3bfcbe77225cdf00fffd5f77cfea5f87f4970b8029350974717d68197c45ddd909afff44f8bacb076e62ea39adfc1a83ed36149bd9c39aa8e742924ed699debbbd64afbe134b938b7ef9a4f75a4d8d45377f6aadff95ce39dbfbe27c05cdd947fea6353577bedbee4ea39757e9bdee1ff78a80bfff00ffff00ffff00fff479cd739f887ff944f75c03ffcbb9e74bc33f83fda39f8877f57b3bec3fff15017fee11ffee11ffee11ffe8f38af73f00fff299fea807ff8773de97867f07fb473f00fffd5e0ffd7e5ffddfbf7ef3fba85d2e26e5e0b4fbec96703dcf68248f1dd7cf599fcd4b7fa777da4b5e9d6a3eaeef6a7fa543d5dfda423d5777e93feb40e35e7a4cbf5a0b96affe98c3eb376db73f9317ca11c836657fbd975d8f826bdf00ffff00ffff07f8c7eea5bfde11ffeab9ee380ff9407fef338fcc33ffce71ece7aaa03fee11ffeb32ffcc33ffcfb39f887ffda4b67f00fffaa5d0dfee15f63e01ffee11ffee11ffe55b7f600ffb987b39eeaf81af9ffb65b949a5cc7eb587a57fff33935909ad81ca0d4a8035eed36a7f3d321affe69ddd2c66f2e4a97a3bbfc9c2ea7c1ad8dabadfb962e1a8d4f35baf598f21cc761d7d74198c04ee7365d067a56d31786b3eea2d2ba35dfb4ce9b2f8c543f9d5df8877f571bfee1bffac23ffcc33ffcc3ffa326f83f8ee3807f975f6b6b5df8877f9d837ff87706fff00ffff00ffff0afbd3a5df00fff55b3ea83ff4ff1f00fffae57f8877ff70eff87d59e6ac0ffa30ffc1fb687df13ff3ffd05ac2ea03bd09b83a662eb5807a9c66f2f2d1d4f3aae5c5c9dbfb374b0a70b4ae3ebbbaec5b40fa95e17e3d6aace6d6aba98742e9c756740eb755a53cc7479251d53fd69af93ef149f7a743155d7e4afdae01ffedd5ad539f83f621df8877fd503fff00ffff00ffff09f7ca7f8d423fc1f56a38e271df00ffff00fffd31cfcc37fcaa5bef00fff9a03fee11ffee11ffe7706fff0efe2e1ff788875be530f5d8c5bab3a07ff47ac03fff0af7a36fcdffd13845de32a72f24b87623a602e4f7abeb289c9a68bcaf9a84d9ad3fa74bd4c6b7ad6dd5ea45bf8dcda74b53b0d699fa72fa2cd25a496d65763376728c5b8f17466d477caa3d6c5a53ed37e54adc731f3dbe5dfc4c33ffca7bcf03fe79fe2529ff00ffff00ffff00fff35aefac23ffcc3ffd1d680ff236ad3daf00ffff00fff4e03fcc37ff587ffd907fee17f8a4b7dc23ffcc33ffcc33ffcd7b8ea0bfff00fff475be397e4ff9befbffffe87d3a92657311310d5cff9a4cddcd826f7f993346eb4eabc8e4df9ddb3cb9320d19c2ea63b5ca9d7ab07d2c5a5c3de1dceb40e9bb1baa7533fd37e6ace3437593a031bdf692df4ecb835d533a6dc4efadc9eba3c9adfc53a6d3aa6e3f00fffceaf8b4de73269ee6cda97c926be3a5ff887ff730efee1bffae9e73bf88f39d238fcc3ff74a6a67d996ce2abf3857ff83fe7e01ffeab9f7ebe83ff98238dc33ffc4f676ada97c926be3a5ff887ff730efee1bffae9e73bf88f39d238fcc3ff74a6a67d996ce2abf3857ff83fe7e01ffeab9f7ebe83ff98238d7fadfc7ff3873ffce187e309d343325d0c572e812bf1696e5a802bb5b787a7bb2837f0d51c534c57dbf9bd5b5cf89b8399f63cf5acbd4cfd75fa92dec95fe7de852fb82b79a69ccec7edcfa475fab27631f5b3eabbbaee531df8f7b9e1dffba7b1c9e01ffee13fe7d1dace0ffe3fcdc13ffc5fa90dfff735f4d9bd3bff343619fcc33ffce73c5adbf9c1ffa739f887ff2bb5e1ffbe863ebb77e79fc626837ff887ff9c476b3b3ff8ff3407fff07fa536fcdfd7d067f7eefcd3d864f00ffff09ff3686de707ff9fe6e01ffeafd4fe9cfc7ff3dd77dffd90926d9f3787af3ea77c934f3a5cdbba29ffd6ea666c21ae732e665b773396b43c03f219b7d9af29877e6ecf4d9767e3979eebc571756d3af8aef675f532e86abeb5c1ff1175c1ffb51cfa7995139767e307ffcf1bfc1f5117fc5fcba19f57397179367ef0ffbcc1ff1175c1ffb51cfa7995139767e307ffcf1bfc1f5117fc5fcba19f57397179367ef0ffbcc1ff1175c1ffb51cfa7995139767e307ffcf1bfc1f5117fc5fcba19f57397179367ef0ffbcc1ffa3c13ffcc33ffcc3ffb51cfa7995139767e307ffcf1bfc1f51d75bf2fff32f60a5c57430ebb86b2e2dece6b0bff6607679bbf7e49f7ae9e23687ec5c8bcd21dcda46d77418aff69b7c5c9ded6f125ecd7d7ed6f3b5a9355907fdb46e57f6b51bef9edd6f183f63f09ffde11ffee1ffd10ffe731cfcc33ffc1f3617fcc3bff3bf521ffee1ffaac13ffc5f35f8877ff8877ff87ff483ff1c07fff00fff87cd05fff0effcafd4877ff8bf6af00fff570dfee1ff73f21fff025675aa497561ae2e96cb59e7a6d82d9c13dcd381edfabad2f3b451692da61c572fc47af8dd1e4e97f25b5977f97497cd065ce7fb96674a63ba2fd02e57aa9bc626db9cc767f613fee1ffad0dfe73dd77f03fc6c2ff9cd7f9c07ff687ff392ffcefe652bd290ffcc3ff3bf81f63e17fceeb7ce03ffbc3ff9c17fe7773a9de9407fee1ff1dfc8fb1f03fe7753ef09ffde17fce0bffbbb9546fca03fff0ff0efec758f89ff33a1ff8cffef03fe785ffdd5caa97e6eefe025612a505af007a75919d5fd5b50551e35cdeceafcb79e5c0d538f73ce54ca05fd1bcd57765bdeacf94d7e5d9ae75e77b3547ba445dbea437edd55bac83d3e32ebcad754cbbf9abfc6f5848e7d6f94d7bf15afe5dce2b5fd64ee7d6e07fc7ff951e7fcffc6f7b9ffa4fbe9a87efffe34ec35583ff1dff9bd86773fc96f8df58c7b49b877ff83f9feb78f285ff23fa773a52dcd7c6ff8685746e9d1ffffd3fd7fbbdf07fa547f83fc69a13ff1bdfc4ff550de7fb364e9f5fcbbf3e777e5d4ef8e7fb3fd5d9e84871f07f583ff887ff6e7e9b03fe1fe737bef07fef0bfff09fea6c74a438f83fac1ffcc37f37bfcd01ff8ff31b5ff8bff77d4bfe1ffe0256da94498036e1164d9f534df759f56c7e6bd4e94b07fa4abe6e7e1bafffa7afae495af7296f37e6d6b1bbd8ea9aa4b914ebe2ba1c55cf6bfb4c3e9bb94d7e576bba54a78b635ab3ed399fd6c1ad7377c1ff92fceb79fb1cfca77ee1df6b7479d3fe6b9d67f99f0cfeb35de17f93e397e47fca53f5f0fdeff376635f0bffafed33f96ce636f97f2bfc9f735fcaf7ff94a7ea817f9fb71b83ff796e931ffef9efffdd1cfcbf8effc9e03fdb33fc77397e49fea73c550fdfff3e6f37f6b5f0ffda3e93cf666e931ffee17faa31c5c33ffc3b7ff887ff9407fe8f9817fe8f23697379e0dff703fff71a5ddeb4ff5a07feefe7e11ffe6bbd9ffbfbf0e1c39da23a99c4d6220ad2664c73a69814970e4197efd4ddf534cd696edd3ca7d95d36a78f7bae966a6b5dd5d001e27ad49eb47e7dd77549f55c8dced7ad75da97a3d1bdf972f85c6ba8b9a7bc4ecf5baea9ebadfb22a85a5c5ef8877f5d9754cfd5e87ce11ffe752cc5c13ffcc33ffc5fc907fff00ffff00ffff00ffff0ffcc1cfcc3bf9b83ff4ff39b31cd9962521cfcc33ffcc3ff957cf00ffff00ffff00ffff00fffcfcc7d0efebff9c31ffef0432a5a93764dab751b9c6a6d729d9a6a4cd5d92dfe26b78ee9783a0cba119bfebb0be4ea41e9f25f9d9ff2a74357dfd31ab833b5f199f666fbbe9d539fa4b93b771b736bb959c36d6ea7f3ca39837ff8efe6e11ffea7da931ffcc3ffe403fff731dbdc4e27fce7dc3aa6e3f00ffff07fadf6e407fff03ff9c0ff7dcc36b7d309ff39b78ee938fcc33ffc5fab3df9c13ffc4f3ef07f1fb3cded74c27fcead633a0efff00fffd76a4f7ef00fff930ffcdfc76c733b9df09f73eb988e7f0efe5f5c91f3b316dc14708b951a3b73ba05d5c65c933547d2dce9abf1aac5d548b95c4d5d2bb706354e37570f50d5977424ff2dc4539daa530f7aea51f3b83395cec7a4c3f9b87d701789ab3ded95d3d59d996e1d367edd99eb384b9af5d9e9867ff8877ff8877ff87706fff0ff0efeefe6e11ffee1ff5e9fd305fff00fffc7da0ffee15febc0ff9f0dfee1bfe680ff23fac33ffcc3ffd16a502dae46ca05fff00ffff7fa9c2ef8877ff83fd67ef0fffbe0ff9befbefbee074dec36c615afefba894970974f37a3f3499a55d3d44b37a707721ba3b55f6beed0eb417531ee30a578053341a6f1fa9ef6bed3b85dabe93ca675eace5457bb5bb72eb7e6dcace5d47f7761aa9faec75516e0ff78d003ff3e5edfe11ffee11ffe27fd5a0bfee17fdaab941ffee13fd5817ff8d7f82eb7cb9bf2a798e95c5db574aed35cd55e7dbb78f8cff5d218fcfb9af00fff937ead05fff03fed55ca0ffff09feac03ffc6b7c97dbe54df953cc74aeae5a3ad769ae6aafbe5d3cfce77a690cfe7d4df887ff49bfd6827ff89ff62ae5ff9af9ffe6fbefbfffc14d4c0b9a623adf74109f39a06e012660527c078b1e8a542f1da26ee3534d57bfabd91dc8143fad79a7c77deaf3c6e79935e9209de2cecfcddebf6b2e92ee8badd698e6936d2e8b2e6efb65b65947f8877fcded3ef579e303ff479caf9f6eae8b83ff3937fcc33ffcc33ffc5f33f887ff2967377f65bccb0bfff07f35eefc847ff8877ff8877ff887ff6b06fff03fe5ece6af8c7779e11ffeafc69d9ff00ffff00ffff00fffbf36fff62f603961937507769abfb2c157dea71c134c9b03b5e9291df62b0772e3ab974be7f72c28671ffa59e3d2219eea55fd3a9762345ed7795a87a996f399a04a391c0bcf9ed96ecd52ec667ee2b5cb03ff7e1cfee1dfe9dce680fffb5cdbf72907fc1f634e1d877ff84f79e0df8fc33ffc3b9ddb1cf07f9f6bfb3ee580ff63cca9e3f00fff290ffcfb71f8877fa7739b03feef736ddfa71cf07f8c39751cfee13fe5817f3f0efff0ef746e73c0ff7daeedfb9403fe8f31a78ec33ffca73cf0efc73bfebff9c31ffef0830e7685b78b301dfcab75529eb33197ab83adabe17cb6b9ae5e3e57ec0af0db0bef97d2f396b927d8dfd2d205f7ecc5d1697fe64249f5a6f33ac5c2ffbd0ffce73c6fe5b78d81ffb91efcc3ff9407fe773ae0ff7e7ef30efff71adfc2e0ffd31cfcf7f5e01ffea73cf0bfd301fff7f39b77f8bfd7f81606ff9fe6e0bfaf07fff03fe581ff9d0ef8bf9fdfbcc3ffbdc6b730f8ff3407ff7d3df887ff290ffcef74c0fffdfce61dfeef35267bf80b5829b12bf24cd357ac6e94ab376de294bb7ebabaf5bdfea65c0750f573efd3864ef07407bed67cf6c04de62e8a6772d44f7deec69ead358da5d867bf3c538f696f34aecbb3897d8bb54b39e11ffe5f9ba37eea7337f66cad692cc5c23ffcc3ff7d0ef8dfc5d6cf9a0ffef7b5e11ffedfa2d6349662e11ffee1ff3e07fcef62eb67cd07fffbdaf00fff6f516b1a4bb1f00ffff07f9f03fe77b1f5b3e683ff7d6df887ffb7a8358da558f8877ff8bfcf01ffbbd8fa59f3c1ffbef66f9dff6fbefffefb1f921015aacdbbc54fe2b446ba68b45eaa73fa9d3edb43bd01f24aae4d6cd57abea775ea36573734d5d3ba696e5ab74d9ded17447dd71e26bf6d7e77c0dd5c8aedfcae9e9169af37393673a987ed45bfdd5ff8f7b9e01ffe9d46f8cfb5cee7a45d7b807ff8dfe4877ff8ef7ad0e73406ff8f39e01ffee11ffed3fc9577f8dfe5d8ccc13ffc6fb476393b837ff8af39eb38fcc33ffc1f5623fce75ae773d2ae3dc03ffc6ff2c33ffc773de8731a83ffc71cf00ffff0fff6fcfffc4f104e3074562f87cd62a86fb7f9d341aabecfbcbf1b2e3da767d297fa4a872ee998346e3477bea95ed2506b4ce07635dd1948905d854bf7a7837ca3378d6d725d8173eac3c5553f7d563fddbf29c6694ce6f6f38a2ffcc33ffcc3bff3857ff8877ff89f0cfee11ffee1ffeafbbb77f05fc79c9f7ebe837ff80f5ae01ffea79af09ff39dcff00fff93c68de6ce17fe67bd690cfebd16f887ffa926fce77ce733fcc3ffa471a3b9f385ff596f1a837faf05feaff1fff35fc072c2b69bb23908298fb304cdf66024a0bb79ad7533b7d8ce6fd2902e19e77b2cb57675f5dd1d880e6a976fbbf69daef3b06e0f7c97eb4abd2b36ad41a77ddab769fda71aaa2ff95d39675daff00ffff00fff75dcbdc37faf01fee1bfe6d167a7db8dab3ef8877f974bdfd3f974b1f00fff6e0efee1bfabb3d105fff0bfddbb494f970bfeefdfd3f974b1f00fff6e0efee1bfabb3d105fff0bfddbb494f970bfeefdfd3f974b1f00fff6e0efee1bfabb3d105fff0bfddbb494f97eb6be2ffc5059d8ebaa87a109d5077e8eb7bba68eabb135a75b9669369f355fb7440142cf5ef36d91dc27498544fda68bd4c5d6e57d7ad5dba9c123c9b2f81d4abc65f8550c7f56077f95c5ddd535d7b5d03e54173385e54737701540d6eefebf8544b6d73565417fcdfc7c23ffcbb5cf00fff2916fee1dff96a0faa01fee11ffee11ffeef0dfee1ff1c877ff84ff9dd1cfcc3bf6a827ff8877ff887ffe34117fcdfc7c23ffcbb5cf00fff2916fee1dff96a0faa01fe7f5ffcbf7bfffefdc72aac3a6e0ab8f9b3c16ec1d2066a9eeaafcf296fd255b5e973ade7344ce39d76973bd54abda6bc494bb7169d96ae27a77163badf671ee7a7e6b4766b34adb35bcfa99f745e9d1e57b7d3997c539d3aeefcafd4ee7cba3a6e6ea3436bc1ff630cfcdf1bfcc33ffcc3bfe6817fef0ffff0ef6a74b5e11ffee11ffe93c13ffcd77137b7d1a1b5e0ff3106feef0dfee11ffee15ff3c0bff7877ff87735badaf00ffff00fffc9e01ffeebb89bdbe8d05af05ffe025675a8c1e74f1d3f85d7b973bcfaa8b0ea7b8aa8b9ba66dcfce457c7ab36a7a1fad77eceb15363d55cc76b4ccaef7c9c1e7dae3d4c3d4d356aadd483f3518d1adfc5746742fddc39d4ba9bdedcb3ead67d74396abd744e3a1d696daba6349efa705c4e9765eaaffae833fcc33ffcc33ffcc3bf6a84ffd9e0ff78d05cdfe1ffb07af4b9f600fff00fff3b3ff8cf06fff09f74c0fff1900ffeef35c2ff6cf07f3c68aeeff07f583dfa5c7b807ff887ff9d1ffc67837ff84f3ae0ff78c807fff71ae17f36f83f1e34d777f83f3efd05acce7403d25cf2ebe2d56f0b9dce698d946b5b33f9b9839d6a6e7ad4b933afebd1f9aa961a7f0cfda8ef15ad299f6a39df5dcd2bf9a7da9b75eaf6cbe5b8729e3b0d9bb829cfb3fbd8f5dbc56d6bc03ffcd77caae57c7735e11ffe93d6fa0efff03fd5867ff8af35e07fee477daf684df954cbf9ee6ac23ffc27adf51dfee17faa0dfff05f6bc0ffdc8ffa5ed19af2a996f3ddd5847ff84f5aeb3bfcc3ff541bfee1bfd680ffb91ff5bda235e5532de7bbab09fff09fb4d677f887ffa936fcc37fadf1b5f0ffcdf7df7fffc339b1b59beff993e65f6beeb0d4e7b3fe74589d96a4fdad2e0497f7cadc9575dd809f72e981ba32afbe2ed6cd5dc9519f9fd1e1f44cb1f56c4ffd4f9a3a3dae5fb76efaf91ad335adfdc13ffc5f99575f17ebe6aee4a8cff00ffff519fe3f8dc1bfcfe1f44cb1f0bfcb03ff7b83ff9d9ef40efff05fc7ab06f887ff94c3e99962e17f9707fef706ff3b3de91dfee1bf8e570df00fff2987d333c5c2ff2e0ffcef0dfe777ad23bfcc37f1daf1ae01ffe530ea7678a85ff5d1ef8df5bb71edffce10f7ff8e15806eb21493eb5f969b336f6161b7a3567d59c2e07edabf34b35ae5e542e47fd7cad6d2e9cb7be0cbab1d7e67c367602d771d15dac296757ef8ade8dcf33eb03fff0ff396ac17f5fffaade8d0ffcef73c2ffe7ad05ff7dfdab7a373ef0bfcf09ff9fb716fcf7f5afeaddf8c0ff3e27fc7fde5af0dfd7bfaa77e303fffb9cf0ff796bc17f5fffaade8d0ffcef73c2ffe7ad05ff7dfdab7a373ef0bfcf09ff9fb716fcf7f5afeaddf8c0ff3e27fc7fde5af0dfd7bfaa77e303fffb9cbf14ffdf7cf7dd773f1c4f8a4d1ba23e9bdfa44c4db97adb83bc813cf96b5f1bfd9bdf9ebbb20ee7e795037c15f267d7e36ade34e67cde02daa946fd9cfcea7bfd8dcb67cec46bf727cd25ad29e6cadac23ffcc33ffcbb71f8f7fe9ddf661efee1ff8a3ffcdfe7d1e73406fff03ff9d577f8bf1f877fefdff96de6e11ffeaff8c3ff7d1e7d4e63f00fff935f7d87fffb71f8f7fe9ddf661efee1ff8a3ffcdfe7d1e73406fff03ff9d577f8bf1f877fefdff96de6e11ffeaff8c3ff7d1e7d4e63bf36ff77bf80a502b60bdcbdeb78d7f0d6522eddec6ec3a603db1dd49ad3d5ddf6b0017c0370377645d39433f975bd571de942486768faede014b7e97dbbb69d3db3dfdda558fddc78cad9f9747a363ae01ffe273ff8dff9766ba07e6e3ce5ec7c3a3d1b1df00fff931ffcef7cbb35503f379e72763e9d9e8d0ef887ffc90ffe77bedd1aa89f1b4f393b9f4ecf4607fcc3ffe407ff3bdf6e0dd4cf8da79c9d4fa767a303fee17ff283ff9d6fb706eae7c653cecea7d3b3d101fff03ff9c1ffceb75b03f573e32967e7d3e9d9e8807ff89ffce07fe7dbad81fab9f194b3f3e9f46c74c03ffc4f7ef0bff3edd640fddc78cad9f9747a363ae0fff3f26f7f01eb8a98ae802ba80ba4f59e694273d4f9f4ecea757aba8375e58026fd6f69dd9aa4fae9537dd3813cf7b5bb50a7b5d88c4db1af5dcfee42e9ce8002e962d3e57b15de2e9ff3735abb2f02f8877ff8dfe786ff6b75e1ff51ff5b1afc3f6ff00ffff00fff5772c3ffb5baf0ffa8ff2d0dfe9f37f8877ff887ff2bb9e1ff5a5df87fd4ff9606ffcf1bfcc33ffcc3ff95dcf07fad2efc3fea7f4b83ffe70dfee1ff97e4bffd2708b7507463dd8627ff29a78e4d8bb8899936d0bd6f72279f29f62a902eb742d45daeae778daf87f26a8fd341ecf64a9fa7fa093cf7dee5b8ba17ba46e93c4d7b917277fecf9eb9ab31f0dfdb336cb858f87f8c837ff87763f0ef73c33ffc4f79afc6c07f6fcfb0e162e1ff310efee1df8dc1bfcf0dfff03fe5bd1a03ffbd3dc3868b85ffc738f8877f3706ff3e37fcc3ff94f76a0cfcf7f60c1b2e16fe1fe3e01ffedd18fcfbdcf00fff53deab31f0dfdb5536bef9fefbef7fd0c1b4187ac0bac4d3dc46688aeb7ca745ac7eafb9143697c06623748d4f5ddd6f284ea687bfe676b537b9d29c3b37aa619a4fbdba2f01f7dc8de9fcd57c9b33a0ebad7e5dfc55e8b55eea67b2c4783707ff9fe6e01ffee1dfe7ecead577f8877ff887ff8dcef4ae73f00fff9d5ef8bff785ff7dae3a07fff00fff3e6757afbec33ffcc33ffc6f74a6779d837ff8eff4c2ffbd2ffcef73d539f8877ff8f739bb7af51dfee11ffee17fa333bdebdcd7c6ff4b15920efdf9ee9a4b809ff3a7d58ddb08ab712e8fd3a18743c7d4af83d15d56a99f4d2f1da8d325a4356bdece6efef547739d63e9906e0eeae97be67467a8fa9ccfe94c557d5587cbe572a6b94e6f1defceac9e2bd59ed6d969aef1ddf9767b54bfa4bbfd4b63bafef00fffa936fcc33ffcc37f67f0ffc91ffebd06f887fffa0cfff09f0cfe0feb07fff7b9e01ffe9d6ef8ffe40ffff0af63f00ffff00fff3557ed13fee11ffee1dff96a3ef8877ff8ffe40fff5e03fc7bfedfbd7ffffea33a3bab05d30639f0925fcdd9f93b58dc467797533db029c6d5d75a9bfeba4fa7a73b5c9bde93bfd33b1de48d5fd757ea2d41a35a936f57dbed6daa31e5ab6369dee5af7d1fc7d1e6d2da5b4beb55eb6f62a7339c0cfe77fdc13ffcc3bfcf0dff8f7ae01ffe3706fff0ef7c92bfd30bff7dbe3a96e65dfedaf771c07fca0dff8f7ae01ffe3706fff0ef7c92bfd30bff7dbe3a96e65dfedaf771c07fca0dff8f7ae01ffe3706fff0ef7c92bfd30bff7dbe3a96e65dfedaf771c07fca0dff8f7ae01ffe3706ffbf4dfebfad85bae467806bd4cdd579b52dfccedfc5ea01701743cddd693dc7cf1c6ed1afc0a37edbb804cde652ad63e9324c07a37e76a6ebe4f6a8db07b767c9a67d3d7d6a7dd75fd7838e558dee22d86a3e9f53edea9feaab06173fe5711799ea817ff8af9f9dc13ffcc3ff6cf00ffff0ff582b19fc3fea817ff887ff1c0bfff7b9e11ffee11ffeab3ffcf73de818fcdfe784ff7b5ff8877ff887ffc9e01ffee1ffb15632f87fd403fff00fff39f66be6ffee2f60697215e5fcdc86a71867d3c14b31aef62666eb93fc5fbb21dbf59d62ebd899c3e97717f6e6e278762d9caecdf814af405ded49ad8bbf72be36e7b75bbfaa67b3e657756c2eeda415fee1bf8b817ff8877ff84f794e5d9bf1291efe7b83ffb90efcc3ff261efee11ffee11ffee17f9307fe7d9e53d7667c8a87ffdee07fae03fff0bf89877ff8877ff8877ff8dfe4817f9fe7d4b5199fe2e1bf37f89feb7c89fcbf9c0fa7d544b767dd44d7b48bd99aaba1b5ba46a60be0197387f0991c67ac03adbed7982e57ead55db429efe622d0f1a9aeebb58bd982e0fa71e745353b2d9aaffaa89e5a2be94b7deab971ec686cdabf5aa7d3b0ed41b5a4f3599fe11ffee1ff511ffcc3ff9403fe7d0cfcc37f57436b3983ffe32127fcc37fd703fcef0cfeefc7e1ff511ffcc3ff9403fe7d0cfcc37f57436b3983ffe32127fcc37fd703fcef0cfeefc7e1ff511ffcc3ff9403fe7d0cfcc37f57436b39fbadf0ffcd77df7df78306d4a00efca9b01bd73915e9346cdf37739d06a7291d8e6dcd6736b303affad4cfb7cef9390e76d2b1d5a5ef9b9eba7cddd93bdfebfe77ba1370cff0b319bb924335384df0ef35c1bff779d6e07fa76d1abb9203fe7b7df0ffc9e7fc847ff84fbe536df8bf1f83ff5ec75697be6f7aeaf27567ef7c877ff887ff479f670dfe77daa6b12b39e0bfd707ff9f7cce4ff887ffe43bd586fffb31f8ef756c75e9fba6a72e5f77f6ce77f8877ff87ff479d6e07fa76d1abb9203fe7b7df0ffc9e7fc847ff84fbe536df8bf1febf8fff917b026319b6257e75da3e937ee3635bac3bb59c404ce1500ba83f60c88534f57724ef55c2ebdecba3579f62275b55cec957dd03c1b6ddb2f9de94c5e59a3cda59472a57caf31f83fee346d636b1cfce71a9b77f887ff4d1ef887ff6d3df8df19fc1f779ab6b1350efe738dcd3bfcc3ff260ffcc3ffb61efcef0cfe8f3b4ddbd81a07ffb9c6e61dfee17f9307fee17f5b0ffe7706ffc79da66d6c8d83ff5c63f30efff0bfc903fff0bfad07ff3b83ffe34ed336b6c65de1ff9befbffffe871ae816e0d94dd888d5dfccebe6939f1b7fab83b93900d3e570e5b0a67d984c0fdab446e9b05db1cde1ae07f2355f1653bc02ad63757cd2e9e6347ed3bbcba1f56e3681ded557bfe94be45d73d9c37f9f33cdc13ffc4f06ffc7a817fee1bfcb03fff0effcdcf83bf85ff99dcff00ffff0dfe74c73f00fff93c1ff31ea857ff8eff2c03ffc3b3f37fe0efe577ee733fcc33ffcf739d31cfcc3ff64f07f8c7ae11ffebb3cf00fffcecf8dbffb4af8ffe60f7ff8c30f53c066e3a7863bb129ff15209c6d8073ef5dcda956ea4f0fd2b4a629ff766cea7d3af8f5795a9bad69aec9af3ed74bea9903dfed815e84dbbc2e67ca7d3e771771aa33e9b972493e73d6e01ffeb76bded986ddea579fe1bfaf01ff47f44b73f0eff3c0ff2eafcb99729fcff00fff9aef993d99c6e0ff710cfee13fe586ff236a9bd6666b1b76ab5f7d86ffbe06fc1fd12fcdc1bfcf03ffbbbc2e67ca7d3ec33ffc6bbe67f6641a83ffc731f887ff941bfe8fa86d5a9bad6dd8ad7ef519fefb1af07f44bf3407ff3e0ffceff2ba9c29f7f90cfff05ffd7ffa0b5853e17af8f5b7f5ceb1ab9ba6f9374d6f0e5caaed1678ea2bd57635ba0bb6d3d5e9df1c6aedeb5ccbed6f0e76ebdfc525bfd71cde676d03e4e6cc6cd77ba3a7db879a6bbac0f539f9745ad2983b3fce17fee17f63f0ffc90ffee17fa3abd30fff3bebce6ff581fffb31f887ff6efdbbb8e407fff07f2cf577f12e4e757739bb18f83f1eb46fd7b333f8ffe407fff0bfd1d5e987ff9d75e7b7fac0fffd18fcc37fb7fe5d5cf2837ff83f96fabb7817a7babb9c5d0cfc1f0fdab7ebd919fc7ff2837ff8dfe8eaf4c3ffcebaf35b7de0ff7e0cfe7f7dfeed5fc04ac2bb825d8e9437f9b818adb93958697e6b7ac8367e576dbb46a9ff7488b60740e79fe9a583cf8d6fa0af7eaafbcafebab54bbe5a7bb20d94cf9c09cd95c65f73e95d39d3f00fff577a807ff8af3952dee4e362e01ffed3585717fe7d0ef89f0dfe9feb01fee1bfe64879938f8b817ff84f635d5df8f739e07f36f87fae07f887ff9a23e54d3e2e06fee13f8d7575e1dfe780ffd9e0ffb91ee01ffe6b8e9437f9b818f887ff34d6d5857f9f03fe67fb25f87fd1049aa442dd8db9e77421d486ea2175e0a543768b3b63b59e9beb204e0b58f325d35eae5aba54bbcd7787b1f67bf6ef6aa575d0da2e3e9d135d6bcd5d634fff9a3ff592d6d3ad593a8bae87ee4c74ba344f977f3a0b49c7f99ed6b47eba7392623a6df00ffff00fffefe0ff70bdc3ffbd3ef8877f975b9fe17f36f8877ff887ff33d73906fff0ff0efee11ffe6d1f4e938e39837ff887ff4f71f07f3f0efff0af7dc2ffbd3ef8877f975b9fe17f36f8877ff8fffdf2ffeec3870f1f5db083dcf9a96ff7ac56173f89df1ce6ea9716f1f47163157877b1743d3b18344e6ba43c693ddc3a2630d27aa6bedc3e4f7ad25a74639b7c5734aa8fcb91ea749abbd86eafddb3f6edce57d2f4ecb972be4993fa272d9d9fcb07fff00ffff00ffff0eff2a67cf00fff5deecee01ffe3547aad369ee62e11ffe533df8877ff8877f1707fff00ffff00ffff00fffc75dfff00fffcf68541f9723d5e93477b1f00fffa91efcc33ffcc3bf8bebf8ff765a38f7de8dd7f769f35d8c6bc8993bc8dde5e472ea7c3a244e9b9b9f167c73c8a60bbaab39c1e4d62c69eab4761aa67332c1a47be7f6a8cb31e5efe2a70ba13b575deedacbe652db5e62754da7b399eac1ff11df6b2e973f3dc3ffbebf5417fee11ffee11ffee1df19fc3fd6847ff8877ff887ff6cf00fff2e877bafb95cfef40cfffbfe525df8877ff8877ff8877f67f0ff5813fee11ffee11ffeb3c13ffcbb1ceebde672f9d3f315fedfbd7ffffee33134f6ac29f8db83d6bd7700a54de90e62a7e1aa2558ba9c6e6d261de9606ed77aaaebf62dad7f57afea72f9d54f6b4d9660ed809c20d1fcdb7db9ea5b756fcfdc5536aff057c7aed478abfaf00fff5a6b32f89ffde11ffe5d4ef8877ff8877ff8877f1dbb52e3adeac33ffc6badc9e07ff6877ff87739e11ffee11ffee11ffe75ec4a8db7aa0ffff0afb52683ffd91ffee1dfe5847ff887ff5f9fff6fbefbeebb1f34c1d4e4f9f38cb0f3bde672e30a559dab9f53ddf4eee2b73d756b50c75dcffafc16f5baf773ec5ccff3a71e942e66d255f7a9dbbfa4e5b590775a9c3617e3e6a73dd2fd74eba93e6eeddd3aa5f7b45e8ea3ab675ff5c07f9f17fee13fe5817f9fb7bed75c6e1cfef7f5baf7730cfee13fe5d73ee07fce0bfff09ff2c0bfcf5bdf6b2e370efffb7addfb3906fff09ff26b1ff03fe7857ff84f79e0dfe7adef35971b87ff7dbdeefd1c837ff84ff9b50ff89ff3c23ffca73cf0eff3d6f79acb8dc3ffbe5ef77e8ec13ffca7fcda07fccf79df8aff9f7f01ab6b56c7125cae809bdb80ed366d133be5bb7270ced834ee0e490244a173f9af1eec4ed795dae91272b9dda176f5baf5ae753b48525cf7beed25e59fced904aef6af3e6e2fd2a5e22eec4e7b67d3250dff39368dc33ffceb18fcc37ff2753ef00ffff0efb5c2ff75837ff84ff3f00ffff07fb4f9e11ffe35368dc33ffceb18fcc37ff2753ef00ffff0efb5c2ff75837ff84ff3f00ffff07fb4f9e11ffe35368dbf05ff777f01ebaa3d73c8b779dce6bb03fdda054d87529f3546173af59040accf9bc33cadd1766eea2bc54f0778ea45e7bbf5bbb2b7d31a5c5d23ed73ba8c266d0aff04f471f4fba0311b4bb95f6bf09ff3d61cf00ffff07f2d0ffccf3ef00fff1b4bb95f6bf09ff3d61cf00ffff07f2d0ffccf3ef00fff1b4bb95f6bf09ff3d61cf00ffff07f2d0ffccf3ef00fff1b4bb95f6bf09ff3d61cf00ffff07f2d0ffccf3ef00fff1b4bb95f6bf09ff3d61c1dffdf7cfffdf73f1c41e804eaf6a06c6d0b9c5b8067724d31dde2756353eeae07f59bfad4981ad75d30c9cf5d3cd381eb00dfee69a76963572ec36eee357da8ef957d4939a6f7e942b9aab1cec33ffcc3ff2ebff385ff6b06fff00fff478cddd648319bf83a0ffff00fffbbfcce17feaf19fcc33ffc1f31765b23c56ce2eb3cfcc33ffceff23b5ff8bf66f00ffff07fc4d86d8d14b389aff3f00ffff0bfcbef7ce1ff9ac13ffcc3ff1163b73552cc26becec3ffe7e7ffa7bf80d51dcc67cc6d5e07f9944bdf9f39c4153285f1ca41dace6b1dbd6036da5d9fdd7aa43e2610a743b6d5b6b1740edc85de5d0e9bfc2e5f5a535d83ee0ba7abe9fa70cf5d6ec74ef785facef0eb62d21971f14ed7d6e01ffebb587d867ff8ef72e93bfc7b3ff8877fad0dfff79af4c7f96d7376f3f09f63f519fee1bfcba5eff0effde01ffeb536fcdf6bd21fe7b7cdd9cdc37f8ed567f887ff2e97bec3bff7837ff8d7daf07faf497f9cdf3667370fff39569fe11ffebb5cfa0effde0ffee15f6bc3ffbd26fd717edb9cddfc96ff77efdfbfffa88d4d97c576dce59cec6c20f9bb7977b8afe8deeabd7a796aceaacbadd5691db4db75745ad27cca5be7ba0b44e3d33970f5aef6f5cc1eb6000c97ede63ca775badacf155e92ee4d7cc710fcc3bf9b837ff8ef6abb71f8877f7d877fafb7cb3fc54dfa5ceca9b78b85ff6bf1490ffcc3ffa4b7cb3fc54dfa5ceca9b78b85ff6bf1490ffcc3ffa4b7cb3fc54dfa5ceca9b78b85ff6bf1490ffcc3ffa4b7cb3fc54dfa5ceca9b78b85ff6bf1490ffcc3ffa4b7cb3fc54dfa5ceca9b78b85ff6bf1490ffcc3ffa4b7cb3fc54dfa5ceca9b78b85ff6bf1490ffcc3ffa4b7cb3fc54dfa5ceca9b78b85ff6bf1494fc7ffcbed21253e03cf60054fdfdda1dc1e862ab00a768da5c549f3daa3eb2341a7b5ab5f5a976a67aeaacbad49f5539fe992705a356ff24beba99a9dde34d79903e799f8ee5c9e7e09c6f465a7e309f29aa3834dfdba7eb4ceb4bfcec79da1145bdfe11ffe9376f8bfd7aa39e0ffb17e9d837ff8d7b86dfd2e1efee15fe7e0ff710efee1dfc5d677f887ffa41dfeefb56a0ef87fac5fe7e01ffe356e5bbf8b877ff8d739f87f9c837ff877b1f51dfee13f6987ff7bad9a03fe1febd739f8877f8ddbd6efe2e11ffe75ee6be4ff450b26d0a6c57763355685bba6af6c5ab760134c9bcb4fadf349f9921e8ddfaced99d7ada5d649978acbd7e9abfbb1eda1ab5f21db8299dedd0599fa73f59de67481b8bcceb7f6a5b5dcb9e92eb6b436ee02adcff5275daaf5b37b3edfe11ffee17fae0dfff0bfd1a3f1f0ff68f00fffc9e01ffee1ffb0f9e01ffee1ff938fcb01fff0af3aeb27fcc33ffcdff70afff00fff7d6df887ff8d1e8d87ff47837ff84f06fff00fff87cd07ff5f17ff3ffd1384f5d0b8c37306e8465431ceb71652e85cbed480e6e94c7ba9632edfb6e6d487ab9d36693a3c5a33e973f9d47f5a07972b9d8173ded5d0dadd9aa73572f5ba9a9a27c576f3c927e94feb3af5eefad9e852db9ebbe9bca5bd807f5f13fe3fcdbb1a5a1bfee13fd549f934af5b0bf8f73ddfecc71f7ffcd9f7e38f7ffaf9d37f7efcd3e73ffff33ffff4f33ffec79f3fffe99ffec7f1c73ffef1f8977ff9973ffdfcf1f8d73ffeebf1c77ffdd73fc5fc78fcdbbffddb4f797efaf9f8e37da18f9f3e7e56f6ee56ef385e5e5e8e9b8c6f5ebef969ecdb6fbe3dbefd8b6f8f6f6e9fdf7e73fcc59f9efff22ffff2f80fffe13f1c7ffdd77ff5d3fbedf3affffa7ffad3fcb77f8a7f77bcbc7bf973ca97fc3f32e11ffe3bfb3df3effa99d6c1e54a67e09c7735b476b7e6698d5cbdae26fcc3bff3837ff877fdc03ffc6bbe6d4df8ff34ef6a686df887ff5427e5d3bc6e2de0dff7ac06ff87f59b7a867ff87735e1ffd3bcaba1b5e11ffe539d944ff3bab5807fdfb31afc1fd66fea19fee1dfd5fc5cfcbffbf0e1c3c72b0bdd09760d38c1aef1e9706f1aea2e0ea72bc5bbda9b3e53cf2e570778976302a5d3701cf325b859d7e9b2d378cd3dadf154b7eb6fcabf59db4e8fab95f4393d2eb69ad390e293beee6ce9fbb40e9a57c73a837ff8af06ff3eb61afcc3bf7e9e3f3ffefb2f48fdebbffedbf13ffee99f8e7ffe973f1efffdbffff79f7eb1eabffdb7ffeff8affff5bf1e7ffff77f7ffcc33ffcc3f1fffcdfffed4feffff0d3f33ffcc37ffb93efbf1cfffccffff2d32f5bfdcb1f6fbf74f5c77fcff5affffe0b571f8f7ffbf1cfb96fbf0c75fb05ae1fffbdee71fbb9e9bfbddf84de7ed1ebe5e5f6ef57ff34feaef4f5e75fc47a777cf3cdcb4f3e7ff1cd37c737dffee9e7e5f60b59dffcf4cb567ff1ededf92f8ebffccbbf38feeaaffec3f13fffeddf1e7ffbb7fff1f84fffe93f1d7ffb3fffcdf19ffff3ff72fcafffebfff6a79fff7cfcc7fff8373ff9fccddffccdf1d77ff5573ffd02d7ed17bbce1ab77ad37eb875877ff84ff15f1affddbcd397e28f83ef7ff8bf7f877ff8877ff877e35553ad53c73a837ff8af06ff3eb61afcc33ffcc3ff71c0bff606fff00fff47ab21f9c3ffa3ae140fffde57e3ba75a8f19a1bfeef63abc1ffef8bffbb5fc0728bbf11d0099a443a515d4c8a77b9ba771ddb5c88a99fd47f7760756eea35d570ef1b6d69ce69d59ea6f8da53cd77a5af67cfd071a1a73a5f3577306deb26ddeeddf9763aa6b3aaba9ee5aaaecb15837ff8877fafdbbd3b5ff8fffdf27fb37ffcc77ffae917a7fef11ffff1f8fbbfffbf8efff27ffc9fc7fff15ffecfe3fffd7ffedf3fbdffdfc7fffebfff97e39ffee99f7efac5abffefbfffe34fbf6875fb6b562f674f7f4e7c57f3e59b973f0ffdfbf8ffcfde7f80dd721d65a270addefb3be7e8e8286759d1922ccbb69c233618130c0cc618b80cf330806786ff72672e333f489e7befc3ccf08f2f3003c3600c38639cc0189c8d13664c3096a372cef928e774f2b77bfdbb5657adaeaeafd6eafe8e642cc9b5e4adddbb7bad4aabded59f1e5eaaa45e243221bd0a095ab339129c9295692e4b4915b696bf67b3d9c0275ccb64ad20e6a29ca42b909ca84af722594bd89a62122091c1706cdab811f6db7f0becbbef66d80f495a5bb6c061871d0a871e76101c72f02170e49187c311cbcf41071d98e61cb0fffeb069d32680b036ce8e7fc7bffcadef3d9ef05fbaaee99fb266ca33cb56edd37ab024e5f9fbdffe6dcd75fc3bfe6b7a1cff8effd23a6d97e3dff15f5aa79f59b66a9f1cff8efff5f83ba6c7f1eff89fa2c7f1eff82fad77fc3bfed7eb937c2e6d76fc3bfeb51ec7bfe37f3d7a4b765bbfadb98effef0cfcafa98035654c05fb9871b5c04bc3f7e6701903923587f5d5d6687d72cd94e763609a62b796cf63cae13636772ce9a6e8298d29feace7505ecf21b89e51038e65d7989cf5e4d4545d53658c61e7d1dae2f877fc4f1d8e7fc7bfe3bf978b2425245a6ddbb61deebbef3ed8baf536b8edd6dbe0965b6e5b5edf0af7ddff407af6c0030fc06eac5e4515ab9024b54082d36cd611a7a85560bb68737b40d6cdfa9020354b737b5bf0baa17b4cc05a2c65a4b5642333a4929c7ee1c03f499ac2ea592dc9408215b6354cfe625b416246b574af61fbe937094dd7e85bba1787a42d9c8f44326c7588d5b4b08de18107ee9f2a641d72c8c170ec7147c1314f39069e72ccd170f45147c281071d08fbedb7ef72de3e6bcecbd270fc3f3a9953d63afefdfd5f7a3e75dd58fcf77638fe1f9dcc296b1dff8effd2f3a9eb1cff8e7f4b76e9daf1eff89f6a270fc7fffa654e59ebf877fc979e4f5de7f877fc5bb24bd78e7fc7ff543b7938fed72f73ca5ac7bfe3bff47cea3ac7bfe3df925dba7e5ce27febd6ad712ab0c61cd40a2ce3e4fdbd096ce9592d98968cf524ce7a0e8edaa6d40edf29878f2577ca21b0dea4e4b19e437aaa1deb05ef14fbf4b3b143ba9483962c4bcf543bf55e4b59b5784c01f69417929597965cc77f7d38fe1dff63c3f1eff8e76bad5bcec3b67f48b6baf1c69be1861b6e868b2fba14eeb8e32eb8e5d6dbe0fefb1ea036815d7b404059210ce42e565713b109d81ed4b59cdb106109c94d781f094f4de8c855282bb506647214559ec2b9030216da8cdcab360eeeb1ef81dafd21618a6d0a1d576b300671265b5bb9cfbc9e9ed1a234a79319baaa5a4bbbd39c90185e10c9af645faab035cb642fb6713eef88591b366eec0859c71c0dc71e770c9cfab493e1a9271d0f279c701c1c74d041d937c7bfe37f6c3c96f8f7f7bfbfffade7fa5e4dafe3bf1e0bc77fd936c7bfe3dff1eff877fc3bfea7ac2bf9e8f8075396e3bf6e87e3dff1cff71dff8e7fc7bfe3bfa673ea33c77fddf6d2b5e3dff1eff8af8f6f25fe3301ab943c2c6c4af02d853c4ace8f395772aa7698951c2fc995366adfb55e4b5e49ced8faa94933e5c0d06b4a7ab4ae10a61d5aebd1673dabd936d5b7f5d8b29e798f269fc3c8015bba5e6f7e4cd52f9f5bbe4ccd0fc7bfe3dff1bf761d0fc7bfe3bf2407894d487adab16327dc7fdffd70cfbdf7c3adb7de0a575c7175fadc7cd32df0c8b66db073c78e44b85aa30b7fa30d4de87545e8eef713d7d837b0032fe3f059f6ad09690292980213910072f5ac967ee3fc96c852527e5e8fb2f139c9ceb14119ababf97ebad776d5b990288624b2aee2d6724ab2a719c49f6388bf53252c5e4bf35afa9d5a21c6beb56143bf99ec85ebb8cad6caca1c56e62b70f0c107c2c9a79c04a79c7c62fa3eeaa823e188c30f4d95b2366edc081b36ac38fe1dff8f0aff7bb3bf633e6afb4a63eafe6a3b4bcfc7462d176abff7c6b7f5d8b29e798f269f1dff8e7fc7bfe3dff13f2d5e536db0e438fe1dff536d59cf3cc7bfe35ffbe9f877fc5beb7838fe1dff6372c6d63bfe1dff728db6c7f13f94e3f877fc4fb5653df31cff8e7fede73f15fe1301cb12aa17948469a1a5df35b963074a69ce948d29ad29add3d7b5f52ca3a6733d0766c916d6535a3b16c7da013c2653ce9bb25f8f857d7b7b7f8a0d961db53d98b27f25b9eb9963d9b13787cbdee0706c9ee3df5ecf321cff50d5ebf877fcd7e43e59f0bf73e74eb8f7de7be1d65bef802baeb80aaebaf25ab8fefa1be1aebbef81071f7810b6efd8d9557f4219b8ae1398ae23c9c68fac4cb57cb8bcc7d5a9ba267e7c3dac44d555d74ad754e18a2b5fe1482d0bb17d1fb7fc633f3b27d6f898c9599d97a97d604b8426d69bed2419019f61e52e9441be7145ae8674253b713dff014af71bde4bb24ddb93e212fbca5c1c07fe9de2884d14697120fb3866dd9c6e0d56cdea48591d21ebe8a38f82534e792a9c70e2f1f0cc673e7df91b495987c37c65eef807c77f693dcbf0f73f54f5aed7bebdbd3fc506cb0e7fff3bfe1dffb64c39cff16fcb2fc95dcf1ccb0ec7bfe3dff1eff877fc3bfe1dffe3f7a7d860d9e1f877fc3bfe6d99729ee3df965f92bb9e39961d8e7fc7bfe3dff1eff87f12e35f12b0b451c545139353ce97b2d7e3ec7a9ee9e0eecde1f76874afd727eb7a2ae0f7e600aa25899439f5e02bd92fc7d81eed8ded7b73e8685ba7e8d7eba5dd5373dff243deb3744cb93f35dfa6d8c5d796eeda3ac7bf7dcff1df0fc7bfe3ffc98aff871e7a18eebdf73eb8f6daebe1e28b2f85cb2ebd0ab66ebd35dddbb17d3b204d29d07cd96eaf15159d92cd31aed1d5cc9a541d2b91a6b0b213d7a78ab1af52c571253d6c1bfb2f9ff1efd4da8f64b01fb2d560b24bee2faf5ffe8b894b4d1360b1dab735c49680a434c94e642a6e77c87b127a62598a01b7455cde5fa5b68a921cd6d09a649f2076a5db82f4257d66b217fbc404ac48e4b36403fb4efbc1fea5fbcbcfa67df681c30f3f144e3ef9a970cad39e0acf79cee970fcf1c7a64a599b37ef9363ebf877fc97eef9fbbf1f637bb437b6d7ecb07e5bb64ed1afd74bbb1dff8effd23dc77f3f1cff8e7fc7fff83cc7ff74db1dff8e7fc7bfe35fcf2bfdb66c9da25faf97763bfe1dffa57b8eff7e38fe1dff8efff1798effe9b63bfe1dff4f16fc6702564d81059aa9c0b6e4ec0df0b47e6b9ea573ec5e29b9f5bd9a9dd67d6bfd981deb3d101ecbe41ff345caa8cd9173a7cad787809663cd95f7f43c807130d672ca5a37f66229f964dd2fe5ad942b7faf77dd182e6b365acf1cff753badfbd67ac7bfe3dff1ffc4c33f7ec5886d055b78f8e187e0f6dbef826f7ef35c38ffdc8be0a6ad37c37df73d008f3cf408ec595dcdedef02ffab8d5d452a242d2d5af12c748425220db14e2656e1a7a54a514c5a0a24b49555b4424f70a25b1db188e3c2fac82f262025df6801129c9010c573711e13b5a2f087db11a6d67f289bda04a656891c27d60550acae857266f3795e5f8a3b908cbcfb31e696893890a486e4af1c2fb497fc401b399651c4786d1ee41dc9f1c258cc97f6613b4224641d76d82170d2534f84e73eef7478c94b5e08471d7538ecb3cfe6e59cd9c06ee987e3ffc983ff313badfbd67a7ffffbfbdff1eff8aff93a7538fe61b0c6f1eff8b7748edd73fc3bfe1dfff5678effba9dd67d6bbde3dff1eff877fcd77c9d3a1cff3058e3f877fc5b3ac7ee39fe1dff8efffa33c77fdd4eebbeb5de9a3f9f02500b207caf9610fa9995c8259d355097e45b7e4c3df46ae0d67e8ef930754c59534b143947dba8e54fd9d7317b2c40c9bdd5f65a79a387f6ad165b4bf758fcacdc2be58b754ffb57b24dffaee9b264957c2fcdb7744939355c6a19f2bee37f384feb76fc3bfe2ddbf46fc7ff131ffff87bcf9e3d896075f34d5be1a28b2f85b3cf3e17ce3fff5278e4914752c5a4e5c26e6d27a423fee4367ab1276241df662fdb41edfc989014686ae894a7f9adf21549472cab15718da2dd5e8e1dad49d59eb03d5f2bf6889eb5b4b6253214128f923c9ca7f29adb0fa6ea554c704af686bedd21cba516866813c896872c33f6d5a720db085da52b7c266c4c5324f10aef711b4218e667a4fb69604c98bc46ff6a9a59267ee15ee1b76ead88f7b04d225620dbb973576a1f79f555d7c217bef0b7b0ff01fbc369a79d0cdffbbddf0dcf7beeb3e1a8a38f84030f3c105656e66b728b65c97cd2f7e433c7bfbfff4b734afbc773b48d5abebffffdfd6fc972fc3bfe1dff8e7fc7bfe3dff1bff69ee3dff1affd74fcaffdedf81ffe76fc3bfec786e3dff12f9f39fe1dffa5398e7fc77f49b7e3ff898bff54016bca264c112665acc7b85a12976ce251ba57f349eb96f36a76959ed76497369fafa5bda5c3a806b2f58ca9f1acd9a2e7c9df964f7a4dcd9e29f3e46f2b7e259f4b326a32e533ebde94dc1acbb99aec5a2ce59a315f4a6b4a3638fea1baa666e7d87ac77fd99e29f3e46fc7bfe3ffb1c43f3ec64a573b76ec84bbefbe176ebe792b9c77de8570fef273e38d5be1c1071f4aedf2b8121457896239dcba2f7d3aa1893085edfa780e7eba0a561d1908b5772422c8c4a1b456109890f0c42df3b85a15de0f50ced3c072fa8769fd6c3ecbed035b41da62394804e36a58282184be7522fb15d8771187a84856ec4bd0f3d85eb487896a222e41c429755c6cfb3ce8095fdd9a5c3d4cb477947b2d63936c1204319d1f898015db1434ce1f968fcf70df712d12d4f0fe860d1be0d0430f81e34f38169e7dfa33e1b9cf7b169c74d28970d86187a5ca59dc5ed1f1efefffd23d7ffffbfbdff1eff877fc3bfe1dff8e7fc7fff83cf9dbf1eff877fc4f1f8e7fc7bfe37fad6e39af6657e9b9e3dff1eff877fc4f59eff82fdb33659efcedf877fc3fd1f11f6eb9e59658126205603da02ac9d163ec4059afce3159964f537cd76b2cdba71ea063b26af1aee92881bae4979e3725e9b42da5784efdaed9573bf4b4eddac6b1bc1ad339067acbe79a9d53006b8da987406dfed821e1f877fcebe7a5f8695b1cff8eff9a9e2702feb1d2d5cd37df02975d76656a2f78e9a557c09d77dc050f3dfc48222421f9070956bc26c960bbf03a4622f174d7e919cf677212dd4f3a711d1192d8bb48b206f6a39c1833792910d929cd593e5eacaea66b8ed120567c4dfad13e20db597eaeca45df4c964239add09345aa38b24d69fe324e3322283532af98e8a5f765f91bed4f95a8d0755c2748678da85e15e5be8700c1d85719af5592dbb24c7a96c86ca1236d25221b406e8708d0e7a28c33c782f52c4846b277b9167dddb2655f38eef863e0d9cf7e26bcf085cf87534f3d198e3bee1858595919c875fcfbfb7f8aac5abc6b3af4bd31fff5bc5aece49a92efb5f77ce9bb669fbfff1dff8effa1ef8e7fc7bfa5bf36dff15ff7c9f1bff679297eda16c7bfe3bfa6c7f1eff877fc3bfef95a0fc7bfe3bf649fe3dff1eff81ffaeef877fc5bfa6bf31dff759fbe5df8cf042c6bf15852969c184bc0b1a0580e58f36b809e62cf946097825902e6d8a1312561c7e6960ee55292d7c03f9624960fd6736b8ce589be5ff2a564ef986ccbf61230c60ec8dabcb17897fc1a9337c5a6124e6a326bf129d9eaf877fcebf95a861e8e7fc77f781ce31f495748b2fae6d9e7c105e75f0ce75f7011dc7beffdb07dfb76585da55678383755a35afeb1b4fc11951d3cb2bd21e416828b1873abc020edc00fb5ca5b70d5a7e5ef34970843a88709422873b6bcc7c41fe9137e6425ae26b7e6eb6c90f1ebaa562da8dd5e1fdf966da2b95cbd69c195b7d2a4613b3ff697dbf7f1bdc15e85907d4ad72886d627fb29b62c8b3f2c03ffdde879b4965b1246b685f4a5f80b3b70dff012efc93686913e783f55055b74715f2c56973afbea55493f7e888c85c290ce86d5cbb83219fbcee4b3fdf6db02871e72303ceff9cf8657bcfca5f0cc679d06471e7978aa9ae5f8f7f77fc9f7b1fb7a2f4b7a6bcf2dfbe41cb9a6e483f5dc1a6379a2ef977c29d93b26dbb2ddd205e0efff924da51896f44c9539d5869acd8e7f18b5493f77fc3bfe6bf129d9eaf877fcebf95a861e8e7fc7bfe3dff1af6d77fc3bfea7da50b3d9f10fa336e9e78e7fc77f2d3e255b1dff8e7f3d5fcbd0c3f1eff877fc8fe01f5b104221205662c9399671b50095f458c68d01a7a447db59f2a7644fcdcf319b2d7d6387c77ae4d46ca9c9d9db676307831cb504aecd91bae41c4bd6def85693ad6d1b036acdae1a104b79608d29f19e82935a7e5b365afe39fe1dff8e7fc77f49afb6ed89807f24c8ecd8b1032ebbf44af8877f380bbefe8db3e1baeb6e843dbbf7e0429c9ce622e926083d89c8d3c6beed1deaa279393ecb6748669a63a52994b3fc8d559816a96520c0ace95ad975f2ba767849576774ae248512b91d1f7bc2ad03933d4cde4202506760bacfd5a3b8e216704cd8d61873152c8e05db2d9f01f987b6620b456c43989e939c44c42252554b72f13eb725e4ef1c27da132ba7f0f782aa5da57dc2b9a2e52213d5f819c746e61eda309b61452baa7645b1648294d41d283ee8d7f261ae88b5742e11af922d9c0349ee2ce713ef55a098058c17085c2d9f33498e2b9661cbc2a79d7212bcf255af8097bdec45f0ac679e069bf6d9d457e552c3f15ff76b4c8fb6b3e44f2d3e253fc76cb6f4d564e9f963726ab6d4e4ecedb39afe526ee8f88ccd91bae41c4bd6def85693ad6db36c9f9ab7356c95f2c01a8effba5f637ab49d257f1cff8f4ebfe3dff1af6d73fc8369574dfe7ae438fe1dfffc7bcc16c77fdd1fc7ffa3d3eff877fc6bdb1cff60da5593bf1e398e7fc73fff1eb3c5f15ff7c7f1ffe8f43bfe1dffda36c73f987655e55b04acda024b410950b5673c6a865aba6bb6d6127f6c43a780b8b466ec5e69fd98ae92ee92cd53125ddfafd9321693a9a374b05907d17af6684a4ca7c6ce5a67d953925d7a3676af36f6e6501ad33725ffc6e63afe1dffeb198efff2bdda70fc3f36f8c7cf238f6c835bb6de0ae79f7f117ced6b67c325975c0e8f3cfc306cdfb92b117a9034d5c616906193f4e35a59018a6d411da24d1edec3ca476831938e80ae656bbbae12d3d296055583229212db992b49c53868b737c0083d435934a9b3939e693cc9d67f6c6f6eeb27e2c4314aa4a11072b5a8e41f4026247165ae28fcc47f2f689e6cf9970958c95ed4197395a9b44f44680b647ffa4e95c69a6c37fbcbad1fd9d696886d2d11a9da96c85fb41f99ccc5fe41574d6c2609664d4884adb47f8930d74d64bb1b45e0cab924634dfb36e3168c821896c96cb87e2967be3287830f3e085ef082e7c22b5ef1d244c43af2a82352b52ceb1cd47ba47f3bfe61549fbfffc7efd76c59effbaa3474ec5967f0f77f7538fe1dffd6da92cd8e7fc7ff983ec7bfe35fdeafd9e2f877fc5b731dff8efff50cc77ff95e6d38fe1dffd6da92cd8e7fc7ff983ec7bfe35fdeafd9e2f877fc5b731dff8f1dfe530b4279a3a67ccc616b5dcdc11a30a7249ee53c5f976cd0cff59cda580fa86a36eb7996cd5336b366474deed8580f48a7e85f6fae58f2f43c2d7bcc9ea9b6af77afd6a3ab96d7355bace725bc9472c65a5f9b57b3db92535a67d958f2b534d7f1eff8d7f3b4ec317ba6daeef81fb7db92535a67d958f2b534f7d1e0ffeebbef81abafbe16fefeefce82f3cebb10b66ebd0576ecd899e6a43544b469e6f354d509efe5967e4a0f5fb37cfac32957496a696dba8d849f9579aa5a95d612b108d704f2832b3fa18c08c3dce67b387ad252377741ad09592edecbad0b53abc0d0d9440423083d816915ed1295b27834a2d21313b5063107185486320964aaf21513a5d21a41064b84378a4d24e219dbd95075ad48fbc8ba78dd2027c8a696885b20f62613a3c88fbc3762ef3022d802723ea7968c745ffa8795c712518b567515cd44ac510e93da624f9e4362588a3f570beb8c4e6bb6ecbb2f9c74f289f0dce79e0edff3caef82d34f7f26ecbfff7ec55c0703338e7f7fffd7eca8c91d1b53e33155ff7a73c592a7e769d963f64cb57dbd7bb51e5db5bcaed9623d77fc3bfe6b7638fe1dfffcbcb4def1bf764e6d38feeb6b1cffd3f594d639fe1dff8e7fc7ffd873c7bfe3bf6687e3dff1cfcf4beb1dff6be7d486e3bfbec6f13f5d4f699de3ff3b0cffba02562d01d613989a717a3e8ff51e0e6360b1ecf8561e42a5b5253ba7c4b336d7d23155df1400957497ee4d1dd6212aafa7c668aa9eda2135e5e059cfbe58eb6bf2d62b733d7a6a73a6c872fc3bfe2ddda57b5387e3dff1ffadc4ffae5dbbe0b2cbae842f7de92cf8877ff82adc7edb1df0c8b66dbd0cfa576a59379b2512cf025b0312916641d592506222f8b03f6c6767001a030d5552e2b55c1d4ab6d503319f6dc7ebd8f615a1584f679b227be13af19d5aee11490ce725fb5156e889564c20e3f5783793c32211a296bf25912811c44247dee2d67c1cdf40ba17e41fc787c9600b2276a10d0db560c498e6fda1bd437d785fdac27e35a25d20846195b0e4438c83fd6e486ecbe42e41768a310ee2cffe729e2492d98c2a65b57deef09a556a45988854b406bf701db615649dfca0ab0e1681fe9764700cbbea673d592cd986315a5e6fde77331c7fc271f0aa57bd025efdeaef83638f3d06366fdec7f15fb18fe7067ffffbfb7f821e7fff3bfe1dff8e7fc7bfe3dff1eff877fc4f93e5f877fc5bba4bf7a60ec7bfe3dff1eff867fb1dff8e7f1c8e7fc7bfe3dff13f661fcf7d32e07f40c0d286f298eadc9831539c2a1d3453d6493deb395cc6646bdba6c8af1d2a53c7a339e44af2d8a6311d6307fe98dc9a6cf96c3d7b555a2beff318cb593d67ea61b91e9ba61e0c25db6aeb6b3ed4e23245b65ee3f81fdae6f81f975b932d9f39fe1dff25d972fdd83aac4af4c0030fc2e5975f097ffdd75f8473ce391feebaf36ed8b57b4f26359190f4d5a4167648b2c16a5090aa31a54a4c828c949631194954586ab88254ec0853c9ae48a41afacd24aef43b20490a32d1270abf989094fc96159542e865d37c243f41ec0951b10f4c5fc94b90c806043011435c97c85a0deae8c862dc02914961294ea473ce55ac5027f9952a4a85902b5e710cd8a7966cee5bfa85ec7f2bfc943292dd2dcaef084e99f04615b1389e492eb707a4ea576c438a2ded373e5b10610a848d1cbb285a19e64a5db443210c5b1072f52f2653e1bf72b52fd49f5675eba28809b79e4cf7295f40ec6ba4bddbb861031c7becd1f0ec673f0b7eecb53f02a79df6b4d49eb0112d30d977c7ffb89ea967f7149bb46dfefe1f975b932d9fadf73d6bad95f779f8fbdff1eff85f2b8f6d1ad3e1f877fccbf9633e9486e3dff15fb2c5f1eff8b7e63bfe87fa1eabe1f877fc4f91add738fe87b639fec7e5d664cb678e7fc77f49b65c3f659dd4e3f8b7e5b14d633a1cff8e7f397fcc87d270fc8fccd315b0a628e531b609ebd9a8923eebf914dd5a96b5a6064acb5ff97c8a1fb575b543abb4e6d18cf526ff54d096d6d680bc37876e6d4c95b7b7e0b6e494d68c1d82a503b3246f3da3f412199b3745a6e37fe8f3143f6aeb1cff8e7fc7ffb83eebb9d48d2498db6fbf032eb9e472f8d4a73e0f179c7f113cf8d0c31d7106093438173ac255a0f9f87b86958d5a51518908481d11a949842c1cb2addef25f89948303dbd6a539a1af3ad5cde9142e8840846b575656d2f742b4e703610357be42bb9814143b27b38f786f952a47e1fa962a5e458a051389805b1232018bed07e8c93f21f47109c35681792e118f16289bf4c86a60b8ba1104288e658a59aa1ad5d99fab7c414fbc62021756b4623d5d4c2219d50c2a7065b94c4c8b714000c37b83368d5ced8aab4d49bb694ece2ffa1e54028b5d0bc1444ec339a25259106d0c395691f67ed00e92f28587ac48c6cf5007574f8b828476e04107c24b5ff202f8d1d7bc1a4e3ffd1970c8218764f2596df8fbbf2ccb5ae3efffb573743ca42d53625d93e3ef7f7fffaf579ff57c8a6e2dcb5ae3f85f3bc7f13f4d5f698de3dfd6e3f877fc3bfe1dffebd5673d9fa25bcbb2d638fed7ce71fc4fd3575ae3f8b7f538fe1dff8e7fc7ff7af559cfa7e8d6b2ac358effb5731cffd3f495d638fe6d3d8e7f1be7b333cf3cf38da545fa9e3412aff9b75e67c91993cb9f310765604a411ad3a7efd70e1efdccf259deabd9326683d65793add7e88fa5736ff7ab14a3297a4ae01dd33d16c7b1784dfd3de5beb5ef16066a7964c9d48743297f6a7b6fe9aa3d2bdd9b32b7e6fb14f935b93a7eb5758e7fc7ff58bca6fe9e723f38fe07d78f07fcefdab51baebdf67af8bbbffb47f8c0fb3f041ff9f027e19aabafeb2a5e2d4587d8915eb83a51d286f797ffa086c8f74826ce8d62de206fbb49696edfe62fa47bd98f107a8217af01c8edf898dcd31041a823e934596724d9dd8826b6650cf2b56ce5c7f6c798c95740b6245fd84e9c4b36b1ecae9560678dac48956d8a31b73c04b14778c9fe06e0eb2e3691634aba9b0266fb5877d732f6b98a17d999db0db28c107a1217ca2282dd9adc94ba75bef27de177b60bd479036a707e717c794e5a03038c37d4f211449c92ec260c08773b77ec849b6ebe05cefee6b970f96557c2b66ddb61df7df75d7e362fd7cf5235b1ef74fc5b67e8547dfa7eeddcd6cf2c9f3556f7d606adaf26db7af78c9e157bb95fa5184dd1e3efffb24c7ffff7d73a7eb5758e7fc7ff58bca6fe9e723f38fe07d78e7fc77f49b67eeef82fcb74fcf7d73a7eb5758e7fc7ff58bca6fe9e723f38fe07d78e7fc77f49b67eeef82fcb74fcf7d73a7eb5758e7fc7ff58bca6fe9e723f38fe07d78effc70effe1965b6e8925e51a3cf2bb64646d48f956e0d62bdf5aafd7c8a429d9a175947cb5366a6c5e699464c9e7d6b0e6681b2cff2cb9251b2d7ba4cdb59c28c5b126cfbad67e5931d67e94e64cd9c3da9a9a4e4baeb56e4a7e8ce55b2df663324bb687cae1e3f877fc5b363bfe1dff35bb4be3b1c03f5681bafcf2abe00b7ffdb7f0a52f7d056ebded76d8bd6b77ae1e85649c54592ac6aeba50086bf634a47e80953d66b362d7ca4faee736835ca1892b2359f6a6ca52f379b28bab5c0dfc43dba053d7ca3d116def745c7056aefc047da52dae6015e819eb4df76485252280e135afe5355c198aab7c259d00b93a14b729e4356c9bf4bf237e8514fb1c3351e58a63883a781dcfc976a43d1076708b3f315fc639cb95e426de179a2b2b990df29ff621ed2913d2f01655274bb982a43d6e9ba872b8a5ea58e93eed0fef6bcef100391e73aa8616b99d64432d3017fd5eb03dac6be3861538f1c4e3e17b5ef90af8d11f7d359c70c271b06179ef3b11ff2579bc5eafa99df1f27eedacb5ec28bd7bf5bcd298fa1ed2a3f4ae0afefe077fff3bfef51ac7bfe3dff1eff877fcc3e8334bae65b3e3dff15fb3bb341cff302a4b3eb786e37f68a394ebf877fceb358e7fc7bfe3dff1eff887d167965ccb66c7bfe3bf66776938fed7fa940858961353023105047aee7ac062e92d2554c916f97c3d496fc99f926452afb66dcac1548b43499796b55e904c3940f5b7f4af2643ea9063ca613c65df2d1db535fc7c8a3c2953afd3365bfaadf5d63a1d83b18345da52923d25b7b49e311b1dff8e7fc7bfe35fcb1b5b57d23d35eff1def6eddbe1faeb6f840f7de8e3f08daf9f0df7de731f2c629bda00e2f346b495430933260dc5be9d9c24f7607b429c875a5b9a4f86f4f3c896144f516529ad67420eb5cbc37b1c8d44f6c1b5548d89894138dad8a62a46492fb7cf239d5192a096eb67721d117d9010964860b1233eb5a28a15c781f79fab71e1fc64ef52e97c36cf3216abab7d952cd2c5c4284d6662df22db42fbc56d103bdf22fd8e6922ef297e2f884c85633e9f67621baee536851c7ff68df7a02172d942e8adbd03d69c81a123a821c989d733812de71fed23fb13c4774e851ae6d23e77c4b320fc0022fbe1bfda36f6ed17c59e727b42de47ce2f799de2b63287638f790abcf825cf87d7bdeec7e0e4934f4895b12409d0dffffefe9fb277a567fefe0753a65ea76db6f45bebad753a06fefe87a25ec7bf6dbfe3dff16fc970fc3bfee5bc9a8cd2b5e37fed3a6db3a5df5a6fadd33170fc4351afe3dfb6dff1eff8b76438fe1dff725e4d46e9daf1bf769db6d9d26fadb7d6e91838fea1a8d7f16fdbeff877fc5b321cff8f6ffccfce38e38c375a1b21eff1b5f52d1558dffc9106d474d5f45bcfad03a236a43d7a63adef31ffacef925efddb4a40cba6311f2c1d253f2d39ec6349a64e486da3deefb1f956cc2d9f4b311b8b77c98e29879ef447cfa9ddb3d6946cd376966c1a9b0705bd63b6c8f858cf2d3b4bdf8e7fc7bfe3dff15f7afe68f1bf63fb0eb8ecf22be19def7c1fbcefbd7f0ee79c733e6c5bde43020f1372a4dcde27e84835580daa99f5f7a90214b78e4bd59822b503c43931e6ea56c015a40291b2000684a7418cac1c42fdd0766d0fb1d051409211af897d3b3db243b6a81bb4990bd09181981405686bf799afb0cc9088664ce6c99886b578601f12112812616d3eebeec74e8fac18c5714badf29044349f772d1b713d402660b15e595d8c094df87b4092e3f5384fc4724195a1987c867a16dcbe91fce198b10d9122c06d1765be753abb6a56fcdbfc839b7c665b5302f13ce10febe3580ee4a12ccc19b48f65e51c5ada414432be272b67c95c6ac41eb1a5ecef830f3e04975d76552221de79c7dd70c8a107c30107ec9f885d83dc13e3898cffb1e7fefef7f7ff985f3cfcfd3f9ce3f877fc07c7bfe37f22fe1cff8e7fcb46c7bfe3bff4dcf10fe6b3e0f877fc07c7ff7af45bcf1dff8eff31bf7838fe87731cff8effe0f877fc4fc49fe3ffb1c77fd8ba756b1cdca880551ba47febf5b52095beb5b1d6336d831c63324b4967c9d420d0beca67b5755a6fe9792d516bfb537a56b2798a4e2bbe63c95df349feaef957f2a914f7d2c15ecab19a3f96ae925d7c3d26a7167fa9c39223e5697b6afe5a6bb52eadcff1eff8d732f570fc3bfeff29f08fdf3b76ec842b2ebf0afefa0b5f84af7ee59b70f3cdb700b250b0052112aab8829126d4703525bc838421ac9035e7767c3ca753820682758a240296a84405c2c744e89acd44f522415282e11f8df80ce8bb2336cd963f913886249be58d36e0cd640a92a724f126c78decedee6125a998afc9059abb58c645b54c243f1bd18e2fcbc20ffd96316b44bbc2d5a5ac39918620fbd8ad67a214af03de3be8aa89710c5907af4ddf648ff435c78cf6955b2a0e7249c8e2f8587bc8fea10d899884f662b5af1006ad0963ec2b7d31b98cd735bae299b09ffde4e79ddce59c459f6381d6805aa7f3495647637fd9fe40f383f01f687f70605e3ff5a9c7c32b5ff9f2e5e715f0b4534f822d5bb6ac89a994ff44c07fed9d513acbadf9b5b3da7a17d4de0f63efa1da3aadd7dffffefed731b1e44879da9e9abfd65aad4beb73fc3bfeb54c3d1cff8e7fc7bfe37f6c38fe1dff63763bfed73e77fc3bfeade1f877fc3bfe1dff8e7fc7bfe3dff1eff87f12e11f5b104e4dd6b14490a376285832b4917a8ee58ce590a5bf14002b19c7003796c42579359fe2c4c3b3742886caa15c1a3559d6ef29601df3d1bab6645b634a52977c2ae91db3b566536dbe1e969fdaee297b358699da613396df8e7fc7bfe3dff1ffedc6ffeaea022ebfec4af8ec67bf005ff9ea3761ebcd5b61cf9ed54c5461024b9acf7a9040446d0617a9e295aade94e7f4c4add8294c1face894093a3857d9d9d25a59b988e52e5a263c4522600d19ff21b7a00358a4e67cb12355c5ae7a5713e69088652807ba0a50287f9ec85dabc9be363680dea54a5e1483644344421a56614a9159fe5ea4f991fce8d6c69e00c4f186b598c16ff693e392e2c0b1e435f8216213b73844b9a99216e99e735b3d8a816eb307bc1f1c27e889465c156a41ad10712eee27ee2f139c628c6bf28609535dcc9b6c4f10ad140755bd624f46e33948ee6b0c9938a7112d2d03c5122866205a472e88a007716dc52d26b4a10cfcdebd670f6c5859c9a439e903ae68c41ea5fd899c3b3193b370cdc64d1be129471f05af7ad577c33ffbd157c3d39f7e8ad996505f5b38fd76e3bf3447dfd77a6afab5ecd27ba0161f4ba6654b49ded8bbb1b62f729d1ea53d98fa4eb16459bf6b39a365967cb4ae2dd9d628e9d1f694d6d5f6bd868f924db5f97a587e6abba7ecd577c2fbbf34c7f2cbf2c7d23f16a7d2f352ec1cff8e7fc7bfe3dfb2a524cff1eff877fc3bfe1dff8e7f79dff1eff877fc3bfe1dff8e7fc7bfe37f6cbde3ff49867fae8035e6d07ae69440a4c75423e51c7d6dd93576a859f3a54d5340b83787d314b0593e491deb39ccb41c2bf1a6f8a26da9e991ebc60ecab8ce0354da3af530926bf57acb667d4fae913a4aebc6e268c5de1a6307c59431d5cff53c9f2a63ca70fc8f0fc7bfe3ff3b01ff4864b9e28aabe1a31ffd24fccddffc033cf8c083802425804e177fb0551eb793c30f134d9820c30498bccf4cbc1273d27da1bfa1ea474ccec2e74cd6614297dc57bc8fd587900583e499becd1e928a668974d5e98e5dc5256a3598ec0b4b1fb01d5d8054196bcb7e5be0a8a38e84134e380e9efef453e1d8638f85030f3ca06b09b868531c6ed9ba152ebdf452b8eefa1be1ce3bee846ddb7642323d6ec8b16c668b448c0aa2429224f448d2d82a919b66a2fa52234848dcce8e5b24e2271193e2b06a15fbd50892532bf6a4a5d8a53807c895cbb87a58c6550a46dbb759643da92d6048c42b49a42247faaa66b123d1a5a0c49e5885fb8ec43ad6837b86fea5250083f6959c2b40f7f02e93a55026b7ae641bbb182ced5c5de418b0cdf804f3024726aaa1ece5f57cd6c09edd7bb22d2b2b2b7d35b75947b003facdfea6bd6239ec2fb5ce0451770d6d38f0a003e075affb51f8911ff94178c669a776044031fcfdefef7f7ffffbfb7f3d6bd63bc7f1eff877fc3bfec786e37f7c38fe1dff8e7fc7bfe3dff15f9b3f26a3766f3dcfa7ca98321cffe3c3f1eff877fc3bfe1dff8effdafc3119b57beb793e55c694e1f887be0256c9c95292d79cb60cac1d1cb5242925ec7a1265bddf35597b3ba6ea9a926896bcb1fb251d63b6d5f69def97e64cf17f0a586a31a9e567e920a81dceebf56d3db1abedcdd8e133c59e922febc19ae3dff1eff877fcff53e01f092558f1eafaeb6f806f7ce35cf8b33ffd30dc75f73d8998d29161706ec30e25524b2226b51dc907a5a3c498db01c6fe5a1085b07a129299f235b69a439bc8077c82641c9c2fc941f89164221c487c4995929864c5cde9965fa90d5d92cb55b0429a87b499102976cbdf2b2b0d1c7ce821f0bce73e1bbefffb5f05cf79de73e0c8a38e80fdb6ec071b37aec07c85daef25a2590b3b77ec80471e79186ebef90eb8e4e24be07ffdafbf830b2fba04eebbf7c1a52d5d7bc3c40fa2d6799248c6319f89ea5ff4305753029ac384ab4458a3aa4c7a5f39462c83d7ea9831710d9fb7b43ee727ade3f9ba2d204a9c911d6824efd79a21e6cb0a694c8843fd0bd14e901326b0dda1ab2a3513390164738a2392b5a89a56b219e50a9256e777c8443bd97651c68475718cf37d2275f1e8897cfd7aae24d6b06f33928f2229aff3fe907df3951538e698a3e19ffff48fc3f7beea1570ecb1c724a2d7e30dfffaba64d370cbfdfd6fc91bbb5fd231665b6ddff9fed83bb2a663cc8fdab596ab6dd0b9c3a334c7dfff8e7fc7bfe3dff1eff82fc9dadbe1f877fc3bfe1dff8e7fc7bfe3dff1eff877fc6b3952a7e3dff1eff877fcafc71ec73f8cdaf678c3ff9a0a58b5c5eb5588633d81ac81b076efb11856f25a73c61287d74f3d606bfa6a3694e229e794d63dda03bbe447cd8ea932c6003176605abfc7c6989df27e2d7eda862971d5beacc7def51c428f6638fed7ea76fc3bfef57d6d83e3bf8c7f24a2dc7efb9df0d5af7e1dfefc831f85eb6fb81976efda356c25a7f506224911a9263d27420c5e2fa8559eb441ced1fba4e5cb4a4ef84f227ad17c24d66035a5f90ab62b5c0c2a4aa5aa4e80ede5b09d1c92b366b92e1112c89043d6552b82448679d977bd18beff07be0f5ef2e217c251c71c05b3b05cbf20ee0f10792a5f213127d21d24dc44b8edd63be04bfff815f8f8c73e09df3cfb3cd8b963378436663b017da36faec404465c7020990cab31c998a36f58250bd7cfe7b3655c63ae16c5e42a88c3967e7abf99082563d4904d11fa9ceaae21b78f0c6c1f55bf4a0423b1c7797fe2b04560d2cb7b2d64a48a5eaad2975cbf10ed14d1361004aaaed215aca972c5316848471484b45c612df655c7b8b523e66d220862bcc53d9e8376e4b8d01e72aea7b6926457924d55cab0ea19571de33de7986cdab4114e7bc6a9f0d33ffde3f0ca57be1c0e3cf0c035389c32fcfdbf56b7bffffdfdafef6b1ba6c455fbb21e7b9fc8ef7fc7bfe3dff1eff877fc3bfea50cc7ff747b1dff8fdd70fc3bfef5b535c7f13f7d0d0ec77f5d464d5fcd06c7bfe3dff1ffd80dc7bfe35f5f5b731cffd3d7e070fcd765ccdef08637bcd19a34d5d8c76ad40ce64f699d755dd323e5d6744f952b65599b52f361eabdf58cc75aa68c137feb9740e99e254bdf976ba7ee895e6bdd2fe92ed9587aa994c0661d18a5f996be9a4fb56725e08fedbb75bd37b9f268f3b324d3f15fbeb79ee1f877fc5bb21e0ff847a2c9c30f3f02677df96bf027effe007cfce39f81db6ebb23919ba46cf411bde4ca5489cc82cf22115362472e5a934f181bb1cf7bb05a15915572e5219ac772998483038947213d6b321928ad4d5f21c9ce558c40e44a40c251933ea8a3d3d5b7cedbb86105beebe52f815ffef7bf043fff0bff129ef39cd3e1c003f68339acc22c625b42a457751f6c4487a4ab19b4e93a04faa0aea5b803f7df1f4e39f91478fef39f0bfbefbf1fdc73cfddf0c07d0f74f6e1fc66d6b5e7a35870652530725fe612b76e8c32364434ca7ba2709aaa5b09795d2cc22076035cb02d24af2319f5242e26d6e19e2c167dbbc89c3f31f6adfa283fb84a54107b1c42df3651ca661fb27eb21728dfe47cb683f330a6e8f623081f589eacc00532e6ac8b2ab7813c3704692e887bbc2ee72b618063cb2d18337190f69ce5eddebd1beebdf77e38ff828be1c61b6e4e761d76f861a91a962410eaf1adc47f4da6bfffcbf7d633fcfdefef7f4bd6e3e1fd5f93e9f82fdf5bcf70fc3bfe2d598e7fc7ffa391e7f85f2bcbf1ffd80cc77ffdde7a86e3dff16fc972fc3bfe1f8d3cc7ff5a598effc76638feebf7d6331cff8e7f4b96e3fff187ffd919679cf1c612284abf2d40d6e6598696d6d682add769d04ff14102ac94c8a53563364d05a3f4531f5a63a316bf92bed25a6dcf149deb05e7d801671deaa53dad1daa357b64acad7552ef94a1d7d5e694625e5b63d95892518b696d5d4dcf980cc73f98eb1cff8e7f3da714f3da1acbc6928c5a4c6beb6a7ac664ec2dfe91cc72fe7917c25bdffac7f0c13ffb085c79e535b06be7aeae8a92f227ef5b77b3239d000c2a2e256215116772eb37b1df488659993789c883f723c9c091483c58b948905e22c7a209c3ca5a54a5a8119594982c83b6e66a5c8270955a1d2e9f2d96f70e3e687ff837bff8f3f01fffe3afc28b5ffc4238e0807d611ed0e74557d72af917bbd68510b3af1df109494b31dde0fb3867be947fe82107c18b5ef43c78c6694f876baebb1eeebce3ae8eb444714ac42489298a25a83c9fcde7e91e13da00fab30ce3305b9967d2598e3d139d00d6fcc7935ccf042610fbc8cf1aaa1ec57bd5905c7ed66a7c8540b1828e18262b5b2defafa6ca64b4ff321f383f0291e7488e244f71ee85d013adf83a55d01276c9b932b6e93a93f3a0275ac598e327c96c6c1be60afee64a5a989b816c887158494c0478e03fdbdbef11fe6e5285ac1d3b76c2b5d7dd005ffbfa3970dbadb7c121871e04471e79c41abfe5f856e05fea29ad9d729ecbb9fefe2ffb38b656db3345e7947749e977ed5d2563e1efffb5369664d4625a5b57d33326c3f10fe63ac7bfe35fcf29c5bcb6c6b2b124a316d3daba9a9e31198e7f30d739fe1dff7a4e29e6b535968d2519b598d6d6d5f48cc970fc83b9cef1eff8d7734a31afadb16c2cc9a8c5b4b6aea6674c86e31fcc758e7fc7bf9e538a796d8d656349462da6b575353d63321cff60ae73fc3bfef59c52cc6b6b2c1b4b326a31adadabe91993e1f80773dddee07f76e69967be512e28092f19a683603d2f1925bf2d806863f73640eb19257d634921fdd0cff47a2b116a711edb4c2ddf5a5b92bdde31c5066d730d08355b6b39a1e74c016fc99f31f925d0d77cace9d1ba4a7b37c5d6297aac188ead9b72dff13fb4c1f1eff89fa247eb2aeddd145ba7e8f976e21f3f483eb9e1869be14ffee4fdf0f6b7bf172ebae832d8be7d074eaaebc3eb480416c68b318f5bb7659210115398a413a35a2775c4b8a66a52a732f6c42cf5471efbd691ac5a610baea1ca45cbabf96c0ea79d760afcea19ff1e7ef6e77e168e3eea08489cb1c59e648224d6040803a212b1773291a7bb45ff04f2232e60c3ca0a9c70fc71f08c673e136ebbfd0eb8f9c69b53cb409d9fbd1e185459caf1e5f67f48129af555a9a44d298fe47ec78ec0c404b840f1c4c144a0744d725a6e0fa8496f6a3f17d49e0f625f894befbdac3415780dcdcdad03973ecc44e53496c104ab2cab6d0755acd896483a521c981cc5b114b6e710913c2650c958c9ea6b9ccbb23562a43c94b6a6b5b1af68c5e4330b7b8c3549fe4a360a5f766edf0e575d794d2262edd9b30b0e39e410d8b265dfc15ee9f168f15f3a5ffdfdefefff315b6b39a1e7d472a4364af969d925f374ccc79a1eadabb477536c9da2c7fffe9f3e1cfff5e1f877fc4bb9c1f16fe6845ee7f81f97e1f82fcf2fd93a458fe37ffa70fcd787e3dff12fe506c7bf99137a9de37f5c86e3bf3cbf64eb143d8effe9c3f15f1f8e7fc7bf941b1cff664ee87525fcaf6941682db09e6b41d6f3b1df7a436af7f5b50643c98712506ac1ad6dead8e684c26164c9abd96f25aba5cf5a539a63e9aef9653db3124dfe2ebd4cb40cbdae9403f2dadac79a4fd628ad95f2ad6be99f7c2e0f126b7e4d5fc9e62998a9fdd6364e3d186af7a1608be3dff1eff877fc5bcf71dc7df73df095af7c1d7eef7fbe05fef11fbf060f3ef4f0800c93c8254ccc4179743f115df023e6f248958404694ab65e0b3c9faef1fe2c57a88ab92a14dfefc8435d7cb8d295ccd5ac0b1fc4b5a49744ba8a44fc597e77449e6eed29a73e15def0865f81d7fcd83f837db7ec071be64b59d8567006d8b1b0af6084b2023bd95de0efeed35d74e49b2693b0928ea520aef874e49147c1b39e791a5c7af96570db6db7a5b0613b44661a35d45e30903e1c33ab9a135bc0fed1dc5c910a8848a570ce542249f889a4b751f8c9bf43e797aeae95c85514736e23c8e42d5c130c7217fec24a5e0b514d2dfbc6f650b52d50675814f9c5ad28d97f8e1bcfcb3a631cdc0fc236ce59d6d310a94df62f6c444ec638ac66954963ddc48c0f49e2e2eb86c8874daadab6162b9064f7777006b6003dffbc8be1c20b2e81fd0fd81f0e3df410d8b46963f65b7eebb15efc4f39cb4a67bf3ecbfcfdbf569fb5a634c7d25df3cb7ae6ef7f7fff5bbaace763bf1dff8effb11c90d7b57cb66458a3b456cab7aea57ff2b9e3dff13f25571dff8eff924ec7bfe3dfd2519a53c3576d8ea5bbe697f5ccf1eff8b77459cfc77e3bfe1dff633920af6bf96cc9b04669ad946f5d4bffe473c7bfe37f4aae3afe1dff259d8effef5cfce71684d6c33185d6fdb10da9adb7e68f3950fab612d1926fadd3baa7ca28255469d436728a4efd4c26a3bc277faf27f1c6f6a6b4a6f4c2a8c5a414bfa931189339e53e3fd3f36a87c358eed4e2a9e5c97bd6c153b3b564bff582b09ed7ec0a8e7fc73f38fee573c77f1dffbb76ed860b2eb8087eeff7de0a1ffaf38fc22db7dcd6b5c75b4e0f44508230fc831e8950106d3b86f3b042d32291509834c56df440d810a54d28273219a5b75912acb8bd21294ea4a208c36a58839805225f05225dcd98d00470f0a107c1effcce6fc2f77fdff7c0e6cd9b96f316994c94e62261a69b2d5a0bf696a518c5ee83bf621c7eb283912f77c121071f08c71e730c5c75f53570c76db743c42e8acd2cb1a3d0e619c50b6d479293cc8df4dd05ba233e517b403998ecc336467276806f91b756bbbdbc87785f11af72dcf939d9c2fb986c46b21d7424282662f13d201f41ec197e33a12b91c8c41e4711834c6e12f9d290ec40ad0303e9c0bde3f68899e827f78f739bfc63f2988541aed8c53ee2ba94d30aff9918a6ec0e3a6f3936229e107adff83e566ec38a69dffcc63970cbadb7c1a1871c02071d7cd0808006b0f7f8d773ad7bd6396cfdae9d5fa5eff5bc43c6cef0a932f4bb616c94e26afdb674ea67fefe2fcb9c729f9fe979fefe77fc3bfe1dfff2b9e3dff16fd9c9d78e7fc7ff583cb53c79cff10f83398e7fc77f088e7f6b6dcd7ec7bfe35ffe76fc3bfeade735bb82e3dff10f8e7ff9dcf1eff8b7ece4eba9f8cf2d08a1a0a466502d2853eed712b47670c8b5a50db40e23cb172b49ad7525105af6f0772976964c2dbb74cf4a5a795f03d7d25bb3696acc6ac0b07495f4cbeb522ed46cd073a6f81a2682b09463d65acbf6b1c3b7240ba08cc5dadeeb75da87b1fc9b12cf12262c1963f71dff8e7f79edf87f72e0ffa1871e86bff88b8fc1dbdff66eb8ecd22b61cf9ed5bcaed57f58c6be8a5520a24822a4a02ee8c925bc9e092251dbc87e527bbb96aa62355c912884010927c9a0fb2d117892342429b57deb3a24c344ed3b74d5a99a66d611a512a92b7654aae59c030e38007ee58c5f86d7bef635b071e3ca525648067732d29461cce0311861918c3aeac8c397d73338eb2b5f83d53d6de7df6a6262a52a4cc937b44354100b149b40b148c423f47bf91c896d81fd5ffe5ef07ef03d522f65d5729175e8b67ffc0196c1d7141fbc877647ad97ede18a50bcbe5398f6330a9b13018d484f8db48f7390e4b744540b99b405bdbdb2d254e85a1dce292f9910d8501e7245ac9ca38039dcfb0dc20f90d80018b41c94be8288f32aeaa3fd6b859e345fc9057996d1ad5dbbf7c00dd7df04175e78096cd96f5f78ca538e5ee6ec8647857f6b94ce2d7fff83697fe9ded83bc0dffffefeb76c9b927f53e259c2842563ecbee3dff12faf1dff8e7f1e8e7ffb9ee3dff16fad998293b1fc9b12cf12262c1963f71dff8e7f79edf877fcf370fcdbf71cff8e7f6bcd149c8ce5df9478963061c918bbeff877fccb6bc7bfe39fc7770afec3d6ad5ba336ba1694527269e5a5f95375d5f4c95192af87956c964d25b935d925fdb558d4e2a8f59792643df1ad5d4f9d3fd5371d87da7dbd5eced5f6c951cab9923d7aed54fdd6bada1e940e9152ae59768f1d4af2592d876bf6d7ae1dff8e7fc73f14d739feedbcddbe7d3b9c7bee85f0ee3f7e3f5c70c12580758116ab8bae3d1a57bd8a315795c26baeb484f79020d42e7ac215ce69893083cf90f8d28a7685e99b09574c98c17540c493e535b790431bd09656de27b2d16275b5231a914c6e8987f35a22e5b0bf290689ecd312010b06c4987d366f825ffcdf5f0ffff1ff3e13366ed8b0941fbb968aa99c1612bb704d3b88e9633162b380c59e1656e61be1ae7bef875f3de3ff81cf7efaaf972a919843a41fd2c5fea598f3feb2ef14c705cde1167f83b5a21a1313dd38f6dc3ab254692aad897dc528fe6ec573de4396893621b169b6b2020daee53c229b782f53a52bf6839f913ddceab025725d5a2b7225b7b4e48a5c1413cc0d898141eb4bcadfd5e51cce17b42bd94a31926b923e244c11298b779e2badb14fe8cf42100f3906d29f2e463193c124b1305785433db9c218646ce5d8e3de8998cf97f17dfef34f87ffe397fe353ceff9cf864d9b36f9fb3ffafb7fca3bd6dffffefe77fc3bfe1dff8e7fc7bf6dbfe3dff1eff877fc3bfed7c6cef1eff877fc3bfee55c6d9f1c8e7fc7bf9667c990b2f43ac7bfe37f4cbfb5cef1eff82fc95f2ffe676f78c31bde6809d4ca6acaf1193f2f81783dcf4a7aa51e4b9e9508fc6df933e6536d949e9740aaaf6b3e59768fc991f7e51a294fcf29e9b17cab1d84966ef96c4c8e0506eb99d637a6db925902484d4eed79698cc9d0f2ac17a09eaf65593a6bb95eca8192acda6f4bbe94adafd7f3aca4d7f1eff877fc3ffef0bf67cf1eb8e1869be0031ff80b78dffb3e04575f7d2d119dbae74957ecab1ce1875bc801dac47644a52ff49599d876ae60945bd0419f4b89c4c34418e875a30ea499442197ed61928a6cb997eca56fae2cd4f4865155a690ed4e329b001b376d80effb81ef85fff02bff1e0e3df4d0247b8604ac26649b4288039b1fab81a4ae4064b3f97c65e9eb0cbefce5af26521cfe6e1731ed47c3b14cf18ed9df56c603baad9b913c993f89581405f989aa49b14f78b7a5aa4c41c5acffb9f6fffb00e536aa2256ce39d28bbf981037883de99dcf67a9325980205a3976735bda531eb2125bb29daea5fd4c0cccb62afba322034a721ae71857d0927647ba9ff412818dfd66b999fc46f90b646333a8b415f21c795ee438735533f29df3305060f479867edf7edb9d70fef917c1c30f3d0cc71e770c6cdebc4ff59c90f1e06b7ffffbfb5f3e1b93a3edb37cb2f48de9f6f7fff0feb7eafd3f354fa73e2be975fc3bfe1dff8eff923cc7bfe37f4ca6e3dff16fc9aae95dcf73c7bfe3bfa6d3f1eff8afd9e1f877fc6b3f1dff6bf558f21cff8eff31998e7fc7bf25aba6773dcfd78bffd919679cf146b9507f8f29af25489800c692be1210c740673959da28cb8e9a7eebda4a1a3dcf5a3b45fe145ff4fd1ad06a49a313b63467cc57eb5e6ddd980cf9cc8ad11888a7e44d4da765c3a339bce45ed472d292598bf1143b2c3ff4dc527e96e496e4d4aeb51dd673c7bfe35f3f73fc3ffef0bf73e72ef8ec67fe1adef1f6f7c017bff82578e0fe0721535d62df42908954e936ea451ba0af7cc4f7f13a57a032e2c16d04d98e083d512adba764f29a1c7b9685d7aaa2517e2ef609c92e5ce1880948c0f31b9c83f357e139cf7936fca7fff27fc3d34f7d5a9a334f26219927a6390d2b9e8887f58cd8b0cc90aa6c1d72c821a90de1cd37dfbcd43b87f9ca3c5500ebec0d031b641e4a62d282895422b6b90214f47cb998b4767266dc0a8fe5f37a996b241f62cc84399691f52ce32c4953b3f90c047368d0fa90ed6d52952fc895b0720e80680dc8b9c83ec9bd24b99c5710fb8a5a898c06e2fc80b544b154e92db603bfb3afec9f3a7f64ee259219edc1e09c62f9bc6f228fd79c774d6f3b13b032518ca396eec5b5ef31fafdd0c38fc045175d0ad75c7d6daa0c77ecb1c7c07cf99d75047ffff3f0f77fffbc74cfdfff4fdef77f494eed5adb613d77fc3bfef533c7bfe3dff1eff82fc976fc4f7be6f877fc5b3283e37fcd5acb06c7bfe39f87e3bf7f5ebae7f877fc3bfecbf76a731cfffd33c7bfe3ffc986ffd999679ef9c6f5047d4ae2f23c1d30f96c2ce1e43c2db3240f472919f4fa9aad53e64af953925783d7926101738a9dd628256269d492d2d25bb36b6af25abfb5ee317b4b79102a2f91317fe4dc31dbc6703125cfc7745872ace7d69e5973c6f2b764d3d81ccb5ec7ffd046c7bfe37f6c3c51f0bf75ebadf0577ff53978cf9f7c10aebffec644f0c9f3e91b7549f2150ed9c62db51da4366acb1b1d8147b5b5e367e91e1167784e54ba02d928095979c48e6482cfb915a18c878c5723f4f01cb69bdbd9717bc3362e60bffdb7c02ffee2bf8657ffd00fc2869539b5eeebda2f86d050fdadced8646f889009458fc508bd7cfcd7868d1b60eb2db7c3d7befaf5a52db3e43aebeea6d8b869246948e54fa4f8813a6f653e354422e2df726d180a4bedef6654794c92eee4756eed87aba58b00c37d23075bba66f2167f7724bc4e4e64ff397f42c8a4ad9c97a21521fbc839905b0eb28f342fe52874a4bc248bab5b252162abb86d26b71994e441b22bc8dfc65e04b03199da0cf25e2019ab157b23aac3c9b699bdcff46cf9c1d68b77dd75375c76f955a9c2ddd1471f05fbedb705c6ce65e987bfffd7fae2effff2334bf798bda53cf0f7bffffd3f16c392ad53e64af98e7fc7ff7af6616c38fe87f3b4cc923c1c8effb5768ecdb5e63bfe1dff7aedd81ccb5ec7ffd046c7bfe37f6c38fe87f3b4cc923c1c8effb5768ecdb5e63bfe1dff7aedd81ccb5ec7ffd046c7bfe37f6c38fe87f3b4cc923c1c4f34fca7168435a3b510e964290896ac5262e803481b5b1ba579534061d9533b702c3bc7748cd95dda28eb772d1e6336e804b5626dadadc57f6a7297e6d472a5747f4a2ca0629f2567eafada1ced57edd0b1d68ee9b23053c260eda56b1d1496ec9a1d381cff8e7fc7ff772efe77edda95da94fdc66ffc2e7cf6735f84071f7c70587107e5c53868df36b043b49a9376cbb68492a4851f26ab00ad61b20856368ac2be448a427215d993492ea483ab0131c186c93e593e8d34973e58056881f3500656c39acf335927d9b9b4e5877ee8d570c61b7e05f6df6f4bae7e35431b67e803f4049c10a1eb04d7dda79d80758d308c6bf22d11b042afa39963a0e1f39fff1bd8b37b75e95fe8dbd00973528cc56f492ceae476319779d2a88a58dc16afdbd738f04658d5c5577e54dea57d209bb8e21413aae47ec8f9f99b497efc87adc8b186da1b724e0db04a64a8b486fc18e425ce41ff51c6accb933c2784819e446a02caf52ef9bbd8508ea5f699406d04253e440c924e189214e5dee478928d517d273f5656bab68754f52bb57724b2a0fe0fd09063d424c9e8c38c885a48a87ce8a187e1fc0b2e86abaebe164e38e13838e4908386e43365bb1c53ce6eeb1cb4ce256b94e68d9deffefef7f7bf35a7b6be3647fb65c92fe54d29fe5a8ffffd6fdbe3f877fc8fadabc999babe36c7f1eff8b7748cd9edf82fcbb3ee3bfec1d4579a673db7743bfe1dffd65ac73f4cd2e1f877fc5b3ac6ec76fc97e559f71dff60ea2bcdb39e5bba1dff8e7f6bade31f26e970fc3bfe2d1d6376e327b7202c09a825fd94a0d70e8d298957fbfd588db10d2ff924376aaadfeb916d256b2d294afb51f34beb1b03952563bd872fffde9bf525708cd96e014ddb52f26dcca6d2bab1834d1e68964dfaa0a8d931a6afe67f49a6fe76fc3bfe2d198e7f7bdd930dff48eab8f596dbe1837ff66178cb5bde0dd75e7b43aa9493b54641be91b167920bde8b1d510509210b41aa923e48fbdadc3e4dc46bf9491289ac35e3aa4df4cdede6b2cf281b86fbc9d58052952224fe203946c5a525d9ac3f5553e2df44f8596d17f0b4539e0abff5dffe5f38e1c413b0b450225e05aee284c4a84802d806590e29864459220be949c8ff8ef474b839148b00f913554340acbab532df08679df535b8f9a65b61beb2827cac2ebe1c4b51054ce622df437f793f24e92ac638c835ae089662b61095c938ee441c522e641d8d6c074872b90216eb66a29ccc033d06f913ba5832a90ff7378aca6a9c13d91fcc4545fa93fe76f91ffb5c65fb1461cd3c13585e0803acccb81522e346fbc5f113b7ac77b0ac6c35630262083d318e7127622fcfd28e1026fec34af98d38dd7ad35638fb9c0bbaf696071f0cfbeebb794d65bbac4bed8735c6ce562dcbfafd588de2be89e7f2dadfffb0668ebfffd7aef3bfff1dff537497643bfed7ae97dfda06c7bfe37f6f86e3dff15fd2e9f877fc3bfeebf63bfeed758e7fc7ff14dd25d98effb5ebe5b7b6c1f1eff8df9be1f877fc97743afe1dffdf4afca716845ac99852adbcb48e7f07e3d0d072adb97a8c25e47a0e0c7dcff2c55a2be74d3d3c78eed4e7ebd9404bce9438e835638745691d0eeb40d4f668dba6247bc93e2db726c3bad636e93dd53ac7b051cac9310c59ebc7f054ba6fd9bf5e3bd6637b70fc3bfe1dfff09d82ffd53dab70c9a597c33bdff95ef8f467be00f7de7b5f5fb9078658c8dfd057fb4943c619a8d291aaa6c4bbcceb794da46bac3e8543b6294c04a0187b020df415841a556d8baf1b22e50cc82b3c17b5a15c2432a5ca45abb98250a096828b7601edf29f7d366e80fffd97fe0dfcf8eb5e3ba8a6145241a1a51eb61bdb100648642a24f2a00c9cbc48c42524b9006cdbb10baebfe166b8f7be07d39ccdfbee9385f1fc14e7dcc710920d1db526d273acd4d5dd5959c6ea820b2e499f1843de032632c52e907dbb3db1e7e86b8a5332ba8f3f8028e685f112ad2067828cd4703e64ff7bf98908c524a1d0db35c8036efdd809ecf7529f21291e7d1ee26fdd5e52fe91cc9f565542cb39a26c4a3242df5230e5f372cd7c2648630646b9e2d682aab1b5329fc926b69de31cd9077adee7638723aed2967391f7227615acd88f96aab581d83fccd700434cb7b12740727c39e62c3730517179fde0030fc245175f0ab7de721b1c77fc318988a55b7d5ae7c894b3c89aab47edac9db2be3447bf036a6bfdfd0f6be6f9fbffc9fffef7bfff1dffd63a1c8e7fc77f4dce145bacb97ad4623d657d694ec9176bade31fd6cc73fc3bfe6b72a6d862cdd5a316eb29eb4b734abe586b1dffb0669ee3dff15f9333c5166bae1eb5584f595f9a53f2c55aebf88735f31cff8eff9a9c29b65873f5a8c57acafad29c922fd65ac73fac99e7f877fcd7e44cb1c59aab472dd653d697e6947cb1d63ed6f81f10b0ace4ab099c02242bc8b5796380d1fac76cd4bf2d209712a9644b49572dd9ac7bb5988fe9295d4ba0ac47bfbcc7f1d03e59be59f1d4731fed9822d38abd068b95ab326ea5784dc18265ebdec6a6b6f7eb995bcbc55abe3bfe1dff8e7fa8aed3732c5b9f2cf87fe4916df0d18f7c12fee00fde0ee79e7b21ecdeb57b6d7ec438680598db0c329186e431712a3d5b7eb7820094f6802a02015da72a5432b7e3905093c83a32ef45352cd627ed61fb92dcd9acbb26c20afb9f284dd82270462ddff03b005572427948ee6ad3cc97bfe2a570c699bf02071d74505abb94b8b4890840a97a559485aad2bf92adecd322c2ea72fad9e79e0fef78e77be0f7dffc56f8f8c73e0d175e7c096cd9bc0f1c79e491b061c3864c9ec9bb4617115afacd24b7243acd4713efb8eb5ef89b2ffcfdd2bf95e5ef45f74c611c63c104218d8338b03dac692b39c8a328883c4015c3640ee669bdff72ce82dadfa5aa58306c3fd891f3626ee3c7b933b043e86a42a89ea98164738e48125a60bfd95f8e0f3d93b6b1ce46e455a0fb9974a5ce98948b9ce740d5bee8778c7df530ae4096f100446884ce9e0830204dc9f50dc5917304cd6c42937f4be214a74e472e247f500ff99b304b7377ef5985ebaebd01ce3de702d8679f4d70e289c7c39c4891724c7dafe979a5b3b4765e9546e9dd66dd1b933f76765b78183baf6b67ee9473dddfffc339da3e7fff3f36efff9a7cfffb7f38bf16737dcff13f6d38fecbf7a7e48935cff10f45bd967cc7bfe3bfa6dbb2d5f1eff8b7e6593ac6e2545ae3f8efe738fe87f71dff8effa9fae53dc73fac9133c53e6dabe3dff16fcdb3748cc5a9b4c6f1dfcf71fc0fef3bfe1ffff89fbde10d6f7863cda0291b5903bb04b365486dadfc2e05ae346a879425c3b2553eb36cb0f48d5d5b7e4c058a6547281c8ea583b826bf745fcb2aedabd6ab7d2cd96a7d8ff93466f31420c87896f646cfd5eb355e4a636abe6a7da5b848ddfa794966ed5bdb59c38c9e6fc547cb0a8e7fc77f70fcd79e5bb6856f33feefbaeb1e78cf7bfe0cfeec4f3f0c77dc71577ac61586e49a85a82834f092daac35ca7f247824920cc0a00a55e8046652d44c92b3f85b54b76aa9c290d49be6916c9ec7fabbdf21b72964920cae61c24a4728e1ca598b5c2d69163af20b9908871e7608fcd2fff18bf08a97bf3457490a48dca2d034a14dbfbb825521118812f9856d4944af197ce36be7c2affffa6fc2dffeeddfc31db7dd0df7dc7d0f5c7bedb570fef917c349279d08a79c7c52bf5f39e8dd5794d10e9d5de951447f02ecd8b97bb9777f916860b359a7977d6e66c3b682723fd70c917303c2165765e2b8c3da738a2b3649220feb090083167b4916ed251299b882d32c5580ea7e4be29ccc0bebd3c5a81f91738209462427fb4c36a62a559d13437bc99e46ec07df0b343ff90b90c96241e4b0f67bc1ad2a631ceae2ca5cd41a313f4339948f7c8f4987b94297b085f7a423c585412062147ea7b8777667621bc0e07910d5c2eebbf73eb8f8922b60e3c61538e9a927c0a64d9b408fd27b02a07eb6eae700fefe2fd9517a3ff8fbbfecb3b489fd2bed8d9eabd7cb58d4c6d47cd5fa4a7191baf5f392ccdab7b6b386193ddf8a8f96151cff8effe0f8af3db76c0b8efff4edf877fc3bfe6dddfa794966ed5bdb59c38c9e6fc547cb0a8e7fc77f70fcd79e5bb605c77ffa76fc3bfe1dffb66efdbc24b3f6adedac6146cfb7e2a36505c7bfe33f38fe6bcf2ddb82e33f7d3f59f13f3be38c33de58325cde9bb261da51ed9c966539a1d75a3694ec1d4b96b13196d07aaed45b02af35d7923fe5beb66dec9e9431066a4b86fc1eb3698a0d16b0adf57ad4724ceb1dcb010bd896bda535b5d897744c916b6145cfd57ba2e339c5664b56699dbce7f85f3b57ea75fc3bfe4b3aa6c87dbce17ff7eedd70e18597c07ffdfffd77f8e217ff01b66ddb3e68edc604a9b41a65e07dd627ee25a28aa88285df8940c376405f4da8a1fbd98ed8573c02617baeee13894844f7587e23082bd2bf489fae1d60f98f43d4b558fe8325a49094d2115ca83251a21a4598cd1b78cd8ffd28fcf22fff5bd8bcdf166852f5a6195548a21670a1ab80d595a5c2d87584a98ea4159289975d7a25fce7fff29bf0f5af9f0dbb76ad6251ad448c5aac2ee09e7bef863beebc1b7ee2277e0c366c58e94ccb36b24fb1bf91fe17a96a5147a8c1d6867ffa677f09ababab4b9bbb7690b29a538a6fa8ff07d29a5c9771a6b8a7bd46e2912045e5180bbb1b9983d01183166a3ede437b1b22ef6105b496732cf44439aee224db084a9b1bd13e2fdbc1be528e669d22069968c47ed23d99abac875b0ce227ca1c93b18321e612f1ad733ec53f930785ee8c2f923ba3d6871c5f4942635b5ab93fc0d5b2a0cbc3d013cd62db55788344caa3ea55222738960b227f05f649e00aefefd8be1dce39fb02b8faea6be0c8a38e80c30e3b745049ccdffff65c4bfe94fbfefef7f77f696f6a365871f2bfff6dfd8e7fc7bfa5dfd2e9f877fcd7ec2dcdb5e43bfe1dfffa5acf75fc3bfed7734fca28e5414d97ceb19a4d536c70fc97e53afe1dff63f71dff8effd2de04c7ffe8de8f8dda9e5a73a55ec7bfe3bfa4638a5cc7ff7726fe1b39a9b4a952b8068cf54c1b2337c89a230dc3fbd68696803aa64bcbb382a175d492ca8a89b6a9b4916c8bb55e8f12704ae029c5533fafad91dffcbc9413d6fdda61661de43a46530e73ed87d62b632cd768fdd6bc503968adbc9f92b3326f6aa0b5f6aca44fdb5b9251ca316b9f1cff8e7ff9cdcf1dffdf59f84742c8830f3e049ffad4e7e0ff7de3ef2412d69ed5054ec8040cd48055a2d2fad8b74cc339a92a11573ac26774cd6dde22c70b443b42b2a35deace7bd819d5114d8804c22357c7a26a3f38972b70055a8773989c92d690dd33aa9a15424fda028a059347168b5690b4b8c2d61cba0a5321919e8e38f248f889d7fd181c70e08110165c3108893c916c88b9b55b475fe95a16066a1f07b181c59e163ef1c9cfc279e75d94da0586887b80959e12f70be222c0b5d75c0737dd7433ed11e4d673543f4b6d6c20fa15e4ea5f9b376f86638e3d1ab0fb1c57ab9a69528db0b274bef13cce1d8e15138d30fe8bd5d5fc0c947cb69bf345b63c9ca9ea655c150a9f633e2e78bfd8365149ab86991cb4e59863c535de6fd2dbca33446014e5a3dec8fa80aa4281c2628c7d5e32f108e72fe3d04ad221f99d31127b792b44a44ab2393eb1afe816889488b1c5a448cfa8c2168e19b5ffcb55bc624f0403c656546751d355736bc8d71483a6cb88841bb681e475ee75cf53dc18e3cbeb5dbb76c157bffa4df8addffa9ff0894f7c161e7ef8617fff077fffebe1ef7ffffbdff1eff8b76c72fc3bfe1dffb0469ee3dff16fe9d3f6966438fe1dfffab9e37f784f0ec7bfe35f5e3bfe1dff8e7fc7bf9ceff877fc3bfe8776e9678effe9f89feb04ab81415f97e65b8694646add53826d1d0cfa7ecd3e1de01a18ad51f34bda61e99d3ab40c0bf4964d966ffc5ccf2fc996f26aa01ab35bcbb70e1129cbb2a3a443ca1e9baf6356f2dd8a81fc2ef9583a10b46c2d43ce29d95bb34befb395fb63b175fc3bfe1dff8e7f9e77e38d37c39ffde95fc2673ef3057864dbf6dc2610c95148fcc81580f843fa714e64db84ce5610a3f06e224f216944629e885d03dbf99b9f0d8dceebf1dfa92216b6148c82e413e3905c840415d1762e7db33e594528115920919b9a304b6ddbdab8e8882aa1233d2141eb877ee8fbe1252f7909c5b4ab8a450c9a8ed61e5b18384ee495c52a243be2f2fe0d376d858f7ffc33b07ddb0ef4a2234fa1ed6dd29e74dd7df73d70fd8db7c0339ef174e028e3378b17147a7234909d9d3f5bb66c81a79e74225c75f5b54b7fe6d94f00d16a2e84412bc6f48c7e6330229172f8b98c31c75956b692fbd592652c37577ca2dc4973394fc5beca7de63b5cad8bf735efa3d8e39c8fa12313cec49e734c3206783e6f93c20edbcd15c39844c855aa56853d91ec64b220db0624370a1d280f09618ca964e712132dc9eb9396885854f16d412d181bb225020c2ac645657ba418b6ed22e5cc8c2a62216daec5f68320cf84e5bc45df7a30dd67fc74899049630d55e24ab16b02ecdabd07aeb8fc6af8a33f7c27dc78c34df00bafff1770d4514782bffffdfdefef7ffffb5ffbacd739fed70ec77f7938fe1dff8e7fc7bfe3dff1eff85feb93e37f381cff8e7fc7bfe3dff1eff8977639fe1dff8e7fc7ffb713ff8d7ca037ac94b8f29e0456090cd64160055ecad57a2c30685963c11f4b563ddfda187daf060e1d036b9412433fd736cb78978017d5e16901b0644fed5018f36d3d0959cb29794fef7fa81c7a16404a79558a9bfc94867eb958cfb55d537f5b7960d96c1de4fa855793cbf32d198e7fc7bf94c9f71dff43bf9e0cf8c7fbd75c731dbce3edef814ffdd5e7e19147b66562125681c236714802c9edfe9094836b592e40ae8c03966f0d91943a819dad743f55dda1ea5781e433d105484724fd78dd885c0ae23be9d4189276405749886383f2b8951b7e32a98564a4c683b16b2d882416ac1a84549a4d1b37c14fff6f3f095bf6db373dc791badda5f68268502b9200e523f1254d208e1692565af85f7ffb8f70ed75d7a74a5941ee67e85acecd660decd9b3803befb8a3abc8c4f9b6a6f21574f2bb7a54493efbbf71e30a9c70c2f16bf0cbd5b0707085a914bbe5f52a55b20af49b639ee34273796f73a5317aded27592c97908bd8f491eb50794f99064d39ecf88783727b25e2bec95baa2a872c615ab702efbc4b6a57da7967a91e5714b3f610730c606f1ed7255e624ee07fb9f2a5289b8326931e394f38be4664c720cc89705556d8b22566c4f265e2df566e21ce9cbd5de18efeaece26a6e491c92fb5225ad367b99f747b4721cfe4d00395fac7319a7a2fefbefbb1f3ef6f1bf8277bdeb7d70fdf5370eda23ea73dcdfffb0665e08fefed73afcfdbfd607ed9bb45dfb31f5fd6fc9b0f0a3f56a3d567e39fe1dff8e7fc7bf94e1f88735f31cff8e7f2ddbf2c1f10fa62cc7ff508ee31fd6c8b374c9b58e7fc7bfe3dff1eff877fc3bfec1f4cbf13ff4cbf1eff82fe9d4f7f5988f25ce98a2d29ad246eba42801413fb31c2b6dd6d83d29b3244f3e2f1d70b57858495d93b91edb2db09536deb2a5b4a6f6ccd25dd3671d0cd216bddeba5f02abb6b564bbfed66b4a7bb1de67fac0937e59bed69e6b9f4a326ab9bf1edd8e7fc7bfe3ff3b1bffd846ecdc73ce83b7beedbd70c9c59725020e901f5cf567de747e33a9229151388e5d50728b42b95fa9cd19cfc12a3c32fea89f083e91db0ce273ba976c8844e8e98c4df3f03b5dabb8253924b36162d7f233279215ce4b55bcd00722e0e4d688d4526d46d58650f52a2ca02152d3caca0658b48be4cf6b7eec87e1852f7cc1d21724acc4f481dc6650f8960951c9caceefd4caad811b6ebc11fefc831f81b81abbca5611929c569062ba3845b8eaaaab61d7ce1d30db7773470ad304ac41834694d59175903487ad134f3ae944d8bccf3eb073dbce7eff4347d441df714f162db75a6c06b184d0ffd1cad5cf98bc94f67719cfd53d7bfa35323f996024f301ef7105256e894855aa987c94887ef41d9078a63c4cbf696e247b98f49448558b454fb6829eec95f20408bf944f296ff9ece1f382f4a05c26e881814df43dc583aa7ab5242fdb881f2267a5ebf41ced99f57a62ec6d07f51f6218377ad6d233d94611789db8df46eb3ddcb9babad893aaba755bd3a46bc6739bf77e39771153652bac881545b5ab86e29b5b8226e224b5874c55dd023cfcd023f0f18f7d066eb8fe26f8a55ffa0578d18b5f081b366c2073fdfdefef7f7fffd79e7f3bdeffb5b9da361ea57d2ccde7358e7fc7bfe5a3e3dff1af653bfe1dff7cedf81fae73fc3bfe2d5b1cff8e7fc77ffd9e94e9f877fc3bfecb73b56d3c1cff8eff9a3e6d9fb645af77fc3bfeb5ecef04fccfe5c2f524901e5662d7126a0ca096c325e7c692a124b704302da7e4476d7de9d0d132f421a16db6ec05806202d70e8e525295124cafd532b4ad969e9acd5257c9161d274b7669bf4b20abbd046ac0abc990365af74a40b746ed65a4d7d70e093d57cfd7c3f16ffbebf877fcebf94f06fc23f9e2befb1e808f7ce413f0a94f7e166ebded8e44be4a95a1701ecaa0383329067f033e13957a9824026c638cb94d1ade0bb486d76aff023de3fbdcf22dcde7bd41120d91a4981082a3215bf0f742b4d4631f1b929f6533b98b7d5171613bbbf683d091ae988cb2fce7a8238f847ff92f7fa6239c600422dad0d9d977220cdd45ec62c25ad2fda5dceddb77c3473efc71b8e4922b60d6cc33f1a85b10933e360ddb1bde74d3cdb073d71ed8bcafb2b53482706f79fdccd34e85238e381c6ebafee6242f95dce2bda3fde5aa5658292a888a4e294f48d88c2a9325825e54443b6ecb87cf486ec3f943bab2dd2897f2a51515d3664422cadfd89e4f54b8e208e50a5c3207491ee751a0b694332264e5ea4eb1f73d501bc041e8645e2dd72c88dcc57ee63684242b119ee85eaa1847a42f12962b74052281e9ea582c4fc690c94f5906c74bf808225f1bf23712890e849fac3ba5289327310772e0049d2fed27abe9f22424a2167495b344d5b08c7bda8b904544585dacc2d9e79c0fb7df7107fcdc122b3ffeba1f85fdf7df0ffcfdefef7f294beaf2f77f59ceb7eafdaf87bc3f96d396cd3a8e25dcc86bc7bfe3dff15f97e3f877fc4b7d008e7fc73f9836d7f4eaf97a38fe6d7f1dff8e7f3ddff10fa65c79edf81fead66bb52f96cd8e7fc7bfe3dff12ff50138fe1dff60da5cd3abe7ebe1f8b7fdfd56e27f5e72b4e4b4a5401a03504f361d3c2be1a5ae2901aae9a93d1f03714996e50beb98b22153ecaa1d20721e28fda5849c62a39e63d957b2c1b245cfd77aac75961e6953e9f08f9503bc16c3923eebf7986d253f4b07a67ca6bf4b3a4b7b3bd586dae1e1f8b787e3dfb6d9f15fd621e53f5ef17fe71d77c2fbdef717f0a94f7d2655ae417a0657b64af3625f818716a1c024934939493e6309f5b18f60e4285d732b43b6837fe31d24c47065a656b46463f24f1436b5b1aff8d310f1855bc0b571ed1fad6c43a4f9d92eba8fd748b899116105929f58498a2a5bc5053cff85cf81e73ce7f4f47b168072129733514854e60a4b5f425f252886192c1611ce39ef02f8e8c73e09bb77ef59ea5a8145ae9a8573b07d5e4f6c413feebaeb6ed8b96bf7524793ba0ca22eac961523fac755a65aa0b244d00624cb51dc96d7c71f7f0c1c73cc1170e3b5372fe563e526da17b4a9ed1a49a2ac300bb9451f9381f29ed27e707c645c53052b1a1cfb7c8f631e45e52c91ab33aa48d68adce17d085cd90a895054652a701ed2fcb4af4c60c275983f41546383ae7ad48c64275db886d665621f74d5bc421c56f8029163d619c515de52ccb01a18b56fe4fccab8a41c66fb78d86f16c8fb93ab93297d793d9f894c14633ff19b6c5fac2e085ffd39c132537ca8d2178877628a77e85a1e8626e4bdc4eb44c6ca3e3739bf794ecaa7e5bf6fbef11678e73bdf0bbb96b9fbd33ffde370c08107f8fb7fe4bebfffc15ce7efffc7fefdafe7d7e2382556ebd97bc77fdd2ec7bfe3dfd2e9f8b76d70fc3bfef575c95fc7bfe3dff1eff877fc3bfe1dff43ddd6fc92cd8effb20e29dff1bfd61ec77fdd2ec7bfe3dfd2e9f8b76d78a2e2bfe1a4ae059f05c80fafb1e6e8005a3ae473bd46dba11dd50ed592ae14bcda013466b3b44d7eb4edd67acb1e1d4beb60b374977c91f6940ed2927d630781b455e641e9b091df35fbb47e2953da69c997895fcaafa9a39403255fa4ddebf5734a5ec8f9f2bb6403af2bc542fbe1f85fbbcef1eff87f32e31fefdf73f7bdf06bbff61bf0971ffe043cf4e0c31d9969b1e88819a123bd009394988c43eb919cd1526b38fc30510a501f3d4f6de0e8377eb04d1dfe66120db7854b3691ed0b9adf107927cf279b935e5151882b13053177369f273b1b410acaa41cbac76422a035485ee1ca4dc0714b7e507b35a433c516f63f607ff8c99f7a1d1c70d041b0d274dd0403b51d4ca18aad8a3792c496721b94d3917ceebaf77e78f7bb3f00d75d7b2336644c15b630f61c4b2442c5dcb6b0fbbaf7befbe1d6dbee4cfbd1aed97f9937fd33b4b76962aace75c8c107c2b34f7fd6d2ef98e66375a3546009095ca96c17b5af5bed5bf901d984df0b222de1404210c68aab29f13cce018cf1caca0ae8bc6b8840073417548e32c18e096028b705852bdaebcebf98e672cbc8285b63a6ade8f62eb727d4e754e8c8450d6f14fb80bad106f5ee89c20fe937575d6b443b4bd9f290e7267f88c0c6bf638cc3368a33dadfd8fbd18ad8478a494b98d2bb9f7148b626dc520bc5ae6d60a076977d9531943347cc900d286f369fe51876b115b6b63d39337da08f8fbccfd5b21e78f02178c73bde03bffd3b6f865b6fbdcddffffefe1f7d2f6a3dfefe7f6cdfff52b6950756cc74aeeb9c9573742ed7b0e4f877fceb675a8fe3dff12f6d9673f4b59ce7f877fc3bfe87f21cfff61ab9cef1eff877fc3bfe1dff50b5d5f13fb4dbf1eff877fc3bfea7fae9f877fc37bcc80a7ec978a9dc0ab60e8e953cfa9ee5a436dc9a6b2542495e6948006bbba54eb9413a09d77b806a59968c9abd524e2d494a723598b45ce973a81c3e3a4ea518d4fcb10e8cda61a07d90f3f59c319dd67c0dac927f53e5597b3d268bede0df569e5bd8e375968c1a2e1dff8e7fc7ff5a9f9e6cf84732d2e5975f05bff11bff03bef9cdf360d7ce9d5dab4180445e0adda24c924a5574b04d5bec5a1066d9822012a0236d2c1fc062cf9eee39db86fab9da4e8c83ea3ddcfe0d09325cb10848e682da0de6e7320f654c04d925b54544d20892bf98b8c273502e572722fdd29644e822f250a4750d55fec1fa4cf3a57f2f78c173e179cf797622acb4a9fa14cec3780c89573c3af3635779083fcb69e79d7d2e7cf52b5fa30a44f23f60a8355c224811d1a6332311576ebfedf614c66104cb23924fe8cbe6cdfbc00b5ff442d8b869537ab640a2155743227d5dcbb9d0572e230738f6fc017a0e142f80e1b92f49403a1832b7641b49897fb96fba0a57848e78c5f3526e08821eef5b14726769af6222002e8864d708d92dd9927c92e79f3cfbc87799bf4c461c5403136b816c913145ab56c89ed8291aac8bb44e92a8baea545dac73552f6c134a642f196fc6484b1840ac73dbce4cfe227d8857de63d13fb08bf1a2238945de73f229b715e5c136936f1d1e7bd25b92b14ce2eddb77c0e73ff745f8edfffe66b8eeba1bbbaa76fefe5f33d77a6794ec9572fcfdefefffa9ef7feda3be2ee5a3e5abde572b174b7be4f877fcebb98effb5fef16fc7bfe35fda26e538fe1dff8efff2dab1e1f877fc3bfe1dff8e7fc7bff6c9f10fa6bd8e7fc7bf96a3753bfe87d78e7fc7bfb67976c61967bcb134c1024e49d1d818034469de58424c4deaa9ba6b874dc93e2b56eb95c3f7a600bd34b7669306c7d80ba0241787965b9b5f9a57ca2fbd6eccc79a6c6b8d2543da32f520af814fcbe6611dd0564c4b630c7bb5dc582f761cff8e7f4b2e0ec7ff1313ff8f3cb20dbef8c52fc17fff6fbf07e79d77d1e0b9246fe048c42a614eec84d0fd9e8c93aaf620532852552baa4c35b0298461cbbf541d2774adcb1afa638ee7cb3f04854f4c9861795996fca389aaee0ce20414778a091259d2b7a8a0c5bf032dc0ca5733de97f41de1e0430e86fff0fffd6578c94b5f02f3b436a6fbd99cb89674d4896812e1298419dc73cffdf03bffe30fe1fc0b2e58fa8eb6ac249255b7571d4d2b66abd9fe00ab8b3d70ca2927c3777fcf2b3abb22ef4d1f8b86dabef1d2001da90af7663e5f81c396f6fff5dffc2ddc73f7dd5d1c1b6ad3089cab9d0d184fde936e5e937389db41a6bd11790232770b39cc7b80f364cb42f93c509ec4ee466f1b40ae1086f7076df8e29028a5ff8896c4bd2c1b447e91dc860965c2379e37f8a61ce2bc639b5857be073da6b8ea1593b6923ee8aa7e31aed2f376ed7fa074b18e7d1e937cfc8d76b44277c60661b4741675b8e964e40f13d9c87724ae01e306ef09fca57d9acd3201331219acf3b9e9cf32ca9d4055caaebbfe4638f79cf3e0c8238f80a38f3e2a911af5a89de553df99a5b963a3f64e9173ac35fefe2fdb61cdb36266adf3f7bf6d4bcdd6a97ed4e254da9f311ba6d8be9e798e7fc7ffdecab6d658321cff6bef3bfe1dff965c1c8e7fc77f6dfd98edeb99e7f877fcefad6c6b8d25c3f1bff6bee3dff16fc9c5e1f877fcd7d68fd9be9e798e7fc7ffdecab6d658321cff6bef3bfeff69f0dfe84525906a5058eb2c036ad7534629f863b6eab9961d3ae02cab9654f84c3e97bff53a7e66e9b2ecd3b659f6d76c9431917bc4bfb5ddfcad633955ae5eaf6db5e458faf51e4a7b4bf962c5ddda5f6d7fc90e7d2d6dd1f76ab92d0f72cb4739cf8a85352c905b71afe1b2f6b2b3f4687f1dffbd3d8e7fc7bfbec7d78f57fc6fdbb61d3effb9ff056ff9a377c14d376eedda04f2fc10329143ca0944f0e12a53cb071d81896d4d1f1507e16b266570cb3f9c879fa6abf4c4248eb48c093d0003020e905cae9a93dbf5c978c290e0c3addcd8b76403c7b0508d0b9f759583da448ce13685ddf208a79e7232bcecbb5e062bf3d972ce6a26ae74feb6d07367d08eb6fb409b88504d334bd5abce3efb3cf8c637be095898a8695688581372dbbf1845beb27cfa7dd657bedeed1974ade440d0b57834e99f7edff07957b56801071f7630bce2152f4fcc9f3676d591122109b79094455870b8ba78c438dcb730ac4496f79873902a34e55ca235207017c55e06b18fdcba2fcabd8ac38a5a0120139698ecd48abd977b292b63f17e33592ce78fa81c959e53a5b75654d7caf9c76712db2ff21d541e0dce9bd057c9e21c9b915c893926c345e87103e23a2a5ca535d85a73f97b2e3099c24d71966b74ebca8568112acfdb1caf54a14c9cad303c975b6a071a798f0355ae439b8870952a70d1fef37971c30d37c31f2dcfa0bffbdb7f849d3b77c1d8f0f7bfbfffade1ef7ffffbdff10fe63ac7bfe35faeb39e95ec77fc3bfea7cad5ebb5ad961c4bbfe3dff15f1a8e7fc7bf351cff8e7fc73f98eb1cff8e7fb9ce7a56b2dff1eff89f2a57afd7b65a722cfd8e7fc77f693cd9f1dfe88dac09b692ca02aedc702bd96a9b65dd2b6d3c3fd3895f93a16daaadb336b5965825606bdff5266a7f4a878b15676d47e97709245a5f696eed90b3fcb10ed1d2de48ff4a079af6b7e65309246380d307d6d4217dadf957c259e9b975c858b82aedd394783bfe1dff8eff2737feb76ddb06ef7fff07e1cd6f7e3bdc74d3d6445ac12a44794e08438206fd4eff10592657b5c279b406891e2d559dc239abdce26df91be527b216116898cc92282ca47796aa5175b632a924b723a46b6e4987d645189e1fb99519c705ed48c490b571c9add3682ece9179a1dbe62522c90ced8064c3ab7ff807e1a8a38ee864123f2610c10ad9575de52eb4255b9a7c4b15aeb00ddbb61df0f18f7f1aeebeeb1e40c6534c9f86e291ccea095571d1c53e49e9e65e7df53570c375377476a366d58e30d17792bb54018c45617bc1e5f57c79ef753ff11a38f2c8c3a06b3918d2dc14aaae0c1760c7c5a872aca55681ddb4985b1bea5cece20179df38d6e999d88759d357488a224f996895c959b46e0d8e38c7294f64b5b53696ff8096f21afa1da81257229a718e897c58502b4bcee92c37c537e656897caf6552931851fa43d86955cb43fc9e116929708c25be5927e565fa4955ca723b433e83682f43ce8ba11dec2f130ca3f29bdb1c32412f88d8a5b5628f525c684de75aef67237242e6c9ea52ee35d75c07bff7a63f820f7ff863b07dfbf6357b9cf74b5dfbfbdfdfff968dd61a7ffffbdfff8effa16d8e7fc7bffcedf877fc3bfe1dff8e7fc7bfe3dff16ff924653afe1dff63ebe57cc7ffd0bfe0f877fc3bfe8b363afe1dffd6bd2733fe1b2b116b4a2dc7796d6dc3f52658cf4a9ba9af4b875449464dd7989dfaf0d0495f024d09707a4d29fe257f2c79d25eebf0b19ee9185a326a7b313561e55c1d137de059874d4997deff1a18c67255c7a364af6597f58290a314df5ade693dd6ba31ccea75da2779edf85ffb9caf1dff60ea76fc3f31f07ffffd0fc087fff213f0bef7fe393cf0c083bd9ff43c912c943d5c91a9e34a859ecc11877f7435c0958b021b90e462cbb585a88683f7d307d742c8d573d8661e92f092fc0a5df5a026af25024faa54b518c609bfa9224f227cd17d491609f4e9fcefef91ab037b166d474041c2c891471d09af7ce577a7ea57b860866dd640d8d9ef929044779010b39473ddf537c097bffc95a58ff8bc23ace0dc44b841f32188c6830d071cb0ba1612b6b63dbc6db9feac54cdaa33549f15a15fc73f9908163a7f4f7fd6d3e115dff38ae463174724c1cd21b52aa49c9084a1c07bcafb4e3641147f44d37d991b99cc267146f765d52499afadd8abac9bf108fdbe34a255de82c8797db0bb3c587376d0bd46e88a540d2bc95cde9f5305ae1c4981b3967ccebed0bd2075928f83bd57e74cca3fc203dab3881d118a4955dcc2935b0802ef07c799e52799864f1cab4c76ecf37b40b82419b9ad27cdc97bc3ebdb7eefd87f599d0b685f502b932c5b81dff97c9e895e98b7fdf90a70e71d77c39fbcfbcfe02f3ef471b8f7defbd79cff52bebef6f77fd9377ffffbfbdfd2a77d92d763b26a395c8ae918aeac678e7fc7bfe37fad1e6b9de3dff16ff9e6f877fc5bfa1cffb62ec7bfe35fc7c3f1eff8e76bc73f98ba1dff8e7fc7bfe35fde77fc3bfee5736d87752d7f7f27e2bf290543feb6944a61d6f7d404290195bf6b9bc5bfb50cb94116b02c005af6695fe4fd920c6bbe35cf3a3c4ab1d1896e81a376e0c87b52de58e258b1d0bf759cf5bd527c4a312dc55caeab81a804ccdae1a0c124e3a76db5e266c9d4fb5af259eab56cafe58e94317610eafdb2e65afa1cff8eff9a6d8effc727fe91f480d5aedef9cef7c23bdff57e78e491aed24ca0aa36b2b5198e44584279548987840fdafde17ad9ca8f6b462d563b020993a502ad6192546a8347d789a8819587e879a40a5a493611508220d944e8dbd3255fd9a63e187dfb36fa1ee4afcc0df209892133415261c296cca7f9ca0aac22092b44f8a11ffa0138e5d4a7e1e2c47b9acd90b4866421b127f84091a23876bb77ef810ffdc5c7e0d65b6f5f4e41df1375ad9317bafd5844f23590fdf81c9f25c766f0c8b61df0f9cfff0ddc7ec71d6b5a0ff6c92095077298ec5a7e0e3c607ff889d7bd16b6ecb7a5931b78514c7144b2d78caa42ad0a52108be59c91d5c2b87a943621c595fc0791137c9ddb0152bc67820cc47bc8fbd9b55114ad23195bb1b35b62a0e5b677838d88999007245bcf41321412a116a2525acea1d8138cd837aee4c53a73c528e3cce33c88c22ff687fd4e2dfb981446b251de827c647b66d46e30b51f94ba487eaaae257ce6399a8828cf905cc98bcf1031278898271b1a8a5d08b92a18b6ed6cf86c913aa9a523da9c2ae2d1f9c055f1eeb9fb5ef8e33f7e3ffcf1bbde07f7dd771ff8fbdfdffffefe7f6cdeff25fbc67060fdb67059926fd923e796622fe73afe87fa1dffc3758e7fc7bfe3dff1eff877fc3bfe1dff8e7fc7bf7ceef877fc6b198e7fc7bfe3dff1cfeb2c5f1cff8eff7f6afc37da70a9401a65396e7deb7b531254eab19c2b0550ceaf2587a5c3da78eb5aead18758093496cdda772b163560ad1748d2466bbe95dc60f860ed9b965302a1b4c35aa3e3a6636b8d12486acf4bb1b17caa0d198fb18341ceafc99e9aa3fac0d1f9a7315ac365e96073fc3bfe1dffe5f144c2ffb5d75e077ff0076f870f7ff893f0c8c38fa47b8984912bff90cf1aeb00039215683fc1c62edee30a3a38f00f0b26f3a4f948d6e8260fe3117a4206de0d20b0037dcb33d6cd44974c0c629bb9c24f0803b24da3b14e55935aed9bf009c950ddad160e3ce840f8f19f782d6cdab489aa55255a117453a51f6ba244f76770c92557c0e73ff73780442a265e25ce1351a93abd68674cdd00b9fd6317c790ab6d9d7bde05f08f5f3a0b12b90a869588805a1e82342b74f1e19b6d5cc04b5efc7cf8fe1f7815cc57e6f971a4bd0ad0ac39f7793ffaf67e3264714dec705e6a41c9fb83be501b432494e5b9f4d1799df638121989e7893551eab5ce099a5f7a5f59efb34039839f191100b3b3f89cab63e9b334501b448c1d9391b8ad1fd99cabc9f1b6849eb8242b46712b3f4d8c0c310eee49dd59268d4c7e1495c4063a04d1d13a7b999c993e1c8326e4d87767424f7e6b557b46de67d9a6903f48d21a54bda3cf238f6c838f7decafe06d6f7b0fdc7cf32da0dfa96bfc067fff4bf9fadaf2d79aefef7ffffbbfe49b753e96ced39a9e122ef533edb7e3dff1eff877fccbb98e7fc7bff5ccf1eff877fc3bfe6bcf1dff6b6d74fc3bfe1dff6b7daa0dc7bfe3dff1eff8e76bfdcc92ebf877fceb21ef35d2b8d226c80dd642a726bbbe6f016cca4132a66b0a30e4fdb143c6fa6dc995312a0107947db543466ebe965f0295e59b9c5f02985ea7af6bbfad042c0122560e220b643af14b87cc984da55c2bdde7513a3cf89eb661ea0ba5b48f535e0a632f1c6b6ee9f0b36c72fc3bfef575edb7e3fff187ff3beeb80bdef47b6f832ffdc35761f7aedd03d24a929bbe490f9122b8f24faaa0432d0523db2a2a1eb1458974123b1206ce6972db329a2b082b6ce7627535b557cbb688566b3866781dbb4a440144bb39805ce52af9cbeb697ebaa68a404c3ec9f6e0735cc33190b8a24f435585786e8a476c5353c0a73ffd6970fae9cfea68533817ab49456a7f88e414fa34892a35133eb78944b57dc72ef8ec673f0f5b6fb975291ff530110de38436c7b42ec4e5fdc5d25e6c5198cc0ce9bbed6b49c103f73f009ffbfc1760db8e9dd09199d0479cdba60f64d24b8a147455b0305edd077d38e8802df0533ff9e370f0c1072c67ac2ee3853634d9ff556a1b39cb31e14a5dcb4f22b9358356859998c53944f16ddb8e4c86fee2ed76d1c57cd1c624137f737e7115ad2ebe7d35ac99aa7a865e712bbd942f9c1f9487b88fb9251fd9c2f6b10ed9e610a2f1073df45869bb8749e682c88b4c525a50a5a9969ecf886098ede55c837e702538cefb469c0f9cf31d9e60e037c735dbcebe534c02f4e4ab148f30fc8f834690cad2be869e9828c98cd27fc63113a702c522fb08d0b7ad24ff789df48bcf078e7f3ed728de780f49589ffae467e1ed6ffb93e5d97527e8e1ef7f7ffffbfb1f4c5b2c6c8cd92f63ab6db27244e75e29874b98b4ee97625f5aeff877fc3bfe1dff63ba1cff8e7fc77fdd7ec7bfe3dff1eff877fc3bfe1dff60ca601df25b0ec7ff5a7b1cff8e7f696b4dbe1e8e7fc7ff7722fe676f78c31bde58da406b8c055c06ab04f41a28f53ced60c9a6d2736d4b2dd1ac4dd63a4abfa5bdd626589f9a8d251f01a0eaa775df3a2c2cbdd61a4bbfd63735fed65cadab96bc3a1f4abad63bb42d25f9727ec9875a8e947c947929efeb3d28c9adc9b6aef53ac7bfe3dff1ffc4c73fdebbf8e24be1d77ffdbfc1d7bf7ecea0aa148e66d664c244a0efec6bec881a1d39845acd51b5295290ed64321448e206da197bc207cb4d0418aa8c1301721b329659ca0310b625df887c820492556a07c7d57d9864d35571127fec08c20adbc2958a64652220620dc701af17ed2aac6c58817ff5fa9f87ef7fd52b97ba967f2c85b6a34f71ab41b5359439f98344a373cebe107efb7fbc19eebd1b5baccd72e4436a0bd8b5314cfc9af42488ea4b43b991d6dc7fdffdf0dce73d0f8e3bfe3898cd5308d15108ca988e74d5a46f209a4eeaf0b8cc81a38e3e1a1e78f021b8e0820b6181ede39a59aa6c84ad1517ab6d9a1bc98d66b65c9bda248634a7a579bc67b3f93ce74dba97ab3b7544a9664e042e11a166dec90fd4fa10a0afa294f38cf78ff284db106a32e0ac195602935599985884f3643bcc9c53420e1385825893f2997530e6d037ca33943913e4bd44d4a27b89d427cead19fd4e7e405f290c0837f8cdbe20e989f5076edb071d4111e5a20e8e0bc61fe572c5b09cdbf461d2d92ce5749b888a09477324abf5d5b18288bfdc93b4763e17e4b0d013c804090cd43b9dd7caca611c8344d29af52d1c71ce9edd7be09a6bae878b96e7d7a9a79e02871f7e18f8fb1f4c7d353fadfbd6bbc9d26badb1f46b7d53e36fcdf5f7bffffd5f93eff887513b4bf7b46ec7bf3db42d25f972bee3dff1eff8874976d77c74fc3bfe6bf21dff306a67e99ed6edf8b787b6a5245fce77fc3bfe1dff30c9ee9a8f8e7fc77f4dbee31f46ed2cddd3ba1dfff6d0b694e4cbf98e7fc7ff6381ff0627eb04dfdb44c621e54943f87e2db1b533728d05124bb795885a26cf95df728e057c1dd452e2588093f7747cf4fd5a42683fad24b2e68e3db39257ef9735c70287a54bc751aeafed83cecd922d32f1c7006d8dd23ee875da5fbdaea6c33a086a60b57069d92b65eae756fe96f659ea71fc3bfe1dff4f2cfc2369e3ba6baf87f7bef7cfe1a28b2eede4123186e7276207122a66545988eecf67b38e1c21085b4c941af84e04914ca0123992f706ba8a3d0b4178019423d62da85a55f693fc602215db05bc163f54c507af07558ea023c830e1238a18cdc82fd625ed665b93ddbc97a4afa1b66b871d7a189cfeac67747eb0e446d8a7460c5d1c521bc1a5c7bbf72ce09b679f07b7de72ebf25e5024a9d0094b2a63a713495e48926a42ff095c0d6a068bd5988853fff88f67c1ea9e55c00a581d2966065306b62044dbf7dbb2197eecb5af81a79efcd4b4a52db78e5b44585959e9f685d7b45df52d205ad26c4e559ce86ebbbada118238a7e97e8a1316e9c2ca574d77b3a16fbcd7500eb602d3494b08d91ed69a2b30d1fe35a2f215ef61d24febb3b59c9744229a23594c54d04af6476a99c839205a02a67c1332e6d452938989996425cf30962d08837d1cbbaa5adcee3032a14c608165490cb26f2dca1615a6706e269635a2bd23f464a9549d8b486234919776b9cae703e3984953fa3d157ac215c79fb379b05ebe9bc29034c72d4d3bb217998379d07479807974e92597c33bdff11eb8f9a6adfefe0ffefef7f7bffffdeff8b7fd74fcf7731dff8e7fc7bf3dd7f1bff6dab24ffaedf8dfbbe1f877fc5b7a2c3b1dff8effd270fc3bfe1dff8e7fc77fd94fc77f3fd7f1eff87f22e27f76e69967be11268e52c2e86f2b48639b5a5ba31d1f936d1d4825209636acb4d16380b636d01a16e8f4e15bba96f7c66236c53f6dcb98dda53c28adb30eea29f950d33f365fdb59b2cf8a4329b635fd63ebe4f3d2b329734af64e952fe5ac7738fe1dff8effc71ffe719c7bce05f0bbbffb47f08d6f9c9bc839e959e90f7af1ac511584780e13a7f80f06265f0ce4f01f1334373d83beca50143a5af17b412412be1fa4dff8cde411e8892bc96ff93bc64cf2c26a3e209f81f11f342164bb74d5a419d9c1c498556813b1e5bb5ff152f8f9d7ff1cecb7655f720fe5b1bd913475f7b12a564c0d0331b6dde7c69b6e83b7bce55d70fdf5376054baea56a92255ece421f12ad07e60852ade1ee8c968816cc298adcc67a962d5030fdc0faffc9e97c391471c9974767b0dd591c861a9e521ca6ce0d0430f85f9ca0a7cf9cb67c162cf025228c3ac6b3f195bf2b1e95c03719e119baa51988a223752ee445e4351223fb9d560200a5710c4a1fc21e253107995f6d2a8b89695e07d51ad2de511b5f5cb3b25f281f73bc922d983417a5baa8e06b4079c2f288b8962390bc80e2628c99188523c97725fcecfbe849e2c18c41c245131f12bcc7a2c7275ab86e586b5ef9e409802c2718e37c5a609eafdda841ef321e48a5b416033e18be7939c8c77249dc5ae5258c33ac8177c9eaa712d65726ce3a2cdc4388cf12db7dc06d75e773d3ce3194f87830f3e08fcfdefef7fad7f6cbeb6b3649f1587526c6bfac7d6c9e7a56753e694ec9d2a5fca59ef28edbbfeb6e44fd9efb15cc6e1f877fc8fed8b65a7e3dff15fbbaee1638a1c6b9de31f06f21dff8eff9a6cc7bfe35faf29e91f9beff8b7e54b39eb1d8e7fc7bfe3dff11f1cffe61abdcef10f03f98e7fc77f4df67722fe530bc29210fd291d02f2ded8180b72cde8d29a5a10b59db5fbda0ebd09b5808fcdd36bacc3756c8c1d9835bfb40c4b9e06514967cd9f9a1e3dbf94e4da86a963ca9a313fa6de9f22a7b64f2599253c8d1d4296bed2a1666141cf71fc3bfe4b3a6bfed4f4e8f98effb5ebf606ff975c7219bce9f7df0e175e7809acaeae76f3b9151cb58b633205134f341ef22711706050d548da86bf2294ff986642d7c0decee8dc4e8d9f643b702edd67bb9880c2f741e5661854fc19124340ce4199a26d1dde97a4b328ec821c93905aeffdc22ffc4b78f92b5e0ef3195648826c5fe85849cb6bbcd9a6f680f8cd6bb1f51f8afadbbf3b0bfee2431f816d8f6c4f84a4a5146a2588b6712cdbe4dac64d1ba06d17892805a97d6120f25317c0085cb9a885071f7c008e3bfe7878c1f39f97aa3a616bc051a42465a82b265b423383638e790a5c7ae9e570fdf537e5f670dd73ca97012f48ee51db93779a613535ceb1d4b63015f88a7dc5a3d857faea885890d7b6642213b4f83b8adf72cf06d70a1b9c3fadc823cec756545f6332d22ab71f94b80ca12799812017718bc2285a11c698096833f51ecb185104afd807b6bb5678443d8c65d9ceafb327f6ed4589acc644c6ec3f578423bbb1ea1de7896c8f18c9065ec337732c045ee5df0132dff2bcd091af24de1a225ba539b9bd235540a30a7d899cd50505f8ebf6dbef84db6ebd034e3ee94438e4d08307b1abbdb74af3fcfd0fe6dad2f7586c4bfed4f4e8f93a6e7ace581cf766cd981f53ef4f9153dba7c7f2fd6fcdb1f252ebb0f4e8b5f2e37fff3bfec7fca9e9d1f31dff6bd739fe1dffb575f2db923526d3f1eff877fc3bfe2d798e7fc7bf5eebf877fceb75357f6a7af47cc7ffda758e7fc77f6d9dfcb6648dc974fc3bfe1dff8f1ffce70a58da282b2053045b41b012c392594b9ada6fcbee927db5efa9a366bfb485af6b1b57d3513bb86a8963ad1fbb9672c3c48360bd71d3ebc60e83d241509aa37db7625002a2ce9f926f63fb56f2c3f2dbd2b537b95fb2c1ca05ed3318363afeeb630c4ba5d86b1963b63afe1dffb5df6d1be1b24baf80fff25ffe1b5c72f16549e682aae468f9ad20452c6f0c4858f89d4826f869457c7249a6902b0f65220cc7178623e78fb457fc3133b04fd8899f864922743f3d9379a8f712bfd9fe180755bbd816d6d510d903c84f33a6797d0b871e7a309cf9865f81638e391a0b5511612a52652626a08444a08226a61685a10b1a60b5abd5450bbfffe677c0d9679f07d82ab009487ae9d66058f193684c4b5d1be633f8eeef7959f2fd81071eccf3207f2171a52331c5b4ef0bb8e79e7be07bbff7bbe190430e86454b6426a88c40a6255f3b1bb66cd902c71c7b1c9c75d657e0de7bee4d9589f2ced1de76c49f6e4da2fec438a8d485c42d26d834628fb07a566c81aa297579c5e4ab8ee003b94d5f202213ef27f05e921dbcb7a9229a3ee3c9ce4690f5723e858e8495e7917d326702917ff23948367404a845aaaed672c536219f094331f4d5aa72bc42b0b10d3dd6306e8da8e805b1af26c7f9dbb701a5f544664acf14d189cf148dfbde67fa2612d79ed5d5e1b94771c08fac6e36cb2d0bfbf742041854f0ca0446bca6e701faf68eb9d25dc661a4769b8d3a8f851fcb395bb7de0a575c71353cfdb453e0b0c30ead9ecdfa4cb586750e97e4d5c6d8bbd47a175932c66cf5f7bfbfffc77ed76cf0bfffc15c57fa9e3ac6b0548abd963166abe3dff13ff6bb6683e31fcc75a5efa9630c4ba5d86b1963b63afe1dff63bf6b3638fec15c57fa9e3ac6b0548abd963166abe3dff13ff6bb6683e31fcc75a5efa9630c4ba5d86b1963b63afe1dff63bf6b3638fec15c57fa9e3ac6b0548abd963166abe3dff13ff6bb6683e31fcc75a5efa9630c4ba5d86b19f869748007ff871605420bf45a01af976bf4864a3952073fd37ab433565294826f2596fe96f3b46f2539da3669b78c81b655c76a2cc1c70e021d6b39ac9858f2b4ddd633fd5bc6af04a292eeb143de1a565ed4f6ac944bd2b647638796abe794f6d9ca47699b8ce9d86152cb0d3dc7c2b9957f8effa16f2539da36c77fd92eaddbf1ffe8f18f448f2bafb80adefce677c035d75c9fefa78a57016591bc453b2098a4b5dcfe4bea40fba2266f884a51c2c6f41bc9335485676c6ff09af5317123920fc9505adf2ebaca4acd5050faa4b8085dc9276e6d0890abe8b08e44be593ee3b671590ed9cef324d90b9f93f7f0f4a79f0aa73eed69e9baaf3cd40e83d14dcdc4a436c710e0ce3bef82b3befc95a52e5cbbb2fccca1e3b671ebb6904831d8ad6df3befbc04ffd6f3f01a79df63488a00829a0dad9c5eede65975e061ffdd82796fe8554cd2a8cd4c00a495f93c86081852ffd79e1f34f87d7bffe67616543037b16bb531a00eb0bbc8f213b1b2886fa7ce3fb0bacdad4190e4854431216d01cdcef76911c80b42d2843128948a66ce117445ec6d0eb67bd8dfaddc528e6bc08f916b51d447d61f81f248dc2015f37d4eaaf299c8991e7939e9cdf6a4ed28dbe93ec86f0c015aa723e87908859f8bd4a6dfff03bb71485b5ef42e95fcafbd92cdbc9cf597fd6456d0be56079ec6b9b31d13dcf55aa84ccbc57e4e782ce03c6736aedb9cc870edb8bc1398058676c72b53dc88d293bf9e8fb45175f06ffe377fe00aeb8fcea35e735db0de0ef7f698b946ff9a587bffffdef7faddfb2b9164bedbbde07fd5bceafd9a4e738fe87b282e37f8d6e39dff1eff8b77c73fc0fe795e63bfe6d3b1cff8e7f699be3bf6c97d6edf877fccbf9359bf41cc7ff505670fcafd12de73bfe1dff966f8effe1bcd27cc7bf6d87e3dff12f6ddb5bfc37fa8616a013d44a2cbdae96a0dae11280e5b56587b6794caf9e3fe5fed8a64e19d26e4b47cd173d4727f21459d6e16a013f540e4c7daf765fe68495e0da06eb2092c3ca3dadcb5a530290655b6994e2518ac198dd1a2bda5e9eaf416ded6b4987b5072519169ea53d8e7fc7bfa5c3f1ffedc53f92182ebde40a78d39bde0ae79c7b7e6ee3957441476e08d4820c8c5c6a44551b6d233fc75f4c8c995998247db2a5207e23810462cc360dfceefee048eb66d95e8096e7f035db416492c81fb9d724af187f615394be50459e1447b233aa7d9bafcce07b5ff53db0cfbefb42d7474fea8add6f31029983adfd90f0b6ba54f5b77fff15b8edb6db9753e7a952d9a2ed5a0de2da98a85c9137049efad413e0252f7d31bcf255df9daa44b5c85a4a31c3af36abc62a5a48a24279f8f9c0fb3f08575c795522b7c07813c2a5305c3f4b7676bad1d7003ff3333fb1d4ff22d8b871b6d4bd48767604bbd8db9c98546d57d18af20a89335cd18c6387842ab4a7738172a6e97681abab7539447bde76449c44c631709b72012b3fb56d4f74a25ce1f871fb48dee396f63b80c06c6760228831010f07927c02080cc8f355e0b514dd84579abfe03698a48b04f5fe88dce74a724c22e475ab54996a46f84d76cdba2a5c8938850432f491da130eb617e5803a9f043eb36f0099e8c5ad0d81d6f2f9c2d5cc30cf640c784e14fb959e2f7fafcc67d9a77c1e714b5249aa63fb48ee4c54c96ac5b981cf3197b0b5eaefbde92db0f5e65b2827fdfdefef7ffffbdffffe77fc8fc972fcc31abd8e7fc7ff94fb8e7fc7bff5dbf10f03fb1cffe5e1f877fc3bfeed185a321dff8e7f391cff8e7fc7bfe3dff10fe63aadc3f1ffc4c27f23832617c9056346e824d3f735c8ac24b3924e2791a5bf747058e0b39e59726b415b4f72f1fca9bf75f259a304e2921d5a8e75306bb9963ceb20a91dc46349aa73ae967f567e6839d67a4b96e58b95ff53805c3a28b40dd6281d9a5a7ee9002c3dd7f648795ab7c69a7c2ed738fe6130dff1eff8ff76e2fff2cbaf8237fdfe5be09bdf3c2f55c7c1e7b2a20e4a451216b00d068682d093bee937cb49848926f4a4289ca7e464dbd84ee8aad9a4df308cb92458247b8db3a511f6ca6f2679c9aa5d6c67235abfe938e6368b51fd41cef3581edb41761d78d081f0f2effaae6e6a5ce4967bc9249c46ff0c2249f791b0f5c0fd0fc0a73ffd9954fd2a327d2ced0be70cd3cc626a71f7aaefff5e38f69863e0652f7d091c78e0013da1ade9da19f631ea62ddf90589e0f5f6b7bf0beeb8eb6e90d5aafa7dd6391c7a7bc906b4e9a8a38f80337ef597e1f92f782e30412c74d626fde874964d2d2281e22a47da8b1ceb41fa913f9d6d98b320f2475651c27d5c5085b6c11c85d7dcee90f34f54744aed028908282b3f658c8ab6941207196f5c918bd67324ab6742e85a24e67385a34c043fd6957c8b31b7df5bddb3a7c70a63575486933b06f2dc02718618e7f2e0dc9064c9e56741d8e13d93672bcb6fa82d24b73cec5a140accd287f7a68b29858df690e56542170ccf0c691fef53a3308cf3772ff305cfbadf7ff3dbe1daeb6ee86d007fff8fd9e1efff27dffbbff65cdb23e569dd721f6bf12fedb3e58ba587e56b7f79beb5bfd23e2b371dff8e7f29c7f1eff8b7eca9d9aaedd5b1b19e3bfe1dff729d96e9f877fc3bfe1dff8e7fc73fcf77fcdb321dff8e7fc7bfe35fdac1cf4ab639fe1dff96afa5d8d66cd5f6ead858cf1f6bfc37d6a6590af44697e65840d5c34a281d246b8e94691d00b5c4b2124deab51248db63014dafb17496625392a313af94d4565cb4afd206eb102af92f7d2e255c2d494b3e495d168874acad6125b49e6be993f6e86b192fbd46eb2a0d0b1fa5178625772a062c397affe53c1d5bfd5cce73fc3bfeb5cf8effa1aed2f8a7c0ff1d77dc09bffddf7f1fce3bf742d8b3674f7e0e388789312174ad080186c415614f947a702d502bb0ee26ec595ded485c823cb11024a7962a07a56bfa00eb07e0667d03e2ca4cb64853b98a1f6ee1975ac5b13f8c69212bade1ca3a626db285ec0de413cf6f292ef87c7541d57bc88ee45fb39c1762aa3e75fae9cf80134e3c0e5237c7860931143124282da7c7b4461278f083763470f5b537c3f9e75f8c93e983b141e20d5637ea2a6135017d8870e00107c00b5ff87cd8bc79131c77ecd1f09c679f06b359ec74435719a869421f32d41366b0482d05e7f0b9cffd0d7cf5abdf58fe6e9035932a6bc5409fd9f2d7522fffeeca52b5ec06c4454c959066cb1faffcee97c22fffbb5f847df6d9048b3dcb7501493198574b5dcb25f354dd8877a18beb6c3eef2a9e89fd04ae30962cc10f92a9b0aa16b6ff0be95e9362badae329921d0dfbdbac791fc97b397739f789bcc3f906d4823252de4a929e245de1da643fe545cb18213d8984c4d5bd28ff193bbcb3ec7b6a954895aa50f74cb6b6a47b834a6139a7292fc9c7dc7e91e6ac72153be85b87a2acf932f6f9ac2433f25e08fc5b7f23cca8a218af0de4ff82da0426fc2efa0a5b5c7d4b5630cbcf50269f09b16bf138a38fd44f28c8c42f3ea740c4856d1954cf036a49baccdbbffffb2fc3efbfe9ad70efbdf7f9fb3ff8fb5ffafa9df2fe2fc9d1fb2fe7e9d8eae7725e69bfb53da5393ac7ad79965f537d979870fc3bfe1dff6bfd75fc3bfe1dff8e7fbdd6f1eff82fc970fc3bfe1dff8e7fc7bfe3dff16ffbeef81f0ec7ffdae75296b6433fabf9e0f8ffcec07f53da34eda0de6c2b70a583c272de028ce5b825573b2ae7c84049bd96be9a4d35a0497dd666941254aeb37c2d1d48f2db926325552d416bf6941258cf29ad29255d2dc1f9be3e44f4c1a46db2fcd1364e3914a56cebf0b0ee5b311e3b98b43ea957ded33e5843e3b694bba53dd1f63afe1dff72ade3fff1857f7c8695affeeb7ffd6d38ef828b1389080930c91e9ccb1353e5aaa6234b107923bde81329866c44799d02140c81da8949f207709b36b6819e73a5a9f9cabc6f2b163b520792501a6a6d26ab4f2555b436b58063df2509843f682f406e0f88eb132186e7914d6bf2829e35442ec9159488c405640bce9c2f6d643bf39eb61df90a894a2f7ed10be190830f4a319887d4f48f23561c81c2898cadbffffb2fc1dd77df0789af12bbb5a83e134d1ae8da10b60b38fef863e199cf7ce6d2bf160e3c707f78ed6b7f14b66cd9dccdc1ea5b4da098b322cecba5ffcd1ceebef35e78fbdbfe186ebdedcea5df1813dc93191093a8b30cd956697decfd889413a9a55b031b37adc06b5ef3c3f01fffaf5f85fd0fdc8c3b007d51a9904858dd55b7d7bcbf5c89ac15fb0062bfb07de57cd6e42a660de546de17ce6f804115b7bc2fb123c8c98a4a40f9b120d214087929a7963996e68b5683407993e58bf386ab3c45512d6bc18424b20de5cc44e5af4cc043db96bf13514a1014176417e36e956ce5bc5b25b2d32c340332d7e0ec0418c44de6ebee3d7b06d5b5920e244d22c170f96ccfee3d9d8cb4c76d8f77d641f601d9cbe42f2661e1ee318ed3b31991c3880cc8a44c266236941340e7118e552288261b976715b757cce72ad9b2e08a7b743665e265e72ced5b4c3e9d75d6d7e1377ff377e19a6baef5f77f41a6bffffdef7f39cfda136dafc681f64de79b95975a868543cb46bd56cfb1d669798effa12cc7bfe35fdbab6d71fc3bfe1dff3090edf877fceb6b6b38feed358e7fc7bfd4e3f8b7d759be3afe1dff960fd670fc3bfe4b321dff8e7f39cff1fff8c57f634d2e25a7a5c0da380d24cbc0d2b09247cbd7df251ba47da57ba5c3c41a5682d56cd4ba2c39969f251b6b7b5502f914b08dd959028bb58f253d357d25e05a7ee87d2fedaf3e78ad43c88a99d4a1f7b894b75a5f291e962fb551cb8b60bcd84af9588a97659f655729fe7aaee3dff1eff87fecf18fa40aac7cf5898fff159cfdcdf3ba6a3bf4619208a01eaa5403ac238441c519b605892433228244d2b390b15b7ecf56563a99742f55b9e2766418170899ac913e4d18b47b6bd4fe35a2621513bd22fb01905b12caaa43f93e1148386ead6871c62ddd58e64c10cdb822d89caaf120f92b55930a7973720bb6740b9fc5169effc2e7c3860d1b204d4412539815364810b348e623db76c2b9e75e00abbb5be89ad2c5142b24394122c3503c66213d3be9e49360cb96fdbab6744b3b9ff7fce7c121871e0a8b760f1162da443e69a9fa10c4be9ddcea2a125c1ab8ec8aabe02b679d45edec9abcdffd0800d0982e747b89d5c966a9aad2cffcf39f841ff8c1ef5deac6b8a18bab94671d9faf8d21e7c3408fc06324b2512231a58a638b8e8044f75b9a2ff1c8642249ace28a549918a8eccea49fb41544e66bba3803639bf32c085220c0e0fcc87817c41f6e6398f11a455536106431e82abbcd696e6eadc9510fa1af0645d864bf80b096702ab0cedf2d3f13b862df138990ab79858ee8d610ae9990c56704e70be27721300cc28728e2ca3163bc77670b93edfafd4967c932675af2238af328e514b531e4fd95c4478e773e53f023485c29c6bc4f7c864147eafaca59df800ffdf9c7e09e7bee057ffffbfb5fcb79b2bdfff53ded831517a9bf142fcb3ecbae52fcf5dc929ed25e5a43cf2d61abe6afe31faa6bb5bd63763afe1dff8e7fc7ffd8fd9a9e9a3ec73f8cca95f7b40f565ca47ec7bfe3dff1eff88f8eff35f71cff8e7f3d1cff8e7f6b38feebf76b7a6afa1cff302a57ded33e587191fa1dff8f5ffccfce3cf3cc37ea442c015e2a2d25af9c2765e93556c26ae36b20a96da265434d566dbdb521d6a68dfd9e9a74567ca7f88aa314537e66ed6b294696ff257ba6d8a781a3e56adb2c5d53f3a2b476cae167e5a8a57f6fefd7005fc360290ff5415acad99abee0f82fae2fc57dbdbf6b7a4b3128e55569bde3ffc989ff5b6eb90ddef1f6f7c2a73ffdd7b073e7aec1bc442a01c8d5a978a0cc44bae078cf9a44044a7f30f07ad44bfe32e124dda3b5382b55b5624294dac7962ad7746bc8178041ab3549486944959b2487893532d7446cf87ac6d58d449c66a28215dbc3fa64dbc5c831a26bb4044950015a41a8a186794b7f0e3df450f8f55fff35d877cbbe54380a49338ba4bf7392a574ff00b6144c6dffb0afde1cbe79f679f0bef7fe193c70ff43543d284531119720afe9e2355b3efbb99ffb17f0b2ef7a69aa1285370f39f860b8eeba1be08acb2e87c52a1ada74eb78cfd2579b5bdccde633d8b37b37dc7cf35678c18b5e00471e714432b5cff514f96ea94af9d0f5764bdbd72d0238e080fde0e9a79d06575e75156cdd7a53b72f0ddb304b8bdab8c8adf302e589ccdb20eecbe79c17336e27487b1c442e6639946f1aeb51e613e50edf6ba0afc4c479058c61fce6ca5d9c5f94dbfc3bad8b1d418c6dcf6d09297fb22e9183ab44226a080bdcd2937d8884a9344792bac8ff19e18eab5081f06d26da1acaf80c5a32b65dee733b45f6b735f6846d4818629c2ee5f3da44d0229f416032af15ad0181740efce3d68b29bf04e92f76d5bf6495b1a8d67439d009efce9f2e3b6218564ddbb56b175c7ffd8de9fb59a73f03366dda047ae8f740ed9d610d7ffffbfbdffffef7bfff4b715fefef9ade520c4a79555aeff877fc9772b6a62f38fe8beb4b715fefef9ade520c4a79555aeff877fc9772b6a62f38fe8beb4b715fefef9ade520c4a79555aeff877fc9772b6a62f38fe8beb4b715fefef9ade520c4a79555aeff877fc9772b6a62f38fe8beb4b715fefef9ade520c4a79555aeff877fc9772b6a62f38fe8beb4b715fefef92ded482502ad0bf4b86f3bcd226b20c29cf922b7f6b905a7ab4fc31fb4ab648bdfab7e587255bdb65add37ed46cb6f4713c74ec6afaacfd94f76b3e8ddde3fb3a416b40b10e7f6b8fe51aad4b3f9332ac78f09c9a9f5aae8e91758868dd5acfde5eeb83aab6573cdf8a4d09bff2b9d6e7f887a25d8e7fc7ffb713ffdbb76d878f7ee453f0d9cf7d01b66fdf91abe1b0e581da827195984c968a3df9a8ab18d5662207ae95edff723e8bb683b1332ab7868b646323e2c424905c458a7d60d934bfd1fb4c7a929c402dc8484fe018ab3dc92d05c9a655aa808536b6725f3896b436558a2282cc9c5a3662f5a9ec17db4224a7d39ffd0cd8b2ff7ec90094d6d9b0b67a14d25b5a10d583da0676ecdc09e79c7d1edc7dcf3d1d992d191c931c2462a5c8a7587471dbb2650b9c74f2c930c3768ecbdb48c2dabccf4678ed6bfe191c7af861cbd820018b63d56b4e214cbe76558db04ad755575c037ffcaef7c2b6eddb5169673356abca34a7b8a608565755ab2743e112fc9c74c23170c6affe7b78def39ebd148155ce703f17c37ca51c6b45bb47ce979cdfbcdfb41f2d5580027a2ec95291e6733e61fcfadc5de48a4bbcbf292762ec09439cb73427521eb25ddcd2320a395c916b90a384994895bb06e71bc9ca3b41b2242680f115e3804cc858c96d0d193f9cf722cf65953088fd0ef20814176e393868db483ea72a776c33c0b05d21aec16a58a24d2757cf6312d8e01d15d7beeb64bbd26c9f20b305c275cb3e04ca55f99ea0bde62a58694fa8d725de4b92c5918ef7b81de28e1d3be1139ff82c7cf2139f811dcb73d1dffffefe7f32beff2d1f78be151b2b963af65a9f952f7a7d095f52b6958f160eadf9d21f2b374a7b3b665fc916a957ff76fc97eff17dc7bfe3dff1eff877fc3bfeb51e69b7e3dff1eff877fccb7bd67cc7bfadcff1eff8b786e3dff1eff877fc3bfe9fd8f86f64e29540a047c940bdde4a74693cffd687869c67c9aad956b2571f1a1a3c56a0ac353ae12c1b4be094be59bff5a16601dd02939554b544b2fc97324bc9529b67d961f9a8e365d969c9b172b4369fd74c1d53fcd0fbafef8fe1a736a79477f2b965432dd7c60e646dbbe3dff1eff87ffce07fc78e1df0fef7ff397ce0031f82eddb7674a4048e7be84944bc26ebc06f2436a01c41a8e06a3fd88a8f8911d21e6e2596ae4917fb96aa16d147ea6c45ebb7c8320559aa51b9c673732c62cc558906add662cc558ef0f78ce570fc411078c41e3069278a3669644caeb6d489e965b1d32bf31578f18b5e041b366c44f544cae13d523991ca497584a248c4aa071fdc065ff9cad761d7cedd4b1b4257252a765a9110b58c2234cbcf8c48739bf7dd0ccf7ad6335325ac4e0b56b60278d18b9f072f7ff9cb924e243f757119e611ef559bf66d013b973a3ff5a9cfc0273ff5d9e5f522e9028001f90d5ae5426ab118a1679185e4d7c67d56e055dffb32f8b55f3b034e39e5e4a53e2422612cdbb5e702e504b7d90b9c17b12311456133304e68ceaac8e786e66a8220af977991fde13d54f9b5e69ca40a4f3247d2ce722e939e01367365b74e8fcec9b45b347f402413f7788eacb2958992a1fc1fe10d40ce69c60b8fbe55675f658ee7ca9681107a6222db134210dd37c55946b167592062cefbdb408ff7245f9fe7b487896848ba671c17ace2157b1256964976ca73afabc427885c10d690f422b5e37ce8e147e01def7c2f7cfce39f4e64b4d2f0f7bfbfffc7c6e3f1fd6fed9fb6a1966b56ccacbd2ccd2b8d525e59b954f253fb5adb572d6b3dfb2ad73bfe87cfadfb8e7fc7bfb6cdf243dba5af1dff8e7fcb47c7bfe3dff1eff877fc3bfea5af8e7fc73f3f77fc3bfe1dff8e7fed93e3dff1ff64c3ffec8c33ce78234c7480afa73cd7bfadc4adfdb60e9ad21a6bed58029536d45a2b37b076c88cd9551ba5b8e9e7a170704a3be5fda9fb35a6cbbab6d6d4ecd1f3c77261ccae296b4af3c6f6d25a63d93016173d577ea61e78536c1ef3af76e0ac47afe3dff15f5b53b347cf1fcb8531bba6ac29cd7bbce31f2bbc7ce6d35f8077fdf1fb13f98aabe5045179860712129800937e0bff3af2077ecffa3fe8930c212284dc3e6d41557f72851b507f6cb09f31e6766a79103923cded8292c9234cae6a04f94ab677038e23aed7b113cf07b156f1cbe41080412bc2f49c3eb804f91bd81930e4b8a3fc08071f7210fceccffe0c3cede9a712e983c575cf3b418c1b94df11e120b5176ce09a6b6e80f77fe0cfe1be7b1e48d5a5baf68331fdaf69e6f41b17b78964f2cc673e1dfecdffe75fc37c3e4bc42b7c8c9f8d9b36c2e67df7834ffdd5a761f7ee55227285ac1740fa0da955db72c792ffb7df7e073cfd19a7c2539ef2146ae116f39e84b1a33ef491425b8f3df61838f288c3e1fc0b2e86071e7c90aa78cd525bc62e36c33f64f18355c678bff590c4a5a651e5b8286702edff600eed75cc26f639d184b5e766147995dbdba95ccef9417a734e19ef8230088ffd1faf1082790604392fed41ec7d243fd79c1a22b7d91ff63d91aa1221aac73757b06a85bf0b51cd8a719b65b35cde13aa4225e391f115c57f90900db91da2921bf3247536d23a19d3c15a9e9bc436e93ce3f3889fb5e44f54ff61b67bd76eb8f2aa6be0c4138e5fe6fc51a92567e9bd2775f9fbdfdffffef7bffffdeff877fc4f5d63d9e0f877fc6bfd53ef5b731cffe5e1f877fc976cd06b1cff8eff9a6cc7bfe3df9aebf877fc97d6583638fe1dff5affd4fbd61cc77f7938fe1dff251bf49a270afe67679e79e61b612fc77a93c64a441c3a794b8eca3596ac2941d26b7994f469bd25795312a964bfb5cefa5d4aee9a5d1c93b1c4aae92a8160ca0155ba67d930e570b7d696e6ad77dfa6ec49cd97dada9aad63fb54925dd23386019d477b3bc6f28fe738fe1dfffa9e6583e3bfb367f7ae3df0b5af7d13defdee3f85db6ebf63d0622c740bba85b1230071cbb6f49b640e082e91a753051fac06a4681f38852bcdc88a3df979ecdb00e2f3055515ca048feec1806025d76a9cca0a39aba23d5a6ecb1663ae92b3a0366b0d552c9aafac0c5b12922e26a0e42a5824333dc79889fdce957a5a6e2318e129c71c05ffea5fbf1e8e38f24851298ba26310b080f33522f9ad812f7df9abf0b18f7e0a766edf0d91fafd6105ab449e126d0cd11edcae9ff8f11f85577ddfab6065c31c6681daf3d1b61c7df4d170cd35d7c395575e95aa6905aa8ba4d32ed988dac21ce272de7df7de033b766e8797bef485b0df962dcb98b45d2b4500183dad03e76517d395f91c4e7cea89a952d7a5975e0e0f3ef06057ad2bf47ec8b388f779c1558ee41942fbd98a6bed0c93f582ca1b9dfb5d95a499681f09b9225cde34de67ce39ca9f8568bd19a03f0324d98b7d00753ee49c26f9b2ea15e75823b1493af07ba6c88a5c612be5a071ee45b22de9d2ef4f80dc4e741017f2218ad8e93d9715e920f615ec928d843f201cb5c2df46bdc7b89217db99cf8cd855c093f9160576a58c1c0bba46d2627aa64865d976ae9a4536b1ef3bb66f87ebafbf7189dbc3e1c4134f80b1e1efffb5cf6bbaf4792efdb1749664f9fbdffffee76fe9a7fffdbf769df5dbf16faf2dcd73fcd7e73afe1dff8e7fc7ffde8eb1fce3398e7fc7bfbe67d9e0f877fceb355a96e3dff1afd796e639feeb731dff8e7fc7bfe37f6fc758fef11cc77f19ffcd7a149412d112ccd732b1d849fe3f58f147cfb3f4f3e0357258724ab6591b553a84e43ca957ca9d72a8685badf9da36cb6e6993f5acb43f96fdda6f6d8bf6bf648bb65ffb5a5b5f8bd758be49fd525fedb796a96322f3c1b2cd92a9655b6bf53c3d2c5c94626aedb7b5ff6373c7f6c49253db8fe0f837fd76fc3bfef53c3d342ece3def7cf8c33f78672215682614ffcc04165ad30ac2c89a5c05181026805af4b19c3c27762412593d4aca694827de634245cb241892915acf897b51d88cb62e885411fa8024e2c78c2b65515bba46c8932dee927d44f09024335ec377c230e89d1f1c67bd0501495e114e3cf14438fcc8c3a94a510b5dedb0aeda9335d25e12676acfea022ebfec4a78e8e187a18548d5a26236a68b6f9f279b366e84a79d7a2aa49046a26b654e5284cdfb6c82fff3fffc2538edb45300c95e781f653661960406e0fdc10fb7248ca915e1e73fff05f8e0073f020f3db27db966c3720293b7b4232a37b92521c44c629b2fe3fe533ff95af80fffe1dfc261471cbc9cb4e8085de44722f335b39e34c3381639d088d6959c3b91f244927ce41e41ecc94914c2419ea60f57d90a6148b4a279191b34af15c4a0642bc99773587ece6f8925c68a5833c037e717f4c4a44024c5d46e11f5930c94bba0b679123fc0fef19e1026e5f9c2042790f3b99522c5b0096140dccad5c7d8669427fcc17d46a10be173106bd91f292310f133b7485cf3ae57efb2a4330c5a8de2ef2c8b728e5b880293f8f08cc03686725d00d1ee12e0aaabaf85b7bffd4fe0eaabafeef339f8fbbf64bff65bdba2fdafbd7f4bb1f0f7bffffd1fa3fffdaf6db5e66bdb2cbba54dd633c7bfe37fea5cc7bfe3dff1eff8d7f36abed5f62338fe4dbf1dff8e7f3d4f0fc7bfe35fdb66d92d6db29e39fe1dff53e73afe1dff8e7fc7bf9e57f3adb61fc1f16ffa3d96934d2df0d6222bd1b4b1a50d91322c593ab81660b4ec1238e43d0dacd27a7d00946cb6e25302b2b65fcbb4406fd96cadd7f7b50da5a4e1df96ae31dbb5dc521c4adfd2777d1894724adf93f66b791638b42eb946c6cb5a57b27fccc7296b6b765aa3746895f242da22bfb5cf63c3f1eff877fcffd3e01f4957ef7cc7fbe09a6bae8345bb48f71be913ad6fe3da3f6c921cbc0f1d9102d8677c262a12651214aec7b531e6aa395cad26cba36fac0894b4882a36dcf22f920d4c9c08540d27922d81e341d7b9351a40ae6c1504b92bdb42eb1bbabf2092090842cd82883e407257b915db6c96485d5c698baf930f4112d096be6135abe5f3673deb1970c0967d619eda0be2f3556800f780ab53b55d34b91a16139f96fab76ddf01175c78716a9f06a9825420224e9b9c4e45b09299686f0bfbefbf059ef294a397a2164b1dabcbf92d347db400894ecf7bf669f0da1ffb67309f753ab19a154a4c2ddac2bca361c56e7f23c51ef53ef8c03678ef7bfe142ebdecca653c50dc7c797fe93fda8184176c9dd82cf773d676d718df749f72b7dbe014cff94a0307ecbf2ffcfccffe14bcfe177e160e3dece0a51d7b92bd3867b1daa6b68490ace7fd5bcdb998739364a65c8a3193fcb89a55e84031a8c814296f025dc7bc7f6c68c815ad1827b8cf98ab0b49ba628cf1d945ba98ac955bef518ee177433923db58029f5374cd791b44deb21d207464e21755754a798d79399f67db5a261cc94a52e437e32d13a6c499c1386e04e690ac34a3fc67221957ca93bef05911c52dfcd78ce3816705123bc9de28f222e9672258ec5a9746929962379f1354e91c22b94d1055f2c85e7e8e6d39f15eca07e927c90f2216280798bc95f23fc265975e096f7dcbbbe19e7beef3f77ff4f7ff13f1fd6fe5975c93df8bc1fffed73e587bebf877fc3bfe1dfff2b9e3dff12f6538feebc3f1eff877fc3bfea58d8effa1ef8e7fc7bfe3dff1eff877fc4f5debf877fc377a53ad042e29d38a4a8788658c36c472d40aa8e5b09ca337a316b8921e0b3c63875f090896adf280e439631bae13cdb2d9028795c4a538e8d8597a2c9dfa90d7f69772c9f255cab480555a579a27f3bbe48bfeadf7c33a28adb9253faddc28e92fe55d698f25f6a6befc74ce3afe1dff8eff6f2ffef1f96db7de0efff93ffd269c73cef99dbd6d6733b7e0c3915aafe17c2296c8d8b7449660f211122456a9ca0e0e9691e4103103c930282155e321fb5baad683bf256923748ab24e6e1798c82444fa60db921dba42956831b860b216d9853a53fc88a4227546f26f26dac471fbc320e42f64f523928bf39854334ff6b57de59ce47f67c7befb6c82139ffa54d877f33e10173b977f18b5aa8a562b360b7d84143b24826038eeb9ef81d42e10095021914c663415c926e8071182e2227d0e39f82078ca539e821c924ea09143fb6cde04bff0f3ff029ef18cd300ab60251f1731c9efaa88cdd235d6fd42de57a458c1f2fe4d37de02ffcffff59fe1820b2e4cd5bd16c8ae8a838d84cc88938e266343be8e64da7e076c8133cff8b7f0a637fd169cf2b4a7510b3d20e21c127542f23d91c39a79de37de8fa8d490d794e7eda06564f29f88795c7d69c6e4254166ca24238947face6d0a0132394892107bff625f654be60ee34256780b614d152f20fcb32dd906b23b90dcfc9bbe399b925d7c9ffccb84cb6c665f318b7d97f6721cf91ecb4d7a985049f818c49d36972b4fe5b59d80aee965a0ccc567e20c4d3163d217ef95208181fc7b82fc6dc55e74d86c3ac222cb6cbb5c0891085de21c9083f701937ebe32cff270fcdddf7d197eeb377f17eebefb9e3567734996bffffdfdffed7eff5bb197dfa53d967f7b97d66add3a67f9a37558be94f4681f75bcad1cb762a13162cdb130256304e07fffeb67da5e30e2e0f8efd738fe1dffda476b38fe1dff8e7fc7bfb5c7255b1dff6b755b321dff8e7f6b8ee3dff1afd795e639fe1dff7a9ed6edf877fc3bfe1dff52c63f15fe1bcb61bda8140c0d147d4f83533bae7f5bc12b1d52d6fa524258769636b1b471b5c3cfb2a1646be980b240524bf0daa1636db4b6cb3a34c70e3a6b945e0c3530eaf5b5513ab8e4a1a1eda80d2bdea543ba768059392e75d46c2b1db6f299f6c5da47fdb2d2d7961dda37c7bfe3dff1ffedc7ff430f3d0c9ffdecdfc015575cd5557b91240bfcb06e5e4f04aaf48c2b4841470a6a457b410861d0b630d94e3a2581693e9f67fb7265abd8b53664720c0e6e4318c80e2678b5a2d20f9368b0edda828855d977804cb6627b817ce46a44b9bd2210d1836c6e692eda857ad9170a546f07ef0f5543028a735adf15a44a95afc48ec2e6cdfbc089271c0f9d0a5129c81821ab43f2cef262f9b9f596dbe0e1871f490fb155e062b127e9e8ab2b896a4ccb7f0e3ffc30d867d306e81e879edc0630d8efa38e3e127ef8475e0dfbecb361290b63ddb2c99924d484265722ea3d02b8f4922be07deff953d8f6c8368a134b351b3516066162f9bffdf7df0f5efd83df07ffeedffd121c77fc31894886c4b0c5825bc36155a2958e8c054dce43ce373d985c857b94aa6d5195b26499a816c515d002e56ba41ce70a682ddde3ca6e184794b9ba67cf8018a43dce6d36f143f25a9643159766dc2e310e0985403ad9fec1591de3603f335191ae1bbad6e7724b7a78ffe599c4784bb821bf734c5937d9d58a2a756c53ae64c5f345352d6ec5a8d7708b4110b670fc66b29a9768290962bdac66962b8db11fe9fc8a3946ec4b23cf35359ff778807b08d94e26929d73ee85f0c52f7e09b66ddb9ed7fbfbdfdfffbcfef1f8fed7cfb42fd63e4afdd6b56587f64de78ad4558a975e636140afd3fe58bfaddcd418a8ad2fe1c5f1eff897eb1dff8e7fc7bfe35fdbe7f877fc3bfea128cf1a8e7fc7bfe37fad0d8e7fc7bfe3dff15fba76fcc39a79539e3bfe1dffa5f54f44fc375ab10c9e6590bc2747c9412bd9ac84d74e9582a7832be59436c1b24dfa68f920d7ebc3b2944c530f01bd4e5e4bf9259ba7e8d3b22d1095ee95fcd47b62e9b7f64eafe7ebd2616dd96fad2bcdaf1d6cdaceda41600dfdbce453281c8c72ae8e8fbc5fd22b63af7dafc55acb2fedade3dff1eff8ffa7c53f92893ef399bf86f7bdef83b07bf79e4e86b45d10acb8ba54901844bd4c7e421df41be46f5c4376cd4425a03515ae966346a48b442421024b23fd4795f419ac0f5d2520e973aa8a45fa9930034c4811c4141c0b267bb5a2fa14c5827d631f00e58bb5725ee07da13871b5ac742f00ad238c5165a8430e39188e3bfe38c0d67a334146b34624258918d4a6a68270e1059764c20757c14a5b94e2dfd99b631717f08217bd80ec8d99acc39b10c9c245bb0a9b366e84d7ffc2bf8097bcec85d0cc96736748ce13b688be81f9dce87616f6ec59858f7dfc93f0d6b7be0b1e7ae0a1ae1a57e4c533800904ac549528e9e948781b376e807ff5fa7f0ebfffe6df81e7bfe0d9d0b5465cedcc4f3ee2feaf241fd09e96db461a984af9215a4f82d85ffe70f526ce3526e7e5e7f84d5868c4fca0af4987246ba5df0ab3335e27b1da19db578912b93de3569a915b70c66c1fd07d6edf899f448452676c54f8ccb160ffc4791f09932b2247d9a610edbf29dad857a8633ce7f323f6ad2007d5bd80885c5c7d4fdc67526550fb25f78fede439b255a21e6ccb9af6a7f21d20beb35e5a839f1457aa8af6e0830fc1bbdef95ef8e8473fb5c4c01e7fff077fff3f9edfff7294f6d43a2fb4efb5586bf9a5bdd5cf6bf1add9adf343cad71f2dd3ca210b975a9ebe5fca53c7bfe3dff1eff877fc3bfe1dffc3398e7fc7bfe3dff1eff877fc3bfed70ec7bfe3dff1eff8d7bab46d8e7fc7ffd8c0b98d255807c54a64eb5b3a3b25e14b89a337b8b4215622588797157cf9dbba2fefd5e2a11340db58b2d34a3e0b8cebd9542dd38ab7155bed876597ce911a206b879815abd25e59bfb50e0b8ca5c3b536c6e65a075aed60b3f65cded7712ccd2fc9ac1dd2fa5ec93e698be3dff1eff82f8f6f15fe77efde0d677ff36cf8d30ffc25dc7bdf0389b480d5a8b8920f1392a49d591713a96257a90a2bd900c70444f5289acfa413263a65b213fbce7bc2d56da4dd02dfd98fd89332e45ef0673e9ff56428b9c72c5354cc61fd524e22bc202143b532647d8db23b3d177162bbb9aa51670b06a68b0f9351e62b2b70ec71c7c2be5bf6ede62c56a13a62f789a47ab11ae1aaabae813d7bb012d26c697720c257db119890f4149bbc2fb3655c9ef3ec672d6d23a21c56e30a1443e07c5ec6af99a76a5ac71f7f3cfcabd7ff5cb22fb658c968011dedabfb27a547e8621c893005e4ebb66d3be0fdef7d3f7ce4a31f85d55524ffa05d4155c42a8f195516eaaa3875df2b2b73f8ae97bd18feeb1bff137cfff7bf72f91b95ad02479f639fdb09cabdc25c24d9b952194026e7a51831c990f31f7a87223b26727141d79ccf896c349fe75ce1fc63ac3422cf988cd888ea5b4de8db5db6d25eae2c453630b98aab6c35d48e31e3843e4c6c0a9c3ad21e189e092c9bd30cc4f9c6e4a448baa38875a0b97da862f69fabca45f241c685bff339cae70b0b5afec6d69dd90e3197f7852b8031c96d955b3e06d1f694e7d39c449822bb1b922fc7e03d41fb96cf57fa44b18f0d9d599d7f2ddc75d73df0e1bffc389c7df639e6392d7f5bf701fcfdefefffa1ddfafab178ff8fcd2fc9b4ecb5e22ed76a59837327f8dfff8e7fc77f6938fec1b4dbf1eff877fc3bfe1dff8e7f29d3f1eff8d77638fe61cdba925c39c7f1bf7638fe1dff8effba4cc7bfe3dff1fff8c6bf59ea412f2a09d14ed4ee69676aa0d34e58734a0749499eb6c10aacbc2e257d2989f4da92bdb564b2e26ed9adef699db579fab9f64d5f5b766adb2c3fad518a7d0d4cb5e7b1f282a9e58be5a705ccd28164e9b2e6685d7aaebcaf7f97ecb130518b9d1ea53daead75fcdb36695d8e7fc7ff7af17fd14597c25bdef21eb8f5d6db33118516aeb519efe17a804c764833a84215bfd47955e814e6b94cc468b16d20ca439be81edab5a0b66bfcac8d71b036c2f00f24be460245142dd1f81b5be3451071152d0f39bead8ab3f46ba087c85a29cf451c53751f252392ef493e13d38009416417b522441254136670c2f1c7c3a68d1b90ce34acac551849246ec95216b61ebcecb2cb13b1ab5bdcb5e50bcd6cb07fc99fa685fd36ef0b5bf6db1f366015232236c9918936f47bbe6106affea11f809ffaa9d7c15ffec54761d7ee45d78990b63022310a989cb3d41b67a9fd2192b5e64b1beeb8fd2ef8c33f7c1b9c7aead3e0bb5ff1725899cf3a225747bf87b121fd48710fd8b271055ef5caef82230e3f0c8e3dee68f8e4273e0bf7dff7d032de48426a12672e5734a27d92adf11ab17f9d0e41b2a300af529e627e25c211556e4a7b1ec2b005274096b558ae4b3909c38a70917433a98af530aeba7c083d31509e092abfd290842c9297f143d7dc3a2fb714251981f1276472bb4abea7cf8a56902565b52b2617f2b39968b9c8f10611fbc03e53cc1a71cef219c415e31226b9bd2991cd8441694fb20f242313ef04212fe1b9edb135a36a6691da3ef2baec2fda82f3d3fe862ea462ef39c6310c1cede42d3a5fb7de721bbcfd6def85430f3d7c99fb27fbfb5fd8e2efffc7c7fbdfda879a3df27e690f2c3d50f17d6c6d6d6e49bf1587d2fee818489ddad792fc3179da06c7bfe3dff1eff8d7b21cff8e7ffddbf1eff8b76cd2ba1cff8e7fc7bf7dcff1eff877fc3bfe1dff8e7fc7bfe3dff1df5dcfce38e38c374a615690e4772958965196c196113238f29e35dfd26dcdb5be4b764bbf4b07596953c7867540e8386b7be4ba12b8c6ec59cfded512c7923945aeb5a636c7dac729baa50f565c4b3e6bbd5301579239754cc18fa57fccce928d355c96ec70fc3bfe6b32a7c8b5d6d4e67c27e2fffaeb6f8037bde96d70c1859724a2108e4c40d27ad9d610062426e07991484b23f92cfd0e24971eaeb591e4f23d6e5316a82a12db124876227370252069a3921f694d605b582f18e716fa218827c90692d5c8fd9736522c1a15f78e4486be23292890c2082b1b57e0477ef807e165af7819cc420bb386adebbfa2a4b484ce89ce8f19dc79d77df03fffe71fc0f6edbb00c94748870a218a1836425684a79dfa5478cd8ffe301c7cd07eb0df967dc75b1e420b1b376295aee3e0965b6e836baeb996647684320e68137a5fb9ed61da97b0806d0f6f836bafbb164e3ae94438e698a774f16b62efe0e4d176fab002d7d2c7430e3e005ef8c2e7ffffd9fb0ff0cb8eeb3e103c75dffbe7d03907a011ba1b4037ba1b99000966c9a624cb33f26835b23db6763496e4a05908bb3bb39ed9fd38f27cdf8cc7df781c86b22dca966c8d240649cca42851122931830498001039a3d139fc7378aff6d6a93a75cfad77eaded7144582e4b9cd877743d549757e755fab7f3a072626a7e0cc9957e0dcb9f36890013b40648bb902f9bfd4d0f8beadff7f535826a3860d96cbb9fb90e8344c1f127a78ab42578dc99d87314426e29f60f8c073d2c30fd21bf39dcda5e7e90a74983caa845734edb1c0b0c06c20f21bdf3b2060b8203224c34c8d0095b18d5a40d27e437a0b46ae8cf1b1d59ad27ec4db42baef3e238f45921aed1fb4feb65a5b47c6f2b2229ce23e98b63774b74f9f3a03972fcfc1e11b0ec2860db3755f8cbefff5fdffdd7bfff37b4d6b338c9d391b7371d3dfff8a7f6ed3307215ff7999c31e8a7fc5bf94a7dc1e3e4ff1dfac9bfba0f81fd4a3f857fc37cd51fc2bfe53398a7fc57f93cc61e44a739ac628fe15ff46f1aff8cfcccbe9e63e28fe07f528fe5fddf82ff80009886d069020c9c9d4813479d2c0f280f139dc68c9216e1f5de7bed3e490c6a5fea40b92da924b50c9bff45cf24b1a93da91dbbcd331399dfc5e0a3c2e8f9fa7637372241f521b9bfc6ecaa9dc3a739f87b14bca1d7ede24a3cdb6d4df5c3cd223f7e248e7e43606d3b01949f353fb14ff8aff541e3f57fc7ffbf0efaa26fdee7b3f080f3df835acd8e3c61581dc20c6a912508f7f788e040a478c0a0485d89e90a641bd85197e823c4770a29660fcb97b0646d81fd8731b6453abba5ea868c3e346b120d2565c3bd225c4879e930d717e1813db28922fa1fd1c565d32265601229f3961ccb50c044617191b1b836ddb77e0bda2308d7b341986a68471172f5c2c3f7360907cd561c6cbc7be7d7b61627cccc7a580f6c3f6b12de2f5d75d037ff3a77f1276ee74b6f69198e597c1af425caaf2bbd7f78414b71e456953bf67e1cb0f3c04eff89577c24b2fbdc4c86b367e0a03431ebecd5ba9050931dbb76f829ff9bb3f05f7ffd23f841b6e3a0823233e0fdc41ede90c40cccb82ef2dfc63abb67e98e744f6e1a1c0981531373a09e9c6c975b94832294f625bc180335b852acef322ab9ce6a4a77ef25e284205279acb5b255af68124d7fb0c53781de615c16ef7ed2a7fd133cafbbe9093715f0aed13532cd1fe80f687f67c2161bccd1473224141b2ef437d4f73d820a2686dff265c4385df5ef02bfa4c73b8fdcc075fb98a644345d4731fb6cf4509bca217913e43cec4b574fb5a19cb4f7dead3f0e10ffd01b67bd5f7bfbeffb90e2e939fff65bfffa5238d556e8e949be9395d4b76a6799ceac9e5b67434ad556a67ba76123ed27549f32e87af5c5ea4df8a7fc53fd7c165f273c5bfe25ff1aff8976428fe077528fe15ff8a7fc57f939d8a7fc5bfe25ff1aff857fc2bfe070fc5ff0f1efe3bbff44bbff47669117389921ad094504d32a5e0a50923015e5ae8544f0ed87c4c3a4e0aac3457daf824e0b4d92ad903309884a91e29f6d26621d9d26443dbda48ba52bb729b5dd39193dff69d9eb78dc9bd68a4f9a95ffc5a1a276d14d29cf4593a26b525b7090c6b7bdbd116afdc1c290787d1a3f86fb60740f12fd92e7da7e76d635e4df877048b0f7de863f01bbff1db303fbf509141245bdd0d6abf17c61181c490dcd0ca8bae8968610229016d08f30772558821115f8c37a65691865abb21098b6c257b8c279140d041adde686ef43390702cf3498c39dd73d3c318aade03748f720f20b649e4f381aa00c5f1cee72e7483998e26b26dfb36f82f7ef23f836b0fec878e014644325055c062f7c21dafa70b9ffddc97e0bdeff97d3c479b7d23b710db22ce7197ce9cdbefb805eebefb0e989e9a84a9c9c958010bdb35827478ed45a780ddbb77c3c2c2227cfdeb8fc0caea1aeaf2b9d247f9166849c976676745bc7bfef967e1ecb98b70c30d8761f3a64d681eda6b4d9cdf7c5824e2f81e8ec67f95b11c1f9b80ebaebb1e8ede7c147abd756ca9b9bcb20cd4780ff30ed3d0555932b135a5093980791b7204ab50b97bd6937ec054f343d2450c606a437dffa1769854d129e2d0d4497ceec07c0df722410906f716cadfdcfee78397ecd9e1bb3015f930629a3d33a66ac988f382dd1d566d4aca0b691f22df6ad741a6810a3348ce0b2d05314b1d898bda9fd21c48f65453d4f6101a03d40a32c16187114add81c4ba80478ac740253daf2cae5f9fdaa2725f21f91dc5d6d8720f8c7fbeb6b6064f3df50c5c7df57ed8b3673774bb9d8198e9fb5fdfffdfa9f77f6aabfefe9731a3bfff07f528fe15ffc3dade76b4c52b3747cac161f428fe9bed0150fc4bb64bdfe979db18c57f7e4e2e5eb939520e0ea347f1df6c0f80e25fb25dfa4ecfdbc628fef37372f1cacd917270183d8aff667b0014ff92edd2777ade3646f19f9f938b576e8e9483c3e851fc37db03a0f8976c97bed3f3b6318afffc9c5cbc7273a41c1c468fe27ff04002569a383910f3ef743390140d6b4c0ed4921c696ed3f32b19978228079a9c5ce97edbc6d276484995032cf72137b72901db6cc8e9921233373ed5cbaf73404d7d4a37b5a6b1922db9f5cdcdc9e992c6e4ce9b5e60c3ac491a3b2937a5cd7e183b15ff3060672ec64d72a5fb8a7fc57f6ad357bef275f837bff26bf0c20b2f556dc9dc3755b249e647d29165ff1f0deeb93b0fb22381c356242960f1e6e48a22a98e15492950558c02b6aeb1f52055fc09636bf1b69e246259651a60be01d76102d986113748273ffac93a76a8459a3115d92a90b9aa8a4e4ea68be3baaf66e508438112857a9d9c6e077a3d67431face9c135d75c057ffb6ffd146cdbb625d4afb2f1bf956dc117471eb2e0a9528e79544affd81ffc31fcf1273e05be256081adfd4ca84ee5efb9b12e8e7d181de9c06beebe03eebce3048e999e9982916e371063faa415ea87413f3b9d02265c2bc2bdbbe1c187be0a2fbdf87c39c5ebb49837943b36f05cfc1af6acab95e52a409579d1b7f0cc53cfc0fce2021c39721436cc6e2cef11f9c90eb6dc0372bf8cb1f5cf2af25411093caeede24819d77dfbf7c03da57f33b31be0f4e9d370fec20558ebf79043e86577c111e09c64ebd60aa350c47476923a9d6e690f04829e89849b3eaea1f7d7af87a1b4aa57594a6c27b2561194f02ca3ea4f7c2e33261e8e2487d5d5a84561c87b1be4113e0b22431a536b89c771c0dbf671e215adfc0076d83d1a13df53bcf59fb591bc654c5535ab1f300c4c3edd8b394e36d07bda5604366a2b5ab32f8ce355fb7aa1d218ed197c1cf789089e96f481af98e5c87054112c5631e3ef8e708ea4ac708d55b32886ce1e8a8ffb6321901a0dcc2f2cc0b3cf3c0f37dc780876efde05d2c1df67d2fba1e97d92bbafef7f7dff37d994f32127af2937f5f77f730eb78d4b6321c5b849ae745ff1aff8378a7fc5bf51fc4bf6f06bc93fc57f5d9e51fc0f8c6d7a7e25e3d25848316e922bdd57fc2bfe8de25ff16f14ff923dfc5af24ff15f976714ff03639b9e5fc9b83416528c9be44af715ff8a7fa3f857fc67e2d8b9efbefbde9e02491a9c022db758b905c925886454dbbc5cc072635239a97c495f5b320ebb18e99c34ceb998e5c035ac1d92ce3496e9f9b0eb96cec96dbec3ac057fdeb67136d9358c1ffc3bb70e4d476e8dd23837f993b38ddf6f8aa764bb34ae296fa57557fc0fca94fc9374f0b94d7927c5391733096b3999921d924e69ed539f8759b7748ee27f78fcbff4e249f8e5ffe99fc2c30f3f56d9908cef0459eed341e24d2792b35ce5a958418a6c20df2bc3ea042a46aa21c284bb1fdb9a05fb88a4d1e7edbd8c89edcd6aadd06cbd555b8dd44231e5d781c0d12d7de9b16a5de4732d87c81eb235105afab6aa7c04c19ea89b620dbeb2cf4877b45699cb1454e9c862059cd5f53518192960e3e60d70d71db7c34ffedf7e02c63a2eaece76a29a546bd7477294411918474720eabb1816f0dbbff35e78f0c1af0055c0c2a24d3e323807651877d58789a931b8f7debbe1da6bf6c3cccc14b8567ea3a3a3a1d253552dab76581374fba79b366d84fd7bf7c197bef4109c3d730e7db6b1e295fbb6cce6985d9e8856ea7015d8bef9cd47e1f9175e8213b71e47b2543011895b411b18668b7f667c085dd52b57312b7e8a38d6fd77626a028e1d3b06af7bdd3d303e3e0aa74f9d82a5a525e8adbb7684c693afc28c5ebf57dae5fd460e131431c73a54bdccfa58164834eb47978a724d4dbc6763f535222cc61682948b492ef3969c7cad696cccc88043ba8e84483e3ecc77b9d70915bc5cfb3b774e581b78a7d84042642d044d54693d01cddaa887eb8a58a52a56c137c3b04f24a8941499be0f6cd041fb43c15a2b465b82ad40f61813e340ba21a94e37a003a0e623b53fa475a6fda4c308a2fd50fd2fb618a4fd9613b4682fa4b5c66b8bf982b1ea1428f9d2a57978f6d967e19ebbef82a9e9a99a8fc0d650dfff83f1c8d994dad6140fc99fb6f7653a3767d7307ef06ffdfd0f623eeaef7fc5bf148f9c4db9b84af649feb4e1259d9bb36b183ff8b7e25ff12fe9348a7f311e399b727195ec93fc69c34b3a3767d7307ef06fc5bfe25fd26914ff623c7236e5e22ad927f9d38697746eceae61fce0df8a7fc5bfa4d328fec578e46ccac555b24ff2a70d2fe9dc9c5dc3f8c1bf15ff8a7f49a751fc8bf1c8d9948bab649fe44f1b5ed2b939bb86f1837f2bfe15ff924ef31dc67fe7fefbef7f7bced8d46829d92443d3c490cea5c0b5256cd3189299dad9149c5ca0d2fbe922e436246888636a9fa45bb233b741e462ddf63c97c44dbe35d92af99c8b736eeeb0cfa478376d224d1b3a97ddf42269caf93679d2752a978f9174f2ebf4459adecbad6dd37ab4e5ade25ff1df64abe47353be49e7c33e93e2fdbd847fd73aeedffdea6fc01ffde19fc6f65f3daae6521ed446b0bc81a40147f0c1fbd693500c247b45f84e2bf3185ef1a7e1f0edd7bcfcd8f62da9a8e36c21b935424bb0c3112f9064124852380da0464a21a294f3d53d5b5fefe17527217a00cb594e58a1568644ce70cf23c10306f18f729c7de0ab5c755d45a5f28f237ed9c211dc0ad8b77f17dcfd9a3be1bffce99f841ffbb1b7c1fefd7bc1d85e30a1f220dae6b827a6aaa6e5e43879fd5e1f7ee777de0bdffce693e1898b0db5582b42080d7802964542d2ddaf7d0ddc78e321585b5d85e9e92998989c0c31765378f4d8114855941f5bb66c834e7704feec539f41429509ede17c852e8a16f9e23fa6f0a41924c1956bf0ccb3cfc2dada3adc74e361989e99f56b6f38690bea2d098d156ecaa6baae8a5bb76e82e3c76e86fd575d058e2475facc59585c5c8c837a7d17a72e54e96c30d045608119cf9b896d15fd5a54be2101aeef5b2fba5b45b713e3136d09f9ebe5db1ab92ae2aaa0ca5e30d036d0120642ce23093160b6967b50e53acae23638f994b7019f8e64e9f2ba1f70d3494892710f31ac3523238cf5495f681d187d0b7b817b96122981bf0bcce0ef0b37becfda9dd6f6bc300782fdb1d21511430973a1929561bea3df619f2802218df45aa8efebb46fb99ca675734fdc3c9fe7d55ec96d2f4cfa9ba9f0f96e7be17981ad31cf9c3d8784c763c78ec0c848b75a23a3efff9c6f4db64a3e4bf39ae60efb4c8af7f7d2fb5f92cbc7483af9b5fefed7dfff92cf925f4d6b21e548936f4db64a3e37e59b743eec3329de8a7fc5bf842fc9b7b6754be5a6b1906c90e619c53f48476eeeb0cfa4782bfe15ff12be24dfdad62d959bc642b2419a6714ff201db9b9c33e93e2adf857fc4bf8927c6b5bb7546e1a0bc906699e51fc8374e4e60efb4c8ab7e25ff12fe14bf2ad6ddd52b9692c241ba47946f10fd2919b3bec3329de8a7fc5bf842fc9b7b6754be5a6b1906c90e6992bc07f574a3ac9003a72e7b904197621245d5c4f5380525b869d933b243ba4a0e70099db104c66b31b2679db36935c724a7649364a63b92c1adb04809c0f92bc746ed379db4ba6297f9af24502576ab364b7b4c1a75868b293cb68ca093e36677b2a2ff75cca8f36eca7f37276f1718a7fc5bfe27f50a72336bcfbddef830f7fe8e391748172481e546dcc90d464abbcc22a378cb8502484111b646135996e07c723912210311c11c1f6fa15e924b4f96206fafbcc07243404f204559e5aa7ca556c1e55b189f91dda90a1cf24d378d24ab7db4512d6e848d7cb0d32781b388a57d7c97272c057cda2a3cf486ae80ff83a53ee1aeb28d5085abe4ad25a7f0ddbef6dddb609dbe3dd7befebe09e7bee823dfbf6c0f8c404b80239a6bf0a064f7abea254ba0db9ea4fc0d6d97a22d7dce2025c9e9bf344210b8c78168c63874502d6386cdbb61546ba23d892d091d15c6c4646477144bf6f6130d5edc0d9f4f404fccccffc1d78f4d1c7e1777eeb3db0b0b08e913040b909d80ad1119a7c45a972cdfb5d249d38228a2344d99515f80ffffe3fc0a54b97e09ffcf2ff0fed1ad44b9fda0a41d3e1886a16caf52ee3b96ddb46f81b3ff123f08637dc031f2c73ff5def7a2f3cf8e5afc1eada3a74583655c4b30e56da5a2ff3b5d7b39ed41649817d245d510bc4001ef0641b880446fc309c23be425e10e90fc946b6c21c087b412f547c2a5885267cd6e9c4f11cf7eb5879ad1bf1ec73dbc7dee5be6555ecd669ef60a42ada13907c149eb97b9d80b17ed2da90b0d90f7e51ab502476b296a3118b014f40ba480655990a982fd0e26aae5f5313e79980679aebbe79db53dc27480791d50229d432bbdcbc2ecda3bd2d641b611e7db1befa56adda56b03f92e1c207c2b7cb7147baea149e64e588988ed4eaf69ff7bee7fd70ddb507e0477ef487b3ef157dffebfbffdbf9fecfc968ca093e36677b2a2ff75cca0f6b077f6ba787a4473a97f23cc5462eaf9bd64f1a93da97da32ec9cdca1f857fcd318c5bfe29fcb57fc2bfe15ff83e78aff41dd8affbcbc746ed3b9e25ff1aff857fca787e25ff1aff81fb459f12fcb976c54fc371f921ee95cf13fa87b18fc77a58169120c03acdcb56458d3759b8c9ca3743425bb041c49a6f42c971cd29cdce62ad99e1e4df6e5c637c9cbcdbf92e46cdaa0d24d2cb7a6e9b826396d2fa154cf30f922812167d73039d0940b4d392ed9d27448f1e5cfd2732976c3ce51fc2bfe15ffdf7efc3b92cd638f3d01bff55bef818b172fd6732354b0e11577a85a14122abce281b668fd40a2c26a378cc0e48856ee280269c5c9b3bdaa621438dd4117559642bfa8cd5f2048b8ff16545927c48b4703e5b9efc45792094117af8063c29c6887fb38ff993cf2958856482e71e495321eee19122e589b344e4081104b2fc311bc2c8c8d8ec086998d70c79db7c35ffbf11f8337bcf15ed8b26916461c51cd389da17257df93afc80f5f8d0990ec133a0f625088ba66fcc2c0fcfc22cc3902563d5300dbe2a5778d8f8023e3acaeaec2e4e4a8af0a142a53f9b0b878c768781b4c4d483c1d1fefc22ffee2cfc3e5cb97e07dbfffd132cf2c442258c8156c96680249a5f4d155ec42d3dcbaf40c2cf556e003efff10ecd8b11dfecb9ffa09b8f186eb81884d602a593e12de7757b50a6ab9e0f5f2a38384aa1eae430789589be16ffdcdff026eb9e566f89d77fd2efce99ffc193cf7ec8bb0bcb4828135a66a37d87776f62d12b89cf3ae3ba3e3315145388aa3f154ac98cb36ac3d120601628539cc778048bef23e789f22a6434e5a9f0011874462243256c403ad05db07481f61cd8d71bef7d9bc4ec058ad0562d04998a6fd23b6f88b5955ad2bc930c11727bf17ec37541d2a99ebd578ccd910230b15a1933f87e06f2fb10131ee626a6dad55a10df12f2856a64ec2244c539c281e91c0453604ff2c9b43fb0091d580f672a8fe42e448a698862efe18177fdd0f382ce85e799c3b7f1efed36fbe0bae3f781d5c7ffd35d18fe83fe8fb5fbaa7ef7ffdfd2ff9d2749d3e6b8a4d5b6c73f2f4f7bfe2bfed50fcb7cf51fc2bfe15ff8a7fc5bfe25fbaa7f857fc2bfea1758ee25ff19fbb966c693a14ffd02a4ff1aff86f3b14ffed7314ffdf79fc77eebbefbeb7a746a5090a2d86a58b4a9f61819993253d978e61829eb33d27679845e3e3a539520c2559b97930640c2480e67c6e9b97ea69da249a7c1e1614d2f3618091cbb95cac726b91decbf9c9e7a4eb9eda91d329c99440db9487a98eb6b8a4b635e51e1f978b8d949b8a7fc5bfe23f8fff575e3905ef78c7afc1430f7eb526b7e07258951d771d09096467f814528c888c15aacca4f173763ba24f9f132302b1a213e6904c6a5fe6e6707f3136c6d45abce178ee2bbbdf67156bd2981021856c91b28bda2a5aa8082638df91af4235b08293bbb80e47682affcc4c4fc1dbdef6c3f00bffe0e7e0bff9b99f855b6fbb05a6a72761a470e37b9e7c85141e8ba617de39f075789855367cf0cbc66fa7f7cc9973f0eef7fc3e9c3a7d162034282c8c97d94717438450be85cd9b36c29bdefc06b8e6c07ee8f77c4bb5b1b151246599a03fce61c7c09e10f46f2ae55d73e06af8f2830f95b69c0d869a58c50be3e4085d6e79a000a2ea99401233d693861efec637e199e79e839b8fdd0c9b366e2ce774822d86c58230e85b04d6cba1d58c43fbf0c4b544347eaeab7cb673c776b8f3cedbe1b6722d464bbf2fcf5d848b972ec27a6f2de6bdaf4cd50d1d0f6d882b44dd3ea720e49bf57e840a5094a37e16d4887d05cb3dfc505e855c323cd7d837301c72e25face66418110aa0c214db1b793b40248885b1369c136638de8a049b36e8c27c67f7e91b6d60642af28bec336c6eba4fa47b1cc9e905c223d9032c46149f22c4876225edb184df58bd8fe21ef61bb2b11f885f682f11a398fdfda4b2578c5b8888e57a0dc498604662cbca1e56c33b7ffe02acadaec1c143d7c2ececec402cf5fd3fb886e9a1ef7ffdfd9f8b01b799ebc9d92dc9929e4b47537e49eb9c93a7f81fd4a9f857fc4b7a52b9e935b799ebc9d92dc9929e4b87e25ff12f6123b75639fd8a7fc53f7d2bfe15ffe95cc5bfe23fb7be4d58688ab564abe25ff1cfefa5f16dc25193ff8a7fc57fdbbc548fe25ff12fe949e5a6d7dc66ae2767b7244b7a2e1ddfcbf8effcd22ffdd2db73039b122717885c4248c169db4c727aa5646c0b263fcfcdc96d0a6df6d23869c1b83d922da9aedc9cdc1af1b1c3802b9525f9c6e74bf2a523b7664d316eb33d9593da90b389ebcdbd58f87c496e9a5bb9cda4e9c523f923e597749eda2ac9cbe58d7434e5b1e27f50b7e25ff12fc529f5a10dffabe53ff2ff5fbff91ef8c0073e8a558f50961f18090603f108f7241293a48f132488148276b867e5b9ab20554e80defa7aad7d613c780e03d45a24729253c1882551479065832e9291fac5ed4e7fa8a43127fb886cd60b5572dc4136c4b9343f104db09a5261e1f00d87e01ffcfdbf073ff7f3ff0d9c38710c36cccce0b3c2ae95cffb9e626521d0adc8b640d4c0ffcafb9c65917395ab5e3a790adefdaef7c2f97317cb191da0368010aa330151d48c453dd33353f0d61f7e136c7655b8465d3bc03e7447ba30363686fa253c5a2196b81e9d729cedc38e1ddb60e7ae3df0e8a38f0412560181ad13e6058f6c55158d7a341a248c15b0bcb202cf3dfd1c3cf9f413b067ef5ed8bd7b0f74bb1dbfd6c6cb0a8e6463036c047df7e3fa7a4f0c5625ebc0be7dbbe0961327e0f8b19b616a6a12ce9f3f070b0b0bd876b0c01816613dfac2de6292539febb4329d90b3786e02e98ac50ea5daaa1d61cc4780d82a0ff83d36b7d25b9121697d623b439ac3f21d494ba6aa7ee5eef1d69bb4a6d133da1f5cee3b4cd35ec1c6160cf7d4aacf80fc17498edb985fd6b6eebbe427f9575598abe2003c0e89dedada316cbba3e87606fca6fd2c9249b9fd30b87f43f08313cd70ef60ed5c4dc8735fd1cdfa96ac7d0bcf3fff12e6f8891337fb1691fafe1f982fc9970e2936fafe1ff447ca2fe93cb5559297cb1be968cae35c6e35e96fd221c9ccfd16480f293f9ae6376147b2497fff2bfed3f98a7fc57faa5bf1aff897e294faa0f81fd421c954fc2bfe25bb9b6c4fe528fe15ffe9fc26ec483629fe15ffe97cc5bfe23fd5adf857fc4b714a7d50fc0fea90642afe65fc77eebffffeb7a746f3f35c8273456d8b2b256c2e28921db9e0e664a57273897d25804e0f49b6e48714f8f4592a57b21f001a63d4749eb32167af34bfe91eb7577a26c997ce73b14b65e464a7bea572a4f51e3607d239a4c36440dd1433d3b029d1796e3ccf0909e44d1b8074dd963b8a7fc57fdb3d6eaff44c922f9de76297cac8c94e7d4be548eb3d6c0ea47348479abfe9f7238f3c06eff83fdf89c49868871f8004106a6d16e78567f8213b6c55b547b4074b3779b9d85d2ecc77d76e46ac2a43840e18cc016a85e89e59d6760cdb98d98ac052b38fc78c6485e7e9de0641566c7dc848326877d0419f34ce4e2611419c2f44b870f73a14d7c255ec5981c3870ec12ffeb7bf003ff137fe33d8be6533741d51097a58ffc9f6d7a0d32d636421d07cdc4155839c6cb60ec251236099029e7fe124fceeefbe1f2e5e746d08bb7e1d4cf0c21ae68be7d0cc6e9881bb5e731b5cb57f37388291237eb8b88f8d8f79120840ad251ab0780cac3bae5d1f4ce9dfde7d7b6174640c3ef399cfc1caca6ae9a7418218f85a5938a7531415790a6da22a47fd32261dfc7ef6f9e7e0e5974fc2b5d75e03bb76ee2c63da09fe50544c0bfd2ad85cb8ea650523ea805f83c2e7a3eb2e38313506fbf6ed865b8e1f83abae3e50fa3f02a74e9f86b9f925f055b60609583e178225cefe82dd87aa0a9509f70cab2245188aadf3a8b55ec87f20f222ab446578ec1946e37dda2f422ea608256cf8ca5eacda15e12e8c43d218cbf9d8a62f120b21decfc65f781f12b6291e5c0eed2fd2bb1be3e4aa53b1f1b5ea53143f8a51d04f7e7548be90b79180e69e5b5b6bd908c9fe8dd5eefafd6abfa0b562fb16c5d3a6f685fca6f94542707484d853a7cf2001cbb5e0ccedfdfafe6fbec7ed959e49f2a5f35cec521939d9a96fa91ce95dff97fdfe97ee4963f4f77fde8e9ced3959a9dca6bcd6dfff8a7fc53f64cf15ff8affb67bdc5ee999245f3acfc52e9591939dfa96ca91d67bd81c48e7908e347f878999e25ff16f14ff8db1c8c5a74976ea5b2a475aef6173209d433ad2fc1d26668a7fc5bf51fc37c622179f26d9a96fa91c69bd87cd81740ee948f377989829fe15ff46f1df188b5c7c9a64a7bea572a4f51e3607d239a423cddf6162a6f857fcd37d6c4128392d296972425ab0a6e04a864909dd1688dc1829c124bbdbe6a5f353b04932a467920d242f276b182049e0cfc591cb4ccf73316ff2ad292f529d6da01926f99b622dcd1b26978601a9f4dda437372eb776c3f82fe12db55b5adff43997db164fc5bfe2bfc9b7a6bc48754ab6a5b2bedff0ffd8634fc2fff17ffc0a7ce3eb8f54ba903000be8e102329d46c2d3f7dd7facbc961041249977be6aab94839691891c7f887f8bc482a6f197edffa7685eeda911a9c1d05ab1454b04a3c106c2d4c458eb214937ebf5e5586f9e8643a1d514e380a662baf6244a4138a43f42f3ca35672ae6bde2d278ec1fff24fff67b8f7de7b61666a0adbef390216d852a729900064985603f55c20b7724127cd8e94e4e63dfbdccbf0fef77f182e5d9a2baf3b616dad976c8a7abe147dd8b87116defc9637c2d6ad5b617d6d05ba2323b0b2b202b31b66f19cdb53d36b129b81f2c9df776dd5aebbee7a5cf3a79e7a1ae62e2fa02dd64f0e71ec84897dfcf664381ae385f67b6bf0fcf3cfc3230f3f52dab401f6eed903a3a363c04d325520bc9b3088790bfe83ffc31c70df26c4cecf73d41b67c3d4f4141c3a7c185ef7bad7c2e1c3d7c3d868172e5dbc084bcb4ba53dfd1879431339b12bd84d39820420cc055f5d09af2160811c351e7b889b709f2a54111e893045794bd5aa0abe2f5b1b5b0eba7bbd50ddaa48aa68453cb37d867297f28e7467f799640fed10f648065438ee852a554c404d76b546953fb59cb3b6164f225ff5435b5060f88bf2c377dc93a4bd3af89deab66c8c09fae35e13621ceda7f504a8b57594f651aaa46588b1c7ec71ad08ddb84b172f618bd85b6f3d01b3b3338dfb3dc55eda6bd3b1b9f7593a4edffffafb5f7fffc350b9d8949f927c694e6a77dbbcdc9a35c9909e493690bc9c2c49469b3dfc9ee25ff19f3ee77215ff8a7fc5bfe23f67afe25ff1dfe45b535ea43a25db52598a7fc5ff306bd67434ad976477dbbcdc9a35c9909e493690bc9c2c49469b3dfc9ee25ff19f3ee77215ff8a7fc5bfe23f67afe2ff2f1fffd882307d206d0a3947da82253d4b03dd148c74e153bbf8b8f479535248f6a6be4ac197f4e5f44a76b5d92ee9cdc96f9b935ba32b899504f09c6d6d4997c64f025daa4f9a235da7ba724793fdb931693ee46c4eed4e7da4833f6bd3cde549d7c3ac7b2edf9a30dda627975be973c5bfe29feb49e57ebfe27f7171117ee3d77f0bfef0e37f02eb6beb15c9c0da48307095595c7b404ecea0e8192fc8df0b15791c6989aae8c43573dfa1f520d9446412aa3645848822892b2762993096aed75dbbc2604fc1f440829122d8196345cf59eb42603ef19686bc051cb703ef510c6a0be108217de8ba12567ea2af9083e32ddc7aeb71f8c7fff87eb8fbb577c3c4c4a86b80e7491ee84310683c1fc98618f5836f36dc7773c016519f2338f938d32dffc71201ebd997e07deffb08cc5d5e0a95a6fa819054e0c7af9345d293fb1e1d1d85a3c78ec0debdbbc196eb3e31318ef1db303b0323dd1130a600c1edd82e31ac2056f3f195a9bce1ce4fd74eed861b0e6235abaf7ef52bb0b6b656b564338e50b486b4a8bee9631ce9990f8123eed0ba019c3c79121e7ce8a132e7ba70edf5d7c1d8f828500b37081433e76f3f04a62818060c04a28c055f3ccbdb68317d0a24c259aaa6857e74905034353906870e5d07afb9eb76d87fd52ef4e3f4e9d3b0b4b4e4e3e88857aeb2963521b6062b60f5ad27d358cac5826a9b79721e12e180ecf3e7962a9d39797dca51bfa6d4cab3c30940010705cb51c22255722a847dc344c9755261ba8f211e83fdb40fd4de8f549d2edc277cc43d00fcda118609b31c7fbcf297bb4fad46693eb06a5b96649ac1bfd4d1fe437b08f7da42587b53b56884104722b591ce1a1134d963f95e1bf72da8f60f1ebb7051d963aaf746e5bfa95536a3db6efce9336761ebd62d70e38d8722f994db107dd3f77fa36da96f92eda95cfdfddfac9bcb93ae875977fdfdafbfff8de2bff1592e3efc5af19fd791b341f1aff8e77a52b98a7fc5bf6423b755f12fcb6f9ba3f887a19fe5e2c3af15ff791d391b14ff8a7fae2795abf857fc4b36725b15ffb2fcb6398a7f18fa592e3efc5af19fd791b341f1df8c7fb105a19438a981e9b914802627f833097cb9856d733ae768fa8c9f37f9304cf2e71644d2cdbfd3fb920e695eeeb93b529f53db9ac0d51473291f52fd4dcf9be22bc99174f3314d3ea7e3d235979e5f09489b725dda78dbe29fda9103ae745ff277d875c8c55fc284e4e7b031e187e25ff1df748fdb988ee5639a7c4ec7a56b2e3dff76e27f6d6d1d3ef4c13f80dff8f5df81b9f9f99abe1e5692b1757203afe4e2be219f534e3e910cdc0749148e9815485cee397e783b35de4e90fbc663e86c73a4ab50bd877414e119de33f9bf2c905ec3ef313f9cbc75574dab3cef86ea576e3c55d8c1f9a9dc40d0e85b4e8229422bbd10ef5049ece0c16be17ffb67ff33dc75d7ed60fa3656bee9443e93f1b4211b2f2bdfb94f710d0c8e89fea6e950582c24f5d4532fc0fbdff761989f5f0a41b28c8005a813c9701d1fcbc9e971b8edb66370ecd851181b1d81b5d555989e9e82d5d5351829af5dd5b0c1c378bbc2c7301b0d50ac2d3e9e9e9a82a3476e84d1890978f8e16fc0fcdcbc9f5684ea50862a4119f06424b2d2f871e05bb5b9ecb974e9327cfad39f86e79e7d0eb66fdf013b77b996845d30a5e3d6395fcae9848a486813b2c4fafedb0ee6aeab806588fce4ce6de1efd91060839429989a9e8423476f8037bfe9f570ecc40998999d8673e7cf22966cbfe75b2b96f3ba231d6fbbcbe7d8b2906262901ce6cff87207bfe986a57696b64672a2b69b4e7687918ce81ea640c8cf82b5c1730755dd8af945ef373f18a4039f734224fa646a98365055faea306c9890171692bd83bed95e86f6131189e9c67d85ed514844e224ca88035bdb832816e2fb8a886c01efe1619d58c9b08715c43ac54045bfe81fd35584ea56745d7b3f185323c3c5969e6c8df07ec7ef75ebeb3d78e6e96761cf9edd70edb557d762a2efffba4dfafed7dfff399f7331e187fefe57fc37dde336a663f998269fd371e99a4bcf15ff8a7fc5bfe23fd5d71403c936c5bfe2bfc956fe9dde977448f372cfdda1f8878171e99a4bcf15ff8a7fc5bfe23fd5d71403c936c5bfe2bfc956fe9dde977448f372cfdda1f8878171e99a4bcf15ff8affef06feb1025613b05323a431390569e2a5464a414a754a49398cc352d2e6f435cd878c8fd2752e2ed2f89cecb605be92184b31e4f7d331c3da993bae245ea92db9b5699b2f6d585cae34af2db79aec1826879a70943ecfad7f2ec79b62dbe427bf37ac6f8a7fc5bfa427775c49bc525bda70979bff6ac4bf6b69f52bef78273cfdecf3a1024f35bfe032189108e5404526701faab0c35b9a912c47b8223b88e460e9392386e07c223504fb487e8c61e203d988a4ae5015c778472399c5325b2c54642ab2d35d735f3909820e2479409da042b62501c6e78e1884d577885c069e14363d3b037fefe77f06def6236f437257a743fe935f36d46c0ae2224527f387c84386c52531c9867a57ae02d607deff115858580a736c2460d930cec4b85998dd38033ff4c36f866b0e5c5dce99c318740a03bd32d6ae3ad6a86b439862cd241fe1204294eb32383a3a0e870f1f84f9f905f8dad7be062bababc1814e19af8e275b850a51c0c857be9ba123ca59086143528c6b69f8d493cf94365f033bb76f2b6d1c05ace8555054c378d36706b5ef17751783c220af5b3a3236360afbf6ef815b6f390e478e1e85c58505387bee1cac2c2f63ed2d8838f17e0023a341683558b5d82b2af9b64f418b2d257905af22e43c468c2aacb11c8e580cf3237e85bd24fa97ee5b35df2bfc106989f4531bc422c11a45cb573ef3ad3eb1f5609061c25cc3da77daa00bf70547da0c2d136db8e7ae098b3dd602d51dbd40da4a4998e43f12b268af203bc90f5b112d2316680fe4ef3a5b9131a5f70dad41dc274877d847e8c0b52caa7dd0113f69bdd1ff4008732d398bb0cf2d2e2e61a5b57b5f7f4f997763adeb285debfb5fdfff39ddfafb5f7ffff3fb46f12fda988e55fc435657d3f35416bf37ac6f8a7fc5bfa427775c49bc525bda70979baff8979fa7b2f8bd617d53fc2bfe253db9e34ae295dad286bbdc7cc5bffc3c95c5ef0deb9be25ff12fe9c91d5712afd49636dce5e62bfee5e7a92c7e6f58df14ff8a7f494feeb89278a5b6b4e12e375ff12f3f4f65f17bc3faa6f8ffcbc77fe7befbee7b7bfa2075b0c9e15c609a12b9cdb9b68521679b6ce6f2a424e2e39b123e273f9d9fea947c92644af3af3469d2b16d36e4124af2b1698c04f6741c8fad3426a7875fa7fe4b36e5e290da41f2a4b872bb9bf22137a749373dcbcd49e549b99afa9182bf2d7f695c4e4faa4f9adfe67b5bde4af9cd9f37cd55fcb7eb50fc7f77f1bfb2bc02bff99bbf031ffde827b08d1f585b9197987d9c4810c949acb21555788915ac9cbd3437c8a9b5f37344913e2396d8aae5171273828cacfdec39afaa83d7ac1d1b84fb7da6cb305c79c25727fad44fd69b6c261d440a8931717e3bb1142bf6dc1dd4c6cd8d73ed1b5d1bbebffed77f04fee12ffe43989e9906a4cb04b24dc795562a6c9d345659d3f0a986c498a5a9122a602101eb831f8385f9c530ddd62a6079bf7ddc1c25cb918aeeb8eb76d8bc710312452626266065651543ebaa57b9e7036d08831d2652c760e0395642a38bd2b0f18949b8f3f6db918cf495af7e0596575c85aeae276059efa78fbf77acc032542e6f0adfeab267bd5eb786bd7578f6b9e7e00b5ff812cc2d2cc055fbf7c1d4f40c92dddc336ca319a95880e42863e54a4fd9c3e9ea84bcc71685fe4fb71433333d05870f5e0b6f78d31be0d6db8ec3f2d2329c3f7fbef469d11383a060794278f3ffa1fce247c8d6dabee0088a4840721f03f56a5200b57707c7124f8b2254a2a3dce698e2fb8de5f2fc433f969edb3a41b2139e23a6833de9de46cf9cac0eab4e45bed35e42fb8af39548511d564d0a4c45408bfe0699785d1491ecc963c3ed107fbb043f5037bb57ad3fd9eb7da0675116c3bd61f188f12412aabbd1f7ab4795be8ad05690e25ef03dccfa0a75eefbe4c953b06bd776b8eeba6bb0fd65659abeffa531fafeafcf69d24dcf72735279fafb5f7fff37c522a743f1aff86fb24ff1aff897629edad4345e9297da99e66b6e4c4e0fbf56fc2bfe736b98b399cb53fc2bfe15ff8a7fc5bfe29fe44971e57637e5436e4e936e7a969b93ca53fc2bfe9b6291d3a1f857fc37d9a7f87f75e1bf56018b0be30ee70c8786a0496324c3b8f35250255bd2f1b9854e039b7eb7e99682ccc7e7fc6b8a496ea1d3fb1230525d397f2479b9e491e223cd69f247f22317dbdc3cc9a7dc26d4a6bf29264debdc944bb9f139ffa48d329753397fd27c909ea539deb43e6d78e0f3257ce5d6808f976ccfe57b6a93e25ff12fc54e5a8f26fd4d31f9cbc6ff030f3c04effcd5ff08e7cf9dc7d68086c9e907020484f381bc868af8e1c6504b343a6c200cd47c753638324290115b6fd9aa9a8c3bfaec592e0f2dc52890be287e44b8e8532519086dbd9cec2416fd7af02201ade6a3652dcc424cdcf9da7a2fda31606b78e03be879228623fa5c7dcdd5f08fffc7ff0eaebbee3a1c5014ce76d7360ee916c1c77a1e0583aaf5c37145f09f11d0a21f5104bb67d1fd175e3c091ffae0c7606e6e0168b14d68731778202c072c123b0edf70088e1d3f0aab2bcb303ad285c5a525d8b861369260465d1b428c573fda2ba2056d34a1aa11f8aa3f211f9c71a363237043a9ab5bea7ce18597e0d2a54be5f38eaf1e15ab45795fc0dd334ece08cb4d1372c707e0dcb973f08daf3d0ccf3df73c4c4d4dc2f61ddb617c6cdcfb1a72c31199a8c561158bba034df8f75d226d586bf79fd0d2b0fc9e9818870357ed873b5f73071c3e7c3d96ecba3c771996979760bdbf863e1076a25e1eaea8bf4830ec095f5801ac08f1349e2cd44b5a7b42c845baeeb096823c6f0a366e00e7bcd520b5370c58a6fb2114b142d5c05fe2827c03759223d058c24c9863d9793fec2b45207d59c27cc02b9199fa7cff097a91b8d5efd7d6926c25ddb11a5638628b469acbf7301a67ede06f26ee43e55cddff603b8f07edaf31a61873032c08718d8055e57295b2ce9c3e0b7bf7ed81abafde0ffafed7f7bf142b694e6a97fefe97fd68b25f7fff2bfea5d849ebd1a4bf29268a7fc5ffb0baa5b84a796214ff03f714ff8a7fe9bcc97ec5bfe25f8a9db41e4dfa9b62a2f857fc0fab5b8aab942746f13f704ff1aff897ce9bec57fc2bfea5d849ebd1a4bf29268a7fc5ffb0baa5b84a7962fe02f8efdc7ffffd6fe70a9a1ca6a3c9f0f4bbe95e2ea8522041703617fc9c7d6912a7e3251d92fc744ccedea685ccc993e2dda633f5259d938b89b40e921f393b9aecc9c55c8a371d29f8d3f1dcee9c9cdc5a4a7994b3439a238de3364b73a5438a4b9bbd52dc737649984bf5a5f1a47b5c86e2bff2351d9f8ec9d9cb73558ab3244ff1ffbd8bff8b172fc13ff9e57f0a8f3ffe548df044630664b96f670340bd2559b81ffdae1e0cae9f3b471287274e20f123900aa875e140ceb36f4eb48aba486eb8ee844a5ab1324e78d68b55972a9285b115f1cbb50274dc19b281885ceeba90f2ab30beaa92b5b1f51ad9ee5b9f15a18558013ddb8799d969f8855ff85978db8ffe08cec54a3ed00fcdff285e55a8b1121524f9894c9fb0d6ae4d63a89e653863a86e6610e80958275f39071ffcc047cbb5bf0c5119c6a05bb5648c39ebee5bb8e6da6be0ba6baf86999949585a5a81e9e929aceae48ef1f13124691505c34f21e75eb58806eaa59e0c12895c75aaa999693872c38db06ddb56f8da57bf5eea9b47dffa368cb314038bc42cdb0fd582c2ba55e5a43048b0bcbc5ce6f7e3f0e0435f85b9f905d8b67d2b6cdab8a9b4b1e3ddb73e2e943b18c782db969c8b2e99187f637abe8e15e6166075ac0d1b66e1c61b0fc39d77de01870e5e07dd910e5cba7801e617e6cb91bd600749f16d2b7ddb411362e92debf73d69ace810210a82ed8cf804de977ec0296fc197c315cae455a8a8bd1f9b8f1f5a63180c09e937549d2ae8afe20a953caa36177458b6f7d041fb11f9d161d5f6f848c227e92938592ce03bdd5fa39f218803fb5c08aa0590f76e56e58fc8833cc67dbe175a3bb08f63c5bda01bfd742d06d9786cc16a99bd613dfc3af945a7768c67cf9e8753a74ec35bdef27a6c45a8efffbcad6def4e2e3bb5f1fbf1fd9fce950e292e6df64a71d7dfff90cd51fdfdaff897ec51fc2bfe15ff8aff363ba439d2386eb334573aa4b828fe15ff8a7fc57f9bee61ec923097ea53fccbe3251d8affbcad6dd8e1b2531b15ff8a7fc5bfe29f1f8a7fc5ff0f1afeb102560eb492914dc6f2eb9cb3e933694124837301959c971234b5270d84e46b2a9bcbe3f79b12458a5beaa32437d5cbed6ed325f9d404b49c8f520249f6e63613e93a676f2e3ee97dc95e9a2fd9d56443134025ddd291dadc9457a9fd4d38e0f39a36eb5c3e34ad832443f225a727278b5f2bfe15ffd275cedeef75fc3b02cd3bdff91fe1c31ff9c358412897434079eeec73f78c19a8f8c465445c84b948308040b8b0be42153e0a555f9cd75176aa3fc4a4cf630055151d43f7f9bca0cb043bdd77af5755aeb2611eda10e6f02a39d46e9193506ca886c3db289a709fd6ad93b44233a1aa51afefab35bdf5ad6f80bfff0f7e1e366eda081d13c8678e80131ce14b86ade99080d589c4b4e8b10db171d58f6c7eadeb0bef894667ce5d860f7ee02370fec225f095a4dc9723bf7590c6e5ab6b795bbc3b05dc74f406b8e1f0a132866b91dc36313909dd4e81ed085d5bc26eb7eb5bf16115ac02600893bc539d78e1e360606c720c6ebce110ecddbf0f5e7cf14578e59557cad81b2427f95887d884adc111839c5eaa0285eb1356d9826ff877eedc05f8ca435f83071e7c082e5f9e839dbb76c2e4d4148c7447716ea7f4dfb77af3761910f6f4762656e8e418185c113f7dd4313b3309070f1e80bbeebc0d4e1c3f0a93935370e1fc79ac288639dd37488433141c93c42a5c13c6908c632092e66aef0853e9a71c760791ecaa7caa638d8f8de3a8c25c886b9154e4e23a4d3033c5a3612d4c4947e547112b7041bac7b07d8130cff14b7231f4414e6c57c86ce3b8253b8191a72c8b03c628609fe740116300f11ef90801e71221ab1663a8f63d2456858a653c167e9ff1fb07c681aa61d9e0b9b5b5ca8367ce9c85e9e949b8f1c61bb07a9cf46ee436707dfc19f924bd4ba4f7437a2ebd77f5fdafbfffa567fafb5f7fff2bfe15ffa9acd4ce9c0cc9979c9e9c2c7eadf857fc4bd7397b15ff8a7fc57ffddc28fe15ff46f19fc62f1727291e46f1df9a5b391f53ff5299a94de97de93a67afe25ff1aff8af9f1bc5bfe2df28fed3f8e5e224c5c3bccaf18f04ac7452936269319a809f7332f7dc3424a6345f4a96b67369a1257b723ea7b6a67372099ecae2b648e7a91d4d89917bd6b4f8397bd343b2a56d4ece2e2e3307c8a643ca8726b0a5f7a4f9a9fc61eee57448760ea34f92d7365f5ac7365b25dd4dd78a7fc5bfe2bf1dff9ffbec03f0effffd6fc2a54b97e33fe4a3ae3016c2396f0be89e1319a856114ac8017ecf9173623b409e03a18a8b1b43d56edc41e48922907ddcc15ba799a09f2a5a15698cd2fc77637beb48fcc271d6c64a3916ea842bae835ab4754295ab8178941fd7f2ad1b5a3772ec78db3d09cc7d8f8e8dc22ffcfd9f83bbeebe0b2b6d79c289c50ff2685040a055e0b70df79c4cc082485475c71d6bab6b3e6e483a81f62310b02e5c5c800f7df0a370e6cc05efbd012428618b44a78711b04c6889b8ffaabd70e4a61b60dbd62db0b0380fa3a3a33056fa73e1c225989d9d8691916ee9df38ab7cd68e0b886ca5900bae9a558848c755162b655e7fdd01d8b37b177ce3e1474b5d17cb585b968b3604adc0e060b61a2fc9d61a4b96f24c0769582e66cf3fff027ced6b5f87575e390db333b3b07bf7eed2fe51df3e10479bca3c3a6dc160a5896ca8266381326793ed41d1b1c8f9999e9984ab0f5c0577de792b6cdbb113565757e1dcd9f3b0b6b60ed4f18ec86fd4d6b187d5af0ce68e3b5c4e51252c1793b85f00c4ea6f9ce444f909741e3e9443ce7bcaff3e5578827acef9e786386a3512579dfc46013191c85464e26739a911d87e0d5555ae1ae9119277a30f408d4819c9a050df47d11663aa768be5a7c7f620e0fbb231b57dc5065b896045b61358624c798cc35cd2e75abca22f44c062fb455c07b61e5481cf175433f539d6b2b97d387bfa1c1c3b7e0476ecd8aeef7f7dff4393fdb9351bc6a7269bdbe64bebd866aba4bbe95ab2457fffd76d4de728fe15ff12769af429fe61407e6a83345ff1aff857fc2bfe259d8a7fc5bf51fc2bfe41f1aff857fce774375d2bfe15ff8a7fc5bfe2ff2f07ff48c06a7232770c03400964e96271f00cb3b869b2e76472c7d3f3548eb4910d9b58c300ad2989a5c54a65a68b9ceacaf996ca6d9a3f8cbc61d63cbd1e76b36c5a770e96747e6a5f9a4f39dbb86e3e26976f6dbe4a764973d24f6e5ed37ab4e12a17c336bb72fa73739ac60e6313dde73628fe15ffe67b08ff8e9ce04857fffc9fff0a3cfc8d47fd7df77172a0aa3243fa52025678e08a3095b2826e8a811b13e616ac0da01bd2eff563e52bd245e30cf902550b316e77b431f5c7c96184082e87481868531887cf89ad446b149e391fbbd47a2dd84dbe634c589e53bb416a2356f0b688dc87f2ba33d2c5f683274edc0c7fff1ffe3dd8b86953b0d31133fa10c30f108854ee3f146f7f4ddeadaeaec3d7bffe287cf4637f04bff7be0fc3273ff5e770eaf469d8b479134c4d4f315259bf568108e86eb95ecb4b2be5fc3f84975e7c05a8671ffe714c21dbabf4a2af2e163d18191d819b8f1d81f1f151181b1d2bd7b10b4b4b4b48bc1a1f9f4012cbf8d87868436807d6307ff88a61a81a63676a641d47303a70cdd570f3912370fee20578f9c59760756d8556ba9c1a5ab9b198f7c157e0a2187b0f3b109850386e617e111e7ee461f8ec673e0f8bcbcb303d3d0b1b666791f4e55b3a1660ab7a63108b59f12d29f3ba315544d94de3f30e6578c25d519a3439350d476fba095e7bcf6be0baebaf03078fcb972e95ebbc1cdbd861fe22863a51a64b3f2207f503632be2a953d40895104961641d554f6238084f89c45484a4e444219a1ddb84261f1b30916285f60edaa77815ba885b8160c5f77fcbac0748f6d2209bf60ea7bfc34863e41fed0744e4ecdbfa5f6a0c8f03c331ad032780d2fe00d4eeb016efc134716beff20ac961aee215db1f20f10dd87b06f79e503d8f6214634be4b410ab8585051477ebadc7b115213ff4fd2ffbfa83f8fee7b29ac6a77e34f9935eebefff415dfafb5ff19fea55fc2bfe535d7c0c9faff857fc2bfeeb76e5f4e7e6348d1dc626bacf6d50fc2bfe8de25ff1aff8afdd6bb241f1aff857fc2bfe15ff8affdcbd61eccae9cfcd691a3b8c4d749fdbf08386ffd8825032527232b7a85200da00394cd24acf9b029b3aca6d1e666187f131e75b5392e5163c7ddea48f7f72f37236a47224dda9def418665c9b0e694de87e3a4e8a5baa5792c3f3e24aec9540dab696a90c49ae6493b441a4df4d364b3e48714ef549f214ff8aff749c740c33ae4d87b42674ff7b11ffebebebf0a94f7d0efeafdf7c172c2d2d7b22817b6eabf65c449290f2278e217b80e5373b8ff6b8935039265d6bc3e405e1f19a4783485aee1e55cd72d564faa102969b43c490686f18ef64520b441bfee39ec7b9617e6c59c6e6d6f64bb28f6496fa7a6c0e97112b77750c46a5db35f0533ff513f0d6b7be19464746f076a7b048f32942eb3d43b6e139e933f8d4553b9a9b5b86f7bdff43f0bffcafff1c7ef7bd1f84cf7ffe0bf0c0035f864f7ffaf370eefc05b8eedaeb61d3a64d41402f1697a2ea5a1862d3075788eca31ffd043cfdd4f3e5bd2ed04a1aeb5ba15583bddf8e28d4eff7e06d6ffb2bb061c334e68f2393cccecec2e4c40456f672f73a23a348fcc0867fd1a18603d558f095acc84e9f674588818ba1ab2eb673e7763876f311585d5b85a79e7e0a4964851981115759cd86a0611d2f4f4642528ae9fb806205a90e4a7471ecf5435ef60d5cba38070f3df835f8ca57bf56fad9858d1b37c3cccc06f4cf554a725c2cac6256f4b1d299293a8c508341230e5bfcb8768e7115f1baf0ebe9da46a28dd5079f94325c15b123371d863befba15f6eddd05ebbd35387fe1022c2d2f06e2a0411f3c1faa83b6badc70b6b9fcc0168cae825741391bb0da27e358f538432dea0af0ad22abea5929d9123156ae6dda2e1008c7844fc207ad5d323e56720a731cf63a81ec4878a3f1ee792449058c522b500b72252d5e29abd3edd6f6a14ed069a1da8fc82e6a5748f805fedec5e5adee0dfc768af10cb960ebe4d1da7bb1e8d4f645dc23681f0c7b5b91ec8f712fc47884fd8bc687fd9a0e5f65ccc29933e7e0f00d0761fffebdd9fd9bdba7efff1fbcf77f2a43922bd994ead3dffff9237d9eda95b345cab51c3e9af2302737b551f1aff897e44a3629fe15ffd2f3267d46f13fa057f1aff8cfd96214ff033636e94ef5a6c730e3da74486b42f715ff8a7fe950fc2bfe15ff793fd298f27ba93ec5bfe23fa73bd59b1ec38c6bd321ad09dd57fc2bfea5e3fb19ff032d08a524ca19980a938291bb671a802a399d0b5853002440b7f995ca4a754bf1695ab426d94d9b714e7f2a373dcf25654e77d3b3f4d374482091fcc8d9d5642fbfcecde5f2a5674d6bc065b70127272b952bc968c217bf9664a7e39bb0326c7ca51cc85da7be1bc5bfe23f634b931f39bb9aece5d7b9b95cbef4ac690db86c692dd2733eefa9a79e837ff12fde012f3cff927f4e72486ff838028a0d2dc9a21eae3b900dd6590b313e96f423c9c1b17e4c45ecaaf91c08603827d8dd096dba8a40b20a0ed565f378713fc2f348d630be55216f7546bef0ca559eb4e37d76cfa9420e84d814ccce74cd0cf9ea64d15a04fdebfd35d8b57317fc837ff40b70dd8103a13a0f205908296383bfbb6b2be11ebbd674bff28e5f83ffed9ffd0b78ecb1a76069710556cb7bceafa5e50524107de52b5f8763278ec1f66ddb42384cb4c39f85b52c46e18fffe453f0f0c38f83015ea9ca822f96c473df7f565696e1d6db6e81fdfbaf82cb972ec096cd1b91e8b2bcb202cbcbcbd0ed7461a4db81b5f535246479620f5cf141b9c76f38c294ebb4b765db26b8fbeed7c0be7d57c14b2fbd04a74f9ff304246b435a5abf7e81bee50937448a29a2ff1de40fb9fced966b5dfab6bc0e2fbe70123ef5c93f833ffff467ca782ec3d66ddb616a721a461cd1c978439c4f8535d12eac0e168d76ec9b105f5b241ea5de99414f0d606bc10d33d370f4e61be0ad6f7913dc72eb09ac3676e6cc195c63cc4bb00113167dc3ef2007895e48d8f3f72c8e2de2b9093ed4f639bfd2be352754edf808477d76ed9578dc58cbaa7341b286e99e04c95f986c45b2c451c97e99e28c133791c8156caae923db822eb20dd81e5894f94afa63052a8eed309eef5b288308aa644f90155b1d5245af4012c34a67166a3ab85e1e8bdafe1e62e5db4afa15e36d52d194646f8b3e3a1bcacfe2e2129c3e7316eebaeb769829738964a76ba3efff1fdcf77f4eae2423959fbb9664a7e3f5f77f7b6e37ad59ceaf5456aa5b8a8fa44bb2ad2d7e5c97e25ff1aff857fc4bbe4b31c8f9d116a3a667e9a7e950fc2bfe15ff8aff363f727635d9cbaf7373b97ce959d31a70d9d25aa4e792ac54ae24a3095ffc5af1aff857fc2bfe15ff8a7fc9772906393fda62d4f42cfd341d8a7fc5bfe2ffcaf1dfb9efbefbde9e0e6a725c0a68d322e540cee549f3241b52fba4e4cc81305df03659925c33c426933e93ec90f44873b8fedc91b3bb4957932dc3240fb72b679fb4865c464e5fea572e87d2b9b99754aa53ca5569bc3437772dd995f3b1ede598ca94f236677b533ea6637236737bd2f1740da0f84fedca3d93ec90f44873b8fedca1f8afc67f37f0ffeef7fc2e7ce4237f54fdc3bdf5e40067616c7b466be4e6065bfbac8a55f4d7d90780a4101bcedd3dac881448018e7cd51d1d055b7edbf03cb6f60aedfed04fd662cb1d8e6c41448bb5b53594dfed76a36efcd30f95a84a39dd5065a71f3e91c8019e7e43957b80c583085eee1ce79536d2bc1833774e640b63eaedf5dc73eb893ffc1a753ac20792be3a70c30d07e1677ff6ef2229c260c522df560c58abc0dcd137161e7fec09f86fff1fff3d9c3d73b6f4b5fc1164bac80be91406ab53b938bcf2ca2b58a5e9c48963303b3313ecf15f15e21c11ad035ffce243f0f9cf3f00bedd9ef3d1224dc7b78f034fd7415f4d986d61ff55fbe1c081ab61df9e5dd87e7076c306989b9b83d5d55594e1e2e3c8581b376ef4b12bda3c4b0f53fbe0cab930211fc5e2edb1b151b8f6da6be1f8f123f0e8a3df84d3a74f21390d6d756bd8f379e0d7c0faaa5398039de08645d9369c7b7cf9ea5b2ba51fa74e9d86cf7fe10178fcf127702d77eeda0913e313655ca85a951f8f313214558bf1231fea04ace13df779e573727c7202f6eddf0777bfe64eb8fee0f570fefc85d2b693a51de55a5b6a5be9ab7a795215783c78c7e2da217e823dde768b31425c06396e4687f284729ef684f2333232e2f710a8aa49d13555bb02f64d38aabd03c33e46a4462252d5f064aaaa50744d15b008577dd61291bf4738d6692e652ed9c5c94bc63b1bf2b46a61eaf617c23cf952b0567ffcdd40fba23bdc5e4755f630efa8625f207e42f43b50344dbd8a18c907dc0f21da8b15e04a9b7a2483d94a361a6a955a9ebb7df7ecd973b06dfb263876ec667dff1b7dffe7ec97e224e9d0dffffafb5fd223cde1fa7387e2bf1aaff857fc2bfe01727e0c638be25ff1cf652bfe15ff8a7fc57f93fd8a7fc57f7a486bc865e4f4a57ee572289dabf857fca7f7529bd23836c992e42afe15ff8a7fc57f6afb5f04ffb1029631cd1b444e510e106dc1488d9400d294d03907e93c5d901cd09a92253767181fd331b944a023175bfe2cb7e06d6bd7b499e5e648b64b73d223b7660032409b3680142c4deb21c540f225671bd7c9f34102a6b4eed2fca6317434013f7d9ef3a929c64dbad367fc5b3a57fc2bfe9b7228d5f5fd8c7f475ef8dad7be01ffe29fff0a5cbc7029fcc37d12273ecf98d8a6ab08642a530dc60f9128808812cc0e2256b836663d47c80a5563a8ed178e0bc4892210b5dc4155aa90c8e188076e6c29a3c3db103a3d10081ba15a56083cdeeb509cca6bd726d0cde1557362fb2ff0240b03506bbb68829c389e113c6af9c1e6479f9d6c8ab9ed43b7db811ffff11f85bff2b61fc65662856b8d17084f10da0fa607af623437bf08ffc3fff04fe0812f7e19906cd3f751c0aa5eb4be6e6c79ff89279f4672cb8913c7617c7c1cb87047dc71342257fde9e4a9b3f0e10f7f0c7ccf3a006ade88059e5c3b3b0b21d6211ec81be9c391a33721f16b797905ce9f3f07a3235dac7e353d3d0d2b2babb06fdf5e58585882c9a971b8e2c3489f90d3f8dcba60c368b780dd7bf6c09d77dc0a53d313f0f8934fc0d2e202040a61d5220fdbbe01fa0be1a923c361c8ac0d91b7be7d5fa823e5d2c8f9f1d4134fc0273ff967f0d8379fc079dbb7ed80b1b1494fa03156b63b9c572d0887759bf28608850663edb8869313e370e38d87e0f5af7f2d5c7bdd75656c17e0e2a54bb0b2ba1272be225741206f21061c310f098ae98f78f2bdca3ba7ae0f6c2f01a8c8447c4f26a22255a03255453b1fdb600b54eb86631c1e8bca4ecbb0456b6593797cbc61b833a62269f17dce06bbd223ee8be5b92327fac4ae7493fdfd501daf2623cc8b7b0bed8fc15f676327c840ae60dffbd55befd56319c859b467599f9455ecfa751266dccfc39e6898ad365412acbd1ba29ffecaf9f0ec332fc0b16337c1f6eddbf4fd6ff4fd2fadbb345f7fffebef7fa3f817ed54fc43cd372e9f9f2bfe15ff8a7fc57f934f4d316ed29d3ee3dfd2b9e25ff1df9443a92ec5bfe2bf4987e2bf3a14ff8a7fc5bfe25f92a5f857fc37f9d414e326dde933fe2d9d2bfebffdf84702566e504e791aa85cf24b729a16b0694cdb22e412af69d1253b2480e4829e8b571ae4a644967c97124ff235b7894931c825ba645f535c52df9beccbc969cab5dcda4b63d2b16d9b5553ae48eb216d086df635bd48d3f3a60daae9a529d997b339b7165c7fdb9a48f6e5ce73b6a432f8b5e21fb2f215ffaf4efc9f3c790a7ee3d77f0b1ef8d24348f40833fc3fe0bb31d60ed8d367959fb8de484e72f75845289bcc757322b9ca988ac05414033ef0d81099035b1186fb8e58b4e6080981f8d00fe751a60d95af78dc43a5ad980bee0315d9cadaaa0a4e116c8630c624f18cb160b2dd79acb6656df4dfcde9873f935313f0737fefff0e870e1f0c55749c6c4fc282a0871fd68435f52ec117bff82578c7affc1a5cbc34e7dbe6e1b43206a6d269912855c667ad07cf3cf3346cdab4a9d47718c95f36b6aba3d66d058efbeddf7e0facbb189a103f34c4fb5f984024a316943e0270dd75d7c2ee3dbb9104e56358c0e6cd9bc0576532b0b2b252aecb2a4c4d4ffb1884aa67c60705aef43010e2e55206894a808429f7d9b66d331c3f7673f9bd1d2e5dba08a74f9f865edf57c372a17504acb8167d6f858d39eee5fa9c2cb08a98639915c6130e7b44c47af269f8d297be0ccf3dff02b8ea511b376f86f1b1714fe4a24a5d1e40108054ceef40beade4e04195aba81d24c51a4c1fdb25ba11ae72dac18387e0d6db6f85ddbb77c2e5cb974b9f2f20b1d1cf075c0b5fd1cc5759f2bc2083360578448c3bfb8b82f607b222d813aa36f9bc62fb0bed278120195bee0104e1503d0fed432386184e7ba1921ee503e925dc12f132ee07c654d5f7c278cc6257bd8eed1b542d6a602f61723a919c57f9c0c7f67d20ab7d217987d01e6699cfe1412442613b4717dbb0c7166c0fa2e734bfe9fd42fb14df77fb50df976a73c3fa7a1caec2fada1adc7efb2d303e3ea6ef7fd0f77fcebedc18e93cf7bb37b5ab69ad72f6e49e83e06bdb9a48f6e5ce73b6a432f8752e0ea91cfdfdaff8e76314ff8affa6354def93cf6df999ae91e25ff19fda9db34ff12fdb92cae0d78a7fc8ca57fc2bfe9bd62a674fee3980e2bf2d3fd33552fc2bfe53bb73f629fe655b5219fc5af10f59f98a7fc57fd35ae5ecc93d0750fcb7e567ba467f99f88f042c496913a8d2316d49922669ee795b72a48e0f030ac9ceb624e33e4a47aabb6993a2e7928ed4de26bff9dcb6cd4c8a976443d326d0646b1a87dc752a57d2dda4b32dfefc5cca23696c7a2f774d36e60094cb81d4169a975bafdcfae5723e97bb39dbaf140b8affba8fd2915b7b29d7e8b9a423b557f1ffeac7bfabfef2910f7f1cdefd9ef7c1d2d24a65941b1baa5ca531b07ef2c01af936752c37036929dae0eedbaaa5a1bbd7611575d0ee84d4e5be897415cfad27431461bc3b90c0447e15f5566f388f3d8f364145e88260bb65fe43b2067d1e77f749706883ae282bd8db0f3a2d55f9024ff0b8eaea7df05fffd77f17b66ddb065e64cf4bc12a5830582b89740612c76ffed6bbe0637ff0a7e043601cb5062b7cf5ac89e31de9a71f8856f30b4bf095af7c15366fde0cd75f7f1046463aa80f5b4de2144700e9c0bbdefd5e989f9bf7fe2351a75af322c6ce5555f2feba167dc74f1cc3aa4ca3a35d989a9ac2f6835bb76e85b5f2dbb51e7455b1a0b4c5558a72e42f4fd2f272c0e4f7ffec412e867a6010694f7e7d5ca5ad23478fc2cd371f4512d52b274fc2c2c29c1f673d31c9f24a57d6535828f7fc73eb09675807ccddef85b5f6ed1d2f5f9c8387bff1087cf6b39f87e71d11ab8c9d5bcbd1714fc47232dc38477ec235008e85615c3460d28185b30933cd73bbcad3916e17b66ddd04b79c380e276eb919b66fdf0a172f5d84b9cb97caf8af059f20e498897e45bc1888bea3bb21a274d079244b4155298e304095a23076261024d9be6ea16a9d678252c3704804c6c20cbe6fd31d98ef67eed30d15f480bd778a30af08394b24ad0eab588784ae700f4994e55ee8aaf1150ccfb4df74420531ba674826ed53ee137c20fba86d6bd33e5e279481df3bc2732492119994bf17781c82df9c9045d5ce72ef9e974fbe02070f5e0707aebeaaf6aed6f7bffefecfad49ba1eb9f5caad9f1407c93f2936dc9e54aefefe876c0ca53848476eeda55ca3e7928ed45ec5bfe23f3d4fedcee56ecef62bc582e2bfeea374e4d65eca357a2ee948ed55fc2bfed3f3d4ee5ceee66cbf522c28feeb3e4a476eeda55ca3e7928ed45ec5bfe23f3d4fedcee56ecef62bc582e2bfeea374e4d65eca357a2ee948ed55fc2bfed3f3d4ee5ceee66cbf522c28feeb3e4a476eeda55ca3e7928ed45ec5bfe23f3d4fedcee56ecef62bc582e2bfeea374e4d65eca357a2ee988042c496913682461e9bda6004906b68d4b1d94ecca254a1aa874be3497fbc0f53725452ef84d31c9f99dfadef42dd9210143b23f675b53d2373d6f922bcd97d6b40d1c4d7649cff83d2936393dd2bae69ea7fad24d24d59f03652e979b72bfc9d7d4ee36dd8affba0d8a7fc53fb7f9b9675f805ff937bf06cf3df742d57a8caa36b1b65b3cdee57f904883edcc7a81f810c6a13dd4becbdf88328d1784e71daa5a2360a5462648c854cca05859062f59cb33c8ec49f139d721ed1d145380aaa28d8548ec22bd129680d689886200b1d51a8e6715be6eb9e566f8e9bff5d330363e06ae8a9223d620c5c38989cdefaac3cdf7159600565697e1977ff97f87175f3c59deefa0554eb56f216781aa2e51352d32676161111e7ef8617084a3abf6ef87c989291819edf82a5f4ebee9c017bef02578f2c9a7bd4c6b0251cd93b59c0c836bd809841603ebebeb48b87ad31bef850d1b6660696909b66ddb0aa74f9d86d9d98df0c20b2fc0eedd7b918c74e9d225989e9e82d1d15130a6aa76f4173a4c3d52c65b5e3ad483dd7bb6c33dafb913dbf4ad2cafc2850be74b5bd7b1052445abca157f5e98e0a36723858a50fd10571388365ea323d0cc5d5e806f3ef2187ceacffe0c1e7bec4924c03812dad8d878ac6a64425532370f2b757d8b7e87d50de4b1e029e68b6b4d58c08e1ddbe0d6db8ec39d77dc065bcb353873f60ccccf2fc0bac3694877976b8151c7438898c0767ce13e7621a5d8da3839546babf0446d07a9c2959be8a7567822d292f7016a95f2680fa016a4406d48c3ba50fb3f9f6f553b402e8fa211db86121e83cd5475cf323f80c6864a54bcfa5d8c4eee3702db3b0cf32dfacd7232ce49fc31612f24fb717b10de3969b52b9245bae9887b2cd32bbd9f5c353a5709ebc4899b4b7ccee8fb1f7e70dfffb9e7a93efdfdafbfff539d4d739b6c6afb96ec308a7fd14ec5bfe23f9d2fcde53e70fd46f1dff8bc49ae51fc2bfe15ff8d7a73b86afa96ec308a7fd14ec5bfe23f9d2fcde53e70fd46f1dff8bc49ae51fc2bfe15ff8d7a73b86afa96ec308a7fd14ec5bfe23f9d2fcde53e70fde6db88ffcefdf7dffff654a164447a9d26620ed0524024b9c3022e952b2dce3049c86d36c60c15fc26db24596d1b557aaf29099b74e6629f8e497d4ac7a7fa73f63701a1c93f697edb1a4b6b9d1bdf24bf4967d3faa771e0f3739b956453fa9d9b3bcc86d1a633cd8b1c2e24bf9bfc689aabf857fc4bdf6df3dbd6b82ddf25fd92fc269d4deb9fc6616969193ef8c18fc2c73ffec7b0b6b256b5f20a6405771001aacfc858ee9923a758ba4ff952f8f673c00806d4c2902ae4b8ea43fd20c3a4636c4582881fd24963c917ebc91454d106ef878a5d646b3ff3e3d842456ae8876b2e3b9e4355e9a7963f2cc67de647088eaf96e3ae437b341cc35a9b2137c8aec35bdffa66f8ab3ff2570341abbc6d43fbc1e0b749ab1fe1651fc943275f3e09fff25fff2acccd2d95727d933a5f6989aa8e39c28fb3d59159829fd6c7c7b5a9fbc637be018be5fa5f75d53ed8b46923787e9071e573e0e9a79f83cf7cf673a1555a20971808e435170fff4db12e4ae3575757e04d6f7e3d6cde348b241e777f6171b9ccb1c5d2c63998981887c5c545181b1bc3166853d353d5fa3660217b589bcc33fe8f35918c63cb18dbd296c9e9093878e810dc7df76b606272025e39750ace5f388f3ef7fbb49636f814ec311e07ae72955b1b97b5553bc90a1f6e9ac3c27a39eef2c57978e4e147b122d6371f7f1c4647c7909036393d83723b31b72c54ecbe2b70d97d8af04d6d0983b31dc4630f5c95ae91910276ecdc06274e9c80a3478fc0caca1256209b9f5bf4766385354f90b4509961e27f295f839fc65763a21c47a216233415acaa14ed0dbcb253c457184b18e41fca03ca318cbfb06717e4b431b5d684e99e11315b0424913f01931dde8294c9ed11f92be8eb313218611f9277108d2dc2fe43fb13247bb18b492462d29e5484bc0de4b5da3b22f811f710a8aa5b418cb5cfa75ecfe727edd70e938e1ccbf7335a237771fefc05d8bb7727dc70c3e10192abbeff7f30defffafb5f9eabbfff15ffd277dbfcb6356ecb7749bf24bf4967d3faa77150fc2bfea56bc5bfe2bfc9a6f45bf1aff84fef29fe411cd794ef927e497e93cea6f54fe3a0f857fc4bd78a7fc57f934de9b7e25ff19fde53fc8338ae29df25fd92fc269d4deb9fc641f1aff897aebf55fc6305ac5c509a00de66ac9408a99c9c734d72738ef1f1dc766e0f3f9ac09fca93c6e536b6547f6e23906c1836d6399d39dfb97c6ec3b049df0414ee63eeb9b47e691e709ba45836e9bed218e6fc695aa3dc38093b526e0c93ff57b239a576e4bea5d8e55eb4b9718a7fc57fee39f731f7fcfb05ff8e0cf0dc73cfc33bdef16bf0f2cbaff8e77e50d4cbff61de2472511654edb8682efa676d0d87355215ab1603e19b2ae8a464276e33705c07b202d968c27cd40b55151923c405f5419277c146226744fb834c246cb84a46e15e2ad7d83ab124185ec51f3c718262e675f6e067ffdecfc2cd470e6385a5c2f402f5c593734c884f9517a8c893ca4c015f7ef06bf09ffed3bb606d15b06a15861d2857aa5818e32b37f975a1367b052c2f2ec3238f7e139e2d7360e3c659d8ba65338c8e8c812d652d2eadc21ffde11f2341af08edf7bccd36fa43761585b77a75650d76efd9811575c627a6e1cc99b3303636026b6b6bb0b9943d3539013333d3b0bcbc8cb64c4e4c04c24807ea07c51df2477031ade214020e9ec4e665395299f3d7556cdbb461231cbdf908dc78c361585d5f858585052487f998faa92e9606dbf3f9b90556abea7bb1c0f08fa423bfae9ef416084ae59c4b972fc1534f3e050f3cf0203c517e778a2e4c4e4e969f299f07d1bfe06b585b72ca5002d052923f8620d30964b3ca5f249e519ee03a59181b1d81bd7bf7c22db7dd0abb77ef287dbd54c67f1196161782ea2256f972e7964ca8c929223ed92e50fb4eb725be27c4ea58e419619eda19d6f6bcaaba16e05ee0ef194e4764f8434253203b460cdbaaa296bbdf6115a6e8796c31185a06d241f8ed06ace2d8d0221575607530887836cc5660d876472f54f2a267bc02184052e92ae4ba05b647fb41def6b0efd0de6ad83e89e77d4ffae4ef2722b019d66295c3c655a97395d1def086d7213952dfff3f58ef7f699cfefe0751179fa7bfff15ff4df629fe15ffb9c328fe45dd4de315ff8aff9c0e6e1fbf967429fe15ff3919fcda28fe45ff14ff8a7fc5bfe25ff1aff86fcaadd457c5bfe2bf29e6921db96fc5ffab1bffb105611320a5c56a7328e73c372a1d2be99664e4e6a40920dd6f937125470e98fc3ab7d9e412e04a932227ab0d70d23c2979f97d29b972fef2f54ded914025812bd5d3e443cee69cacdc91cb9bb68d4c5a6bc90e495f9ab7c3d8dbb6167c5c532ea47a8799cff5e7e42afe15ff397bbe57f1efc80bef7fdf47e0231ff9c3aa0a8ef5ffd04fb33add4e55c926cce3642a2eb710d690aab630236a6d09692ccaa5f9b5e14426a874235926ccb190c4da56d578b8bf54adc78d74f3894c9556ce011e67a8aa7f99e43e274ac4ea3fa1351a11b8621b46be96316eae62cd1afc77fffd2fc1ae1ddba163fad87e105b102299067c1b3713621d2a87b96a56d6f4b16ad3273ff959f8d087cab5b358fb0a4d0fcb57b5770bd5af6c7fddcb423a95094422c05664cf3ef32c3cf4d5afc1f2ca1a6cdbbe036666362071e7831ff8305cbc70a9f46f04b03296a9b79ef32a6c8c8ae3768d8c74e1c03557c3f4d424565d9a9e9ac2388d8d8cc1a9d3a761ffbe7dd8aed055f05a5d5b874ed18191d1311fa7be055fc1cb7d7742a41b3ec6c7537e6c62ac3b9d6eb5bea5de89f151d87fd55eb8e3f61370ddb507b01ad6d973e7a0df5b0ff1736bd6431b3c71abc400acfb8a5116eb8ff9fc72c4341bee39f293e76361252c5f7dac0be7ce5d84c71f7b12bef8c097e0b1c71e87f18971d8b8d191d166a00f6423603c7cda585c2374a1083146f295ad7c73d2a9ca97fbc6f350ed0c97c493aa3a6885f31f4a9d5370f8f021b8fdf65b61cbe65978e1a517e1d2c5cb68bf71642ee34793ff88aca28fd71dd30d396d92d67fbe0d25d96443ce1721e72bc078fb90cc8655bafc3a4324b351f52c170ff2c355dcf2f322b10c825da6febb22dd2b707f62d5a40002e112635ac4aa525429ae48e4c57d10eaef26f2cbe503ed81fd5e2fd816f6a8209bef935518ea7b7fba8f12398c9e911c0876731f7332dde1299680a4ac3edb57a93a96ab4ed709a4b2b367cfc3d12337c281035789f6a6baf4fd3f68cff7eafb3f6743aa43f22b3d4f6da3738eab267bdbd6828f6bca8554ef30f3b9fe9c5ceebf8469fdfdaff8cfc94e6519c5bf388fdba0f857fce7e428fe15ff92cde918c5ff951d926fe973c5ffa03d8a7f18b08dce15ffd02a233747f12fcb51fc2bfe259bd3318aff2b3b24dfd2e78aff417b14ff30601b9d2bfea155466ecef722fe3bf7dd77dfdb87057daa5402413a3627a7c9e0dcf354beb4e8579244929e61f4a7cf73499eca92003aece698da908b63d3c674251b6e0ed839bb523bf83d2937a479697c7231e47372a0cfcd698a474e760a206e9f243717cb544f5b8c243b9bd630a72fd5c58fa6b1d23cc5ffe03cc5ffa01dfcdef70bfe1f7ef851f857ffeadf21c9c68db5fdaa851555977263d3d68338df06a68ff1240477f06a597dd64a0b2b048531ee3e111c4c1253bcedae038189c803782fe82559dd50f506ef7119212681d611ed2a82cd4464e0642f3ed61d4450c04a45a142cebab33fe8a2794e9f1b9be2a343240fd69a917c767abd6e0b23a35df81fffbfff1f989c182f6d443a8f27a71445b4cc181b2a1439fb1df9aa87c62e2fafc31f7de253f0a94f7ea61cd3655659a4a954696059551e27a783c4218c57e16929fdfe3a9c3d7b163efb99cfc1a7cb8f2365eddbbf1f9e79e65978f4d127c0579082409c81c6c355563a70ed012455eddfbf17494d6e1d2f5dba08bb77ed82b9f939989e9e868585255fb567640473c211b71c192b50ea22a1a5f13016863d0c97691c29c9c0cccc14dc78e38df0d6b7fe105c73cd5570fec205387fee34acaeade2685731cb47d0868e9a0540a0af01556432be4296fb387f30da16426efa6a5abd9e85b9cb0bf0e8238fc1273ef127f0f9cf3f80b1dfb2650b4c4c4e42b7dbadf2c710fd8a54f45921b08067cbc85682a710c87886c5c9c97684a69d3bb6c1edb7df0e77df7d374c4e4dc04b2fbf040b8bf35ebaf5f8ee50de53ae060308f29e1ce47559d6c07060af6316e19c50d90b494b318e613c84fdca923c8b3eba9681e84b51c5a413888e36ec2116aa6a5a784ef20062bb3fd3edd4f6b48255854adf0d640bb529b4618c2371bab5ea31922540b5cf16acd216c933cc966a75c2de6d584e3af9b48fbabd8df6c990afa4c7ad4da7b4c1267673d21b56c20a3aa32eb2c12b0c55ebfc5ef7d2cbafc04d371d86eddbb7d5d68f0e7dffd7ef7dbfbcff5359e9fd26fd929eb618497636ad614e5faa8b1f4d63a579b5df1e567fffe7e42bfeab7b8a7fc57f2a477a6e14ff8a7fc5bfe2bfc1567e5ff10fa2fd8a7fc5bfe21fb2f715ffd06883e25ff19f9ba7f81f9ca7f81fb483df53fc2bfe5339d273f30388ffcefdf7dffff674626a782e499b1230e7480e30e9792e51da921b20bfa9a57e4972723ea5b635e96cf2454ad854be94d0dcbeb6f8e63683269bd223b54392977b9ec691df4fed48c70c9383d2a693decbe96ef25b5ad7363fa478f1d848cf737924ad6dcece548734a6ed259ece53fc2bfe7376fea0e37f7dbd07fff6dffc07f8fce7bfe40956cc4e6aeb45b6bbeb02abbe049da1e2939b93ee117c5ddd39565a616362f518aa52e5e425f1afd91bee3bd2018e09c4046ae787b259c52bd26b99ff44208bb10cdf7d229c195f21cbc970fa0aa1da0c113120c963aa1a53b0b9bdf5751cd7279f820e1ea7f552dfa6cd1be1177ff11fc0d888ab466363073a0b2c2ec689626411f06b3537bf0cef7fff47e1a1871ec60a4655bc9c8d36d85405d7583726d4443255a52308a42a47ca7244215711e7cb5ffa0a7ce31b0f2331efe597cff8781aa717063ee97a2d2d2ec2860d1be0f5af7f2d92e41617e7b1e2d5b62d5b60646404ce9d3b8f84b2e59565585d5d2de3eae2e9c93e23e51afb766fb6f2b9e9188a80656a1febe967184322cacd6c98864387ae87bbeeb81d366dda0c972f5f82cbf3a5dd6babd00b2d1b7da5a820c75439ebef9848b0c1dc2c8cc747580bbadf717e2f2dc38b2fbc049ff9f4e7e189279e8415d79eb1d43932325a8eed626cb0e5619864c9475605cbdf6a8e0f554ef3f9e4084384cb3ee6c5ae9ddbe0c489e370e8f02124669d3cf922b686040f29a0ea5f5e56824df73cb49d248a1ae67ec0b3a549d116e3abb9d922def6388568a357557f1f51f53cd329f3d5f11f0b5bdf2bf059a7ba36550b538c50204411f66c9fed4dfd7ed5bed05672a3bdcc9f58250f6c243af16a7f6e7edfb26a53e97e487b0754d0211b6a6b06817c857863ef093bf817b5e827d35774645219ea0bf788dce7f63b9271e9d265b4ea9e7bee8c95b1f4fdfffdfdfecff921c58bc7467a9e7e52fdfafb5f7ffff3e746f1aff817ae15ff8a7fae4fb23b97e34d3890ec54fc2bfef9dc26acb4e5677a9eca6ac25753aea763dbfc92e40c9b8b4d3a9b7c91f09dca57fc2bfe251b24ff8de23f3bb6cd2f49ceb0b9d8a4b3c91709dfa97cc5bfe25fb241f2df28feb363dbfc92e40c9b8b4d3a9b7c91f09dca57fc2bfe251b24ff8de23f3bb6cd2f49ceb0b9d8a4b3c9971457d882109280e4168e8f9104f2ebd4d12699a9ee54766e4c5bb0a40d8cdb92063cb527677b2a376743d30626255d4e569b8dd23c29de4df14f0fe925903e4b65a76b95d3d7941fb9c496ec48753701a4edc8bd14b90e1a97ea6c1adf3456cac9dca62aad6dd32691ce97d626971f8a7fc5bfe2bf7eb8b668fff25ffe5b585858740a6385299a53840a2fb5167c36b4c40ae7383e9058dc799f5598a1e7bc6519ce0d320c540485daf3340efec4cb0af382835849c7135e8a8135306c3eb78fe218db06f2f582fa1af643f52b30f5bf2890aeda1a969f9e6515bb9279e82ff9ec67c375d75f037fe767fe2b5f15ab00ac8205b19f1b44928b63a778d28daf90e5e270f1e23cbcebddef83279e78863c0cb6b818f7ab8a419e810248baea7bf295f7cf7f8a20cf6045a56e39a603cb4babf0fcf32fc1cb2f9f42f294ed7bda88948103b829ff2c2d2fc1ccec34acadf560c386594f76713fce3a058c868a57535353b0bab6066363636e2961797915a6a62703a10daa38341dc310b04cfde3f317a04ef0ea97767461fbf6ad70e2961370cb2db7c0c848072e5cbc080bf3f3e5baae03586697a13524c1d4b60e2a0256b8aeed333e60d86e706969191e7de451f8c2171e80679f7b1e656fdeb20daba1e11a1506ace8abcf11ac82d5e8b7c1bcf13a09833610f40cb6ba9c9c1c870357efc3b68493d3d370e6f49932af2e96ebe3db2fa21e6aa7197cf7fef0bc2fc8d1aad213c7525c06d6ded0b267116ffe3e70af2d30395eba3b27f210112fa95e5ceabf65fb453fe0ce048213104e69df49f75afa38192106ae95656c276a4c9d040510f74dda23623541dbfc973c2271f6a89d61a8401884d6aa09f60396c847ac76e5ccecd7ed187897d23e1efc27f229ee5b25c64f9658bfe7b577c1d6ad5b06ec8bcba1efffacec74ccabfdfdcfed4c75368d6f1aabbfff65dda9ecdc18696f90e6a6cfd35ce1b2f8fc9cedd26f30c5bfe25f1adf3456f12feb4e65e7c628fe15ff5cb7e25ff12fcd55fc8338bfe950fc0feaa071a9cea6f14d6315ffb2ee54766e8ce25ff1cf752bfe15ffd25cc53f88f39b0ec5ffa00e1a97ea6c1adf3456f12feb4e65e7c67c3fe07fa005615b8235053b75203d9a122c358e1b9903144fc2547e931ec94669017363d3e4cf2d301f2bc948c12e8dcd2559aa5fb2b529e1d3435ac761622fc52ca75702269f2b25763a3ef5bb4967d3cba36dbd72fee5d65bb2359783b94d33f7f2cce54a2a378dcf956ce6dcb6744c1ad3742d7276a447934d924fb975e0e3a5bc69d323d9a8f857fca77ed1e7e2c54bf0eef7bc1f3efbd92fbad140a69950990ae7832729f9fa3ff5a3082dc0203ce376d13fea43d0c5c7d17da7b0089556884c10c90a28cf0fc3fbae6296bbe7e4055d162a0215b51684a08baa4d51152af225facffc0b46e375c77dd3b50fa21f571435ff6d88133f28d6fd7ea58bfc8c04167edf8d2dff1c3d7a23fce44ffe048c8d3802868d358d2c125f5c7c5cd91f0bd86eced23a58d473eedc1cbcfbddef83975f3c1d63ede503c4525a7e75b1f2122d645a59cabb62432d236a655820a1c3574603e4d8e01a9b103b53540b04553b474f0201585e5981d90dd370d75db7c3f8f818acadadc2f9f3e761d7ce1d70796e0e3a23a3303131eeab0bf56c68f958946327b0c55b515415bda88d23087bb744c01a768fa5f2629ea8b61ee4038c8e8dc2ae5dbbe1c4891370f4c88d1e2fe72fc2c2c27cc8e51ef80257558cdd3c22de453d496cb04212fad2f16df842c5b585f90578fc8927e0cb5f7a085e7ce1455cfb8d9bb6c2e4e40456c3aa722ba414e69801036d04ac2a9b7ceb48661d62ce62752d6ac578e4c84d70f8d04170bcabe79f7f0ed6d77aa52ed712cf931031b70321d093f82a5158952d8d31b5fd048898c6ca5e155c2152116d856b13485fa6a8dae4a1bc4e1014096483ef78c22d84bd82f69fda9e0e83fb368e0b39866bd6e9604e16893fd4c2d0b238ba6b2250f1bda2b61706fc5b607b379b172678d9d6567b5a8c9dadc9255db4bff836a810ab73f1f74141b91eaa7f59d63e31da5bde5f595e86d9d9593876ec088c8e8eeafb7f08bdfafb1f06eef339fafb7f30e787c941fdfdaff857fcd7c7366140f10f03f715ff8affd42fc9bfdc7a4bb62afe216b5bd39cdc5a48f2a4bc69d323d9a8f857fca77e49fee5d65bb255f10f59db9ae6e4d6429227e54d9b1ec946c5bfe23ff54bf22fb7de92ad8a7fc8dad63427b716923c296fdaf448362afe15ffa95f927fb9f5966c55fc43d6b6a639b9b590e44979d3a647b2f1bb857fac80950377aabc2d78392724e0e58292cacbe94a03d0b6401298b86d4d36e400272d7eaa9b1f39e0a536f1b157ba2e92df1290b81f395b723e0f73e47ccfcd6f5a237e9e8e695a3fc98ef47e6e63934094c62bcd6be91e5f832bb1311787b67ba9be26ff739bed3079a6f857fc371ddf4ff8fff33fff1cfcce6fff1e9c3b7b0eef51a1251c4f9f102f221d908c5a452c487287556a291f783200b51b64076f7fc52bd9b839803a425e900db4ee959955cd275ad782b7e983788fb704e3adc380c73fb926bbc806de3ed05d7782ae7ed242ac606d15b9dd10ec36a6228cb9d676b7df710bfcb51fff31e862cb3917bbd0eece90777d882d0443a33bd70ad0b5633b7de63cbce7bdef8753a7ced6c938ac5212ea2b1f6ddc380b53d313b0b4b4844f1cc9c9116abc7f50d5c2b2211190f445b134315c554bba484d61710953c3b395d525b8e69a6b60c3ec14cc2fccc3d4d424cccf2fc0cccc3492e31c496bcbe6cd38c9c5cdd934313e0e67ce9e85c98989d802931f03b99f21600d7dc4a181dde4196a18b30da59dd75c770ddc7df75d70db6d27a0d335255e4ec3c2c2e538afe38862983fbeba1391e4b85596021c6253b0b5c5ffba5c58efc1c50b17e11b5f7f143ef7d92fc0238f3e8a24981d3b7694df1304c840122a509f81163f0d3507b450abfe856b06219bfc03d7da6f7c6cb45caffd70e79db7c3feabf6c3b9f3e7e095932f9758adbf7f5dbb4294cbf2bb53300c05ccd0185a93887320d2a5d76dadc71092b302c9c9b5d2c39021be8a98a32ca2f53d13a0762fdd570c4085595b55f74bf7e33e1201c39a323f20d842e3a9dd28f91ff78a04f3c0ec42f9d4ca95b54f85648f3190ece1e579b7dba9bfafc238da67797ce99e0d7b2cc5872a71f5d9de6b581cdce7fcf90b70e4c88db07bf74ed0f77ffdf87e7afff378f271fafb5fd62dcda3ebdcfc36fbd2675c5f2eafa471fc19b7adc986262c48e75c373f14ff8aff616dccc5a1ed9ee2bf791c7fc66d6bb241f1aff857fc2bfe15ff8a7f3e4ef12feb96e6d1756e7e9b7de933c5bfe29fe65d891de97dc5bfe2bfcd06c5bfe25ff1aff8ffcbc07fe7fefbef7f7b9301d2759bd192b3d202a5014ce74a899f064692258d315708ec610020053cd52701b049b6b4e8d206da94c0399f731b751ac7548604d4dc3ae77c688a49d366955b9fa698a4b6b5c5b12d67a47ce73a722f38e9792ecf9bec968ea6f54f6536e160983c4aaf15ff8affdc3ae77c688ac9ab19ffaef5d93bfecf5f856f3cfc4d585b5d0dcf4cacd013c7935c270faa7fc077ff68ef880a2669adc5f5e17df7319eace49ef679ce733de0090236d1edce3b8cb8456402ba177587f9be8d5f11c94f1dd61ad11dd8de2b8ca7f65b35fda427f885adbd820c4b72422c6c126bf49bfc87e42f029c5c46f3d1bf3edc71c7adf057ffea0f95b6ba47168214818065c21fc03687ae22d6e9d3e7e0f77fffc370f6ccc53096145a6c2f670301cbfdb9f5b6e3f0533ff537e099679e81b9b9cb580da7dfa7f5750615580dc986eba0325628a21c29c2335a674f1a29d0177f1f45630c171616e072a9ebb6db6f85ad9b3762e5a6e9e949181919854eb7832d08171717616c6c148959abab6b30313901e7ce9d83de7a79ee4858618d2c64f6f6bf2801ab3e3390a54cac1de6ee4c9576ecdbb707dbb31dbfe518fa76f9d225585e5e86f532a7fad89ed113b0b8358e50e473b65a77679b8fa11fe9e7d850f1cb1374e62ecfc3638f3d0e7ffa279f82279e7c1a26a7a7cab84dc3f898af0e56ad6b9b3b0552bceae108449d7e0762e33eca35e3d7706a72128e1eb909eebceb8e727e0f4ebd72a65c9f4580602bfe8f889621590c11b0acad132443de7b3c14a18256c04240925f571c5c9190e29e1956a24f6d4bfd9c7efa9784e45d094c860dd7cedb7eb03146830527e29eed736e5edf32a21912a1208ec17d8a934ea1bef7939d03ef556b3d8e59ac72ef8eb83eb4e743b5e7910dfd50258ebc416cb26a8671ef0e7ba423b841fa1e2b3f8b0b1e8f6e5f1a19e982beffbfffdeff3c26d2bc9c5cc9af745eaa2767b77434ad7f2a537fffe73197cbafdc796a43ceef5caeb7c9368a7fc5bf51fca7f3521d8a7fc57f2e268a7fc57f9bdfe9b964b3749eda90f33b97eb6db28de25ff16f14ffe9bc5487e25ff19f8b89e25ff1dfe6777a2ed92c9da736e4fccee57a9b6ca3f857fc1bc57f3a2fd5f1fd827fac8025296c0356ce815c60a0c53109943907d34d82ebc82547aab7cd0fc9f6f4796a534e5edbbdf47e6e5c6e11737ab80f5212d3f3d496369ff9216dcea98e2630b6257293ec61f4355d0f9397b9f54e8fd4a726794d2fa5f45cd22be9977437c5bf2d7f8ce25ff19fdc97f47e3fe3df11191e79e49bf0ce77fe27989f9ff755702c234a8571b1c5a03135c25391b4ae325e383eef6788085869096cad8a9409ed07a33e8a1b934336505b2d6a0b4671e295b80cb07517722a127888d093fcb8f1b1e947a2195586e1556f480fc9831037925b24f95724bef5058cdf71e76df0961f7a8bafd063a022e6188a6edf136e18010ba3593e7fe59533f07bbff7513877ee12d493c1428d0653bab27fff2ef8d99ffdaf60cbd62df0c20b2fc0d9f3e77c0c505e41ae94ff59471b821595cf14271777e3affd1a38424abf166f53744b5f5ddc2c5cbc701e366dda547e6660ebd6cd70f6dc79e896eb383a3282d5b85c7c47474790ec36d2ed94f7165195ab963537b70863e3e3558c41784f7c1b0858e82d926d7c6e5013b902d7ce0d58c7efc9e9113870cdd570cf3d77c1f5d75d0fe3131370e1c245589c5ff6a4331f288a3ad18b3c6129c4179fda7e205d192c5285d5de20c4159f1b5c93c5c56578f4d147e1739fff3cbcfcf22b488cd9b471234c4e4ea1dfc636f8440660dc90aa43d181c23acf3ac1c38e1f4b99856bd9c78a58dbb66e823b6ebf0d76efdd03af9c3e55aedd39e8f7fac117af00e73a329fedc57d84b01bf70e74dc8f3785a9b053ad586d3550aed3d3b735e29d65eb65ece05faa2b719ec845ed48296f6ab232f82702a73731b425a5b18527ac39f229ee45b437514b3fc3d69eed4d05db3fa3efc17e2479926d61cfe9075c76c21e49e36bbf11828f486b0b845877c4b6aee40bdbab81c581f654887bb427bf3a9fe6caf7c25d77dd065bb66c1eeadd27ddd3f7bffefed7dffffafbbf6ddd14ff8aff541e1d8a7fc5bf74aef857fc37c982169f53398a7fc53f3f14ff83f648f64be3f833c5bfe2bf6ddd14ff8aff541e1d8a7fc5bf74aef857fc37c982169f53398aff6f2ffe3bf7dd77dfdb25e5b9494d499e4ba2611626d5973ac8e7a532d345918294db28f8c2370120e7677a5ff245d2912e1c3f4fbfa5b9692c2499529ca40493eee57c6c8a4fdbda4b73729b16f7a74db7949bb998a773249f9b7448472e5772e3a4584bcfcd90584bc7b4c9e58794934d360f6b93e25ff12fe9cfc9cae55e93ecdc182986529ce8c8c5ddfde3fa6ffcc66fc3035f7cb02228b8b93cd630181f220fb9ca29b44644262012009117dc1c4e6430606a2424e3058b841a9ccb3ebd4080723a0bee0f8f95104b4e3ec0713c8fb99f6e5ebab66873204c584f52408242b80ff43cf8df0fc42d1ea7149b3127687dc2b3db6ebf05defaa63720e1a58815b02056628a841853040296b3c7c933f0cac933f0fbef7704ac0b41a08d15977c452a2fa353ce1d19e9c09bdefc06387efc18ecdcb5134e9f3e05172e9ec74a53de4e22df71bc850a495425c9a946f297bbb4fe1a5b45faf9319e185f17831eacadadc3a95367e0c081033031390d1b37cc96d7a7b17a54a7cc255705c855575a5d5dc5ea4e8e5ce4485d6843bf8755b11c198bd63428abd61a9283c578d803a31a384ab1b1e2c01e83e1c5f84e96f61cbcfe7ab8bd5cbbebaebf062626c6909cb4b4b480442483c4b59093c6cda9da35e283a21f09899496c674c0b77ff4ade62cc6af8f842747f27af8e147e1cb5f7a105e78e965989a9981d99959181d1b897a22e52e168cf3e4499ff7a6528f3a8ae837f033632b3f0b7f7f7c7c1caebfee3a3878e8fa52df289c3e75aa5caf3957832a2c4539b71809eb92ee9b219b7b8084b20018e285451d4e96cf2d7f58a0ea4d5091ff3c2083dc2a07703ced4d6c2fc308585b6bc157d90511cbb5fdc754d5fa0c935db0b67ef45da4fb32e4f76280ca0fc3aa01726216cac48a6955c53f6a6fea6ca5769cd1571e6763eaed5cd377045458b12497d60baab816c1b6c5c525d8bc7913dc7cf3918a0496c64fdfff03ebdc64031fd3243b37468aa114273af4f77f0316bf459bf4f7bfe25fd29f9395cbbd26d9b931520ca538d1a1f857fc2bfe15ff8a7fc5bf645b7aaef857fca7764ae7b97152aca5e76648aca56314ff8aff1cfe24ac2bfe15fff43cb7e6576a93e25ff12fe9cfc9cae55e93ecdc182986529ce850fc2bfebf5bf8c70a58a9b074e1f83d7e5f0a501b3872f253e772899cca4c033f4cd2497a72f7ae14044de32590b7d9974b86dcfc3491b8bc54462ec6a90d929eb6f84ad752924be0184696f48cdf977241b227bd9fe653939edc2622c96c8a535bbe4bfaa4f1c3bce8d2734996e25ff1aff837f0e8a38fc3affeea7ff4d5af9ccc300edb73594626b29ee410fd731f56e12aae65186b836ebc0f29fe834d610e91abb8bdf4cd090454810b65b14a36f4219215112db0551855a381644f209b8baaf5961bdf21dfc87ef29dc7d5da5a1cc84faa5e53f3358c8f3686f1b1fa97a9da85b9a0df72cb3178eb0fbd11c948c6d354002b2045bb9032e10483afcd64b1ba94bb7deacc7978dffb3e0a172f5c06dfa1cc06f5a51ccf860a6b5c40afbf066f7ed3ebe1f0e1c370fdf5d7c0b19b0fc3c5cb979188b5baba146b3eb9b145d10d6bd1f16d21037906095926543a72441abcedc8291d1f97103aeb193771e9e72ecfc1e62d9b60fbf6ed70f5810370e1fc051cb3b8bc0c5bb76cc141ebbd3e4c4c4e96be5c804d9b37e33d17dfd1b1312467b963c4b5df8b7917293550238a091f343b83f96a9d631ad4d73e7c302eb670ffc55b45b703b3335370f0e0b570fb6dc7e1c081fdb0b2b20ca74e9f86d5b565885cab8274929d165b3d02118f30b62e8f0b1f7fd73610896d80a4bc7e2039adaff7e1f49973f08d6f3c020f3df855b878e912ecd8be1366376c80a2d3457dfd3e5587f315acbc3f01fbd678021fb19ae8c3f2bd360688f067d087bdfbf6c0adb71c872d5b36c2934f3d0de7ca752a970c3ac568a89ee4e51a6a331882e9ec47c21e740231cdc6587b3266b033d84af7a81a9809d5c102ac42b530bf6091e419728fef673e7f4d6d1d73ef35dabfd05e46f8340cb3f1199fefa68631fde002ed0d31a9482e787fbbae85219361d9de42244f5a8f5809cbf969327f9977fba2ad13d0708fc12a5d1e17bcad237f4ff016acd196d286d5f5f5128717e1f5afbf07366edc00b9f7a2befff5f7bff42cd5951b93ea93c6ebef7ffdfdaff857fc37e54b7a2ec952fc2bfe15ff8aff5477ceee9c2cc5ffa0dd8a7fc53f7f96eaca8d49f529fe15ff8a7fc5bf644f53bea4e7922cc5bfe25ff1aff84f75e7eccec952fc0fda9de23a12b0d2c5958032cce2379da706e5ae9be4e736b0b67b6d0192c6371d528cd2e7d2b5942492ccd4d79c8f3c3ea9cc74d1259b9b36bc341e39bb25ff733920c5bf093c6deb21adafb489a6078f470e746d36e49ea532e99e64737a2f17637eaf6983cce96cca1da3f817c7371d528cd2e7d275d31a2bfe5f3df877d588feddbffb75f8c2e71ef0ffb86fc23ffa33d212ce71e31951895791317e909749e7612c27190561d11f7e6f3db4cb7247ac5213e444d2049b1f84d43823782b5488a11689ae3da123157492b5b52cee4e0e5e538c594b426ae515db0f2639168963410e8473cb6283b242fb2f77ed2a78392291bbee91bdc13e47a43a72e406f8d11ffb11e8042954ff262560f10a58a8ba3070e6cc79f8c007fe00ce9dbd009ed8126cc359dee64e01c1370b571fd80f1b376d84bd7b76c155fb77c35bdef43a387cc361b87c790eab2cadac2c79f9b0ee825b55ce317c9ff6ebe2495a2684c160b52a0a4281e41f228df9e3e9679e8191d12e6cdbb615262626cab55a85b191d158fd6af79ebd68c7d6ad5b60797911d6d77bb0b2ba8e7e9e3d7ba68ce188af8ce46289d580bc5caa22d47498f6adaffd7031b0ec3ca6b98599d969b8f9e61be16d3ffa36b8e9c61b9124e46c5e5e5a8c39d4b744de617b04929202792dfa43f9e672c7073492b21c31aa6fe0e4c953f0994f7f163ef1893f81975e3a09bb77ed82d9d90d303232e2c723cfcbb0a4046fbce12be20f0b76d0d184a1e5886245c7c2d4e4141c3f7e1c5eff867b71cd9e79e669585d5b054ff8aa085b040f97f7fd7eb88b24381bb14324444b78048818233c017d873da2e07b7bf88e7b0e2985648fa471acf2d4c098b09751153fba46dbc21ee90ef4c75695f8fa417f4c0bda270a6a6509becc17ed5d86d624ec2545554d3092a14a5dd8ce92b74ea47d86bf4b18d6dd758f1167e3be05951fbc25217dd7fe0f06146be3ab809d3f7f117176d75db7233934eae59962f4fd9fc641b2538ad577e3fd9f8b412a437fff0ffaa4bfff15ff8a7fc57f7a2ef992b335275ff1aff8cfe991e428fe076d55fc2bfe15ff8a7fe919b79fdbd5861dc5bfe23f774ff1aff8cff992b335275ff1aff8cfe991e428fe076d55fcbf7af0dfb9fffefbdf9e0eca399b7e4b0a72011a06f0c3249fb4294841cde9690b705b72a509ccf534257cdb2694eae1729ac0cce735ad4d6e236db259ba97cb83a67c18c68edce620c9e63ee76ccfadb1f42c97134df9d8b499b5cd6fcbefdc3ae6d62195d9e47bd33dc5bfe25ff1ef0fd7c6ecdffedbff0073f30b7e4e22279259dc5c92e315f87b248ff94a4401f79c884e2483e617897db1b5a0adaab03859be928ecdfbe5cec33556af0a84092236914f445870cf39e182130c90804604085bd150a23f490e20392be8a1a34344062237047f0d2356f03813610ca2bf3d3874f87af8f1fffc477d2c80c53dfa3a48c072e5ad5cbbbaf3e72fc3873efc316c454861abf2995cf0badc9f2d5b36c3eb5fff5ab0bd35989c1887e9a929b8faaa7d48b2d8b37727cc2f2c96b93107ebeb2b48188a5588bc144a06fcf8b8843c201a8f4f04a8c26722e9c8b5d2bb78f102ecd9bd1b76edda09b3b3d318f7a2e3bd5a5bebc1c2c27c69d3245cba7419264afb2e5dbe8cb35d1b425789677171113addaeafee531ad7ebf5075aa44947c3d6786507ad89a18898d026cfc76364a40b07aebd1a5e73e7ed70f0e041f4ddb55a5c585c28d77a1d285ec6524bbb0e503c23dfc9338e00a01ff165c3039faa9eaee4be2e5dbc0c0fbb8a585ffd2a5610dbb47123ccccccf83676947766d0054fc60a1fdb161cca3f1b2a2a59d8bc793312b1366eda04e7ce9e810be7cf956b82480708d5b37056df4ff75df5fae05b67fa2a5784c388595bb5fb8ca170d7015bde7613f15a84ea73b46fb87985b44786f58aa18db793bd9ef60477ede4d13e0015a638d92fee17b437e3cd607fd87f2c8058918be6c796aa8e34caf70c467e257fd3f717f98663887c25bce3b8fde96f1122be928e18af709c3f771e8e9f380a3b77ee188899beffdbed78b5bdffe97e9bfe9cdf4df39b7c6c5ac7dc3aa4329b7c6fbac7d7455a6769dddbe64af773faf5f7bfe2bf2d47539f73b61bc57f5647ee9ee25ff1aff8afdf6fd39ff3bb69bee25fd6aff857fcb7e568ea73ce76a3f8cfeac8dd53fc2bfe15fff5fb6dfa737e37cd57fccbfa15ff8affb61c4d7dced96e14ff591db97b8affef3cfeb10256d3e47442ea4c6eae64443a569a2b8d498f6113da081b4c9bec367d526c72f119f63c772f976469dc73729b00da065209301208720928cd49efb7e58504e0f43c1dcf8f26b049314de54be0e1faa497806453cef6dccb2895dfb6d9b7d921f99c1e8affe1f429febffff1ef8820ef79f7fbe1339ffe02568a8a7e3a1d7eb2af62634c2442618b3dbacf7538f9ee9ceb61d555a88a4c4a5220bbe23d7fc37f82cc58618ac5808e582526f845a4284e662898ad4eae23e97039062a3215d96c58751c13ece66d1289ec45f2818d4dd78b1f54e5a6476dcd2886c136d78aee9a6baf869ff8cfff1a74fc22003533230296a1fba6138950369071e6e696e0a31ffb04bcf8e2cb5e9e65f10b5f5528dd7a02fcf51fff31247eb9aa45d33353303a360a5bb66c82e3c78fc289634761c72e4fb6585a5a8495d595d0ee9093d8821596d69f62610319c9067b7db40d11b60a8304aae79f7f16ae3e7000a6a64adda31d181919c7b94bcb8b30333d8544ac6eb70b0b0b8b30363a8276eedcb51356cbfb2e87d757d7606d7d0d8958a32323df5205ac2bdf2d25a15eaeed5bdf72cf7abfbbdd02a66767e0a69b0ec19d77de0137dc78187aa5bd737373b0b0381ff616530909341d63a8c253dfd3d90ce9e8f878324a2412ac42cbc2d5957578f1f997e0cb0f3e084f3dfd0ce6f2b66ddbb19d63414065d5ac783030d72c7b261c58480bab77592ce6e4f2c1ed0b1b36ccc0d1a337c1d5575f05cf3cf32cbcf2cae932afd2757755a39c13166590feb8afb96f5b1195085f96ceddf350158fdaf1a108da5fd8bd4e18e355247fa180faef1fbec7f0df33c0f473d21784f9b457c4796e6c421a2542a00d1f1a1b6d75e7818859dbcba9bd2b6b0b18f7e670904d368ca573b797393c4088636daf65fba5f85e62f7f83eefe62c2e2ec1a64d1be1d65b8f212681f9d3769ebba7ef7ffdfdcfbf87c90fc90ec9e7f448e7ebef7f59be149b5c7c863dcfdd53fc2bfe250ca5fa14ff8a7f4986e25ff19ff3393d14ffc3e953fc2bfea539744fb249f1aff86f3acfdd53fc2bfe250ca5fa14ff8a7f4986e25ff19ff3393d14ffc3e9fb76e33f12b0da14e79e4b804f8d6a4a6e7eaf4d676e619b12999fb7f99326686ea3cb2db8742f079e9ceed44749f6300998decfc992d643b23da7537adeb459e66291b32fd529ad09bf9f3e936c92f4e4ec6d7bc134cd95c6e472a22dc639f94d7e49cf86f1e74a9e2bfea1f19ee2ff7b0fffcf3ff722fcea3bff239c3d7bcedf6bb333e8475283f5442d6c63654cac30e5c653351bc3c707f958d5898d71df44b6a0b9d12ff761649ae83ff94063c86f6663278915f7cf9163481f6f578604894084e0c4ae388fc586b70a233b3acc566c8b674c247ae0bd14c7a66a39d661f9b27dfb16f89b7ff3a77d5b42ebab0451fc21aeb9b7a8080d0a9d95cef4a5e535f8e427ff1c9e7cf2597cee6303418689f132e03ba1adaeaec0a143d7c3aeddbb4afb0d8c8d8dc2c84807ab498d74bba52d5be1c89123f09ad7dc01478fde0853d333b0de5b8795d525fcc6aa4c8e0c84057a5cab46eb493ea1d451877c34d42e2dd2b1705cafdf83e59515ac9a74f5d55723c1cae9989c9cc21686ae85dea5cb976079790556cb71b3333330bb6123cabc70fe42a9660d89454eb423354d4e4ed4161be3cdc829553ed773e35b3e7840a1d2193f78a357ae521ff36366c30c5c7bed3570db6db7c10d37de8004b273e7ce953158f6ed149168e5dadcf523c6307790d00551910d242947bc72b144a213b89c75559b0caef4e2c2023cfdf4d3f0c0035f82175f7ca95ccbedb075cbd6725d47025efc788b642f8bc4b17a96e47d76e6381e98f72f36c984d132770e5cbd0f0e1eba0e56d77bf0cc33cf20692f566b7331414253b9b6dd8eaf1417305184aa4fbd408424e2538de0e43eb6de7acfe7135b0a8a3bf722c4717d7dddc793f62e46b8ec51d5ac64cf311cc7219fdcd167b876472fb450ec04321830acf7894cc56402dfb721f34e086363a5bd644f737b99f5bd282b5fca7347fa72295230bb010652b5e61fee1561cf0316f7b82786b683f3f30b70f7dd7722112bae81beffbfe7deff52dca4396d73a531b99c688b714ebefefed7dfff8a7fc57f3a27f74cf10f8df215ffb2ed8a7fc5bff45cf10f8df714ff8affd426c5bfe23fa75bf1aff897e6e4e42bfe15ff8a7fc57f3a27f74cf10f8df2bfd3f84702562e404dc6351d6dc648b272b2a5203425ddb0b6b7f97c257148fd6ab239972ce986c2cf87492ac99ff4bb2d717280cd2565d3469bfa22cde7f34cc3c69cc624d5919ee7e29dcb3d69534aefb5f99bea480fe9feb01b42532e4a7e35ada3146bc5bfe25ff2ed0711ff8e88f0fe0f7c04fef0e37f026beb55f52b4722884422c19e7e686d852db28c09ddd182ed81b444556aa86a565cdf407080c43eaa0a45f6f14a3386d9ee3eb52a34c98f132211e0789ac3aea31e567d0a095fb62228001b6f829e22b4ff2a9258f4a8151ab3cd8618a53ef179d49e90681e443ac3a7c65ffffccffd0c560a726dea1cf906fd19c81ddfaece40554dc85523fafc171e84af7eed613781c25dd59d32ec53f8567e070f5e8b04ac4d1b37c1cad212ac94f7c64647a13be25aba59189b9880eddbb7c10d870fc31bdf782fbcfef5f7c089e3c761ebb62d286369711e7abd75d48d15944c55610cab6515ee7f61cd3df5cb9346c0dbd72985bcfcf24998dd300b1b6637c135d75c0d4b4b2b30313e8e1577666767900ce68856b333d3f0ec73cfc1dada3aca5f5d5dc55c5e5f5b45a78aa2037373f3e5bd1e8c8f8da24e4f7e7195887cbb3c1fc3c177d7b7ed88e2bd7f3e0b436bbdd2d76e69c7a6cd1be1f0e183f0863296476eba09969717e1dcd9b3e5f71278b29c8d723a4517304559f5b04a11df178aa093884180eb72f1c265f85a990f5f78e04b30bfb054aef56e9899992dd3a3001becb454be8b55aa6af2af6a8d19c6ba6a6778eaab95edddbb076ebbe55697ecf0e4934fc1eaf27289178beb83ad07c1e304dd0c58f63e048c045505db23dc784eee74de7a99454598647311938108d9b755b5aa22a95c17f7a2f09c0edadb684c5cdab08f51bb444bf70006aa53f1f71addb3d1f7ca0630a646ce4afd316c3fa1ea56b57d3ef8482d04fd3eecf7e03edba7e2fe91c87657349f485d03ef1cebe3e00858070eecc7fc4ddf59fafe1f9ccfe7bd5adeffe9da703ff4f77fcbfec7f449f7f4f7bfe25ff10f03737287e25ff12fc94b65e7fc51fc2bfed3e78affc17b8a7fd937c57fdd0fc5bfe25ff1aff8e776e574a787e25ff12fad536ab3e21f06c6a7314975a4e78a7fc57f9bedaf16fc23012be74c9abc9201b960a49b4b5bd072470abe36fde9914b522950a9dedc4696ca96ec94f4a5f2d3f31c889a6238cce62ce94f75a46386d9b425fd4d3195f498864d91fb9d8e9540d7066c499ee40f8d93f470db52f9a91e29279ae6916cc9f6a6eb363fdad6a82d0772be4bd7928e5cee4ae7e9a1f857fc7f27f1fff4d3cfc0affebb5f87975f7e65c00efc3062117d706e684985f29d3ceb490e2423da10ae910041edfd4c456602b237cc21b296445ae0b6130981db4b95683a8cb4c0c73b5d06f2b9cbafc9771edd4087896490b846fe24c685fc211204da584da8d9c5e546bfdcb5b1b0b6b602bff88f7e1e464647906452153f22fb0d59e4ab18b92a548ef4846a47e0e1471e874f7df2330090b4e20b76c54f206e3932d3eb5ef71af4616e611126c6c760bcfc38fd8e61e563e8a7b80a593b77ee80a3476e807bdf700ffc951f7a33dc72db09b8e1f041d8bc79134c4c8efb1a573ee0beaa92f52d12c9766b0be874ba5e3f806f7f593a79f2a55790b0e5ec191bedba9a5e5875e799675e806ddbb6c0d2f232bc72ea345cb57f1fccce6ec0790b8b0b58c56bcbd62d70e6cc69e8763bb0b0b080ee4e4c4e606b4547faf1c41f5f29cac762f02f0fdfca41524c72d360352b970e26593797af3d4f432bff333d3d0d876eb81eeebaeb4eb8e9c88d1891f9b9cb5819cbf3940c42042365b916caa78a58833a0b5b2d318ef26be7fc3e73fa2c7cf9cb0fc297bef420dabd63e74e989a9c02d30904435719ce7d5a426342f532036c7ff4149e2acfcbf399d949b8e5c451d8bb7b0fbcf8d2cb70e1c225e8adf743252c9f2358d9cbed0b9d4e2401f13d367d3fa415ebe83cda96bc570c93d361e4abb887595b6b0b18ab6e41d5c234950d4466a2d684e1beab3a8595a720792f909eb0bf7122676ddfe0ad06a1dabb6afed17c4c0a5bdb57785b57e0ef12926513d219b3dd867dcbef8185f87ea15838dc395cdd7adb71d8b06156dfff99e3d5fcfed7dfff833990f35dba9674e472573a4f8f3466fafb5ff12fe1322727372ef543922fe9e1b6a5f2533d524e34cd23d992ed4dd78a7fc5bf24af2dc68a7fc57f2a57f10f03e315ff83b1c9e957fc2bfe531db9dc95ced343f1aff857fc375f2bfe15ff92bcb6182bfe15ffa95cc53f0c8cffcbc47fb605614e799341e922e5002319c4e7355d4b016a9b3f4cf0d2f9c3065f4a0e69a173c93e6c5272d9c3cc4fe7f1eb74ee956e9aa9feb6046e9393de97d66118c048b11d1674397bb94d92ada98c548f243fb5235d935caea4b6a4cf9b724492655a362ec5bfe2bf49b6945bfcd9f71afe5dc5a03ffee34fc2873ff471ac808463ac8d155fdcdc1e55ba32f51f59913cc0649ba40a14f9036e0e8d85c197773fb199aa46f559cb381e97da1ce72391ba82bf51678247e3154499540d07ed3555b519c3ede5310eb262051a77cdc78479e9fd58ad2769e5e5481ab1a518e934a1255ac72001ebeffcddbf0d1b376e800275db8480e58e50090c2936aeca54a8b364baf0d4d3cfc3c73ef647e51a02126390d825e6025a05ae75e075d75f0bbb77ef82d1d151d8b4697379bb87d5a5c6c726911ce3db0c52f5b13e56be1aed7660cb968d70f0e0f570dbedb7c0bdf7be16de70ef3d70e2c4713874f810ecdcb5032b5639325707497abe4d22ae0f5856d9c7e0b395955578f9c59760716909366fd90c6ba5fed1d12e6cd9bc110949d85eafd783175f7a095b13ba0a59939393b071d326989a9a81898971585e5a869959470ae923e9c855949a9c98004f42ea63152c5f51acfedefad60fdea292c7d750d021fd13b87361741f5bf94dcf4ec3e11b0ec11d77de0157eddb036365cc5c6b50f41b094f1df15deb5b0842c8659f2bc0a97d6843112a9079ec3ff3ecd3f0e0835f85f3e7cec3b66ddb617319bfd1d131ac26853287080d4210bc6c63c3b5213264c041b9062e9f0e5e7f1ddc78d35178fef967e0e4c953b0deebe3fab80a6fb8f261af7188706bd6e9246d47831b0355a1006a6d3e6bfb34ed178439b62fb91c8220cbd9db6115b1f8dec92b4a154c1eed27c0d6a308c42bdc5778cb4b5a9b40ce0266ab1b477e48ef299a4ff7f81e5fdb07ddb9b5b1eae0c0fb2e10cfa8129629eaef681a9712be624c830c1efb4b172fc38d371e82ebcb7d23ae93beff1be5a4f7f5f7bf9c2ba92de973fdfdafbfffd343f1aff8378a7fc53fbb4ee72afe15ff92bf8a7fc57f936c29b7f833c5bf2c5ff1aff8a773c5ffe07dc5bfe23f772df9db363f3736677793ffe97ca3f857fc27f7d3754ae52bfe15ff745e6b41d826400a666ea3c83927254f2e507c6eea6c2e70b944e7cf52db72f29ac09a0b7eee7ece07c9bfa66bfe916227d993b33b959d9b7325e0cd6d0adcd6dc91c62d8d41ce6f2947db36eb5caef3b834f9ddb43135e9a5b9fc5e1a63494ffa8ceb917437611032be2afe15ff4db9969321e99064bedaf17fe9d21cfcda3bff233cf9d433402dc0f039119168befb30721208d8c06f6b07ec7455523861090900b6aaa4e56415a95f8100106d166245edfbd29844fb2be7d12e8a612e2e8613c6c2b73be3ad0521c90df79cc813548dc626eb8936a66b1b62ca2be07442151db2d93de9f5d7e12d6f79035c7f9d2337f8f679487489ab525962984ffeba808b172fc0fbdfff11705df948668c232e78e1a940857fe6884fb3b3d3489adab279135cbe741e565756b03adaee3d3bc1ba0a5596ec0e040f24eb40a866e42b63cdce4cc2ae9d3be0e0a18370ebadb7c03d77bf06def8c6d7c19bde742fdc79e7ed70ecf851ac9c75d3d14370edb5fb61ffbeddb069f32c4c4d8d63e5acf18971e8768b323f2fc1fa7a1fb66ddb067b4bfdb3b31b61e3a68d18dbd99919981c1f2f63d443b2d5fcc2028c8e8cc00b2fbc108831802dfe9c8d8e60e4aa79b9bc5b5f5fc53685be259d1b1772acdfafadd1951fa6225655b7e27a0ac3436edbb866940fdd8e810d1bdc3a1c86db6ebf15aebe6a1f8c8c74e0ecd953b0b4b214883d3e1fb17a94a913954cbcc60cc50a6258812b90af5c4d2eca4fd7daf191471e83471ffd2692bc76efda59aec134748b11acd015db8b16412756b7f219ecab6e795f2ca3587a0c900d1541d0553bdbb37737e6d77299574f3cf104ae2fd822cc02acc2e54c7363dd1dd73a11e5758cdfa302710d475b8a99a9b00af53d94621adbea998a8c457b1945aa56e98aed15b1e25dd803a26cde7ad03db3b6c2185f73da9f628420b63d256217a4bf35d83d2e0792772324fb7ffa9d92c92cdfcb80ed47c1a874ff3490bc9f299e8180e688bbdd912ebce98df7faaa5ffafe87ef95f77f1a9726bff5f73f0cc801d0dfffb9fb8a7fc57fba9e742f8db1a4277d668ce2df28fe15ff8a7f517713060114ffc3c853fc2bfe25dbf873c5bfe23f6777533ea6fe37f9abf857fca7cf8c51fc1bc5bfe25ff12fea6ec22080e27f1879df09fc9b175f7cd182703425f030a04aefb75d4b72db82d20468299972cf9a6ccbd971a5479bbfb931c3ca6a1a338c9fb9f94deb4b47532c73b634c992f4e7d6fd4af22c9525c91bc6a66fe5b812dd349ec6f0f992bc61309aeae4b273f6e6c628fe15ffdf8ff8ffdc67bf08ffe81ffdbf616e6ede3f2b0cf47bfd3896fe411d65798178ce2b3cb94f3f9cbb8a32eebba0f14e57a8c482cfad271c0c546771c4894064723a3869c011b89c9c824801e5796f7dbd5605c791340c23a150fb3037b713085f31d6449822c253d0e76cc22a38d402cd8dc34a549da0a387c404276f7dbdb4c97a021845ab568187621d0821fdd25ed7a6918fe187096371aefbe0790fd67aabf0cbffe4edf0fffa7ffe4370d5a65cb520a44dd8c11f57b5f50da41ed7eeedee7bde06af9cbc00d8a6d0b95fcaf5a40f6787fb8f01cf77c3ba4370d581bdf0bffff3ffb5f4d3c2ecf474b9a66b30353d05dbb76fc5aa448ea0532049c6d9e2ab5939728cb181d4e364db10151b9c0ba9e32ed77b1d582d65baf8ba38cecf2fc2caca0a2c2e2dc3d2d222123a96e6e7e0d2a58b585dc755cd9a999d2e6d9944f9fbf65f0da74fbf02539313b061c30c9c3d7316e61716717da64b3ba7a7a6606e7ebeb4bf0b5bb66e82916e17d7d4b5331c19e9a2efd333d3303a365a7ec67cbbbd32775c4ef93cebc077ed70e10bad116d2058b9abb53226274f9d85df7defefc3effede07e01bdf781c6366c093950c74f1dbb55decf52c2ec17adf61c205bde7d70a31e8da3d7670cd5d7e81e9079ffd1c57c5ec2d6f7913fcedbffdd370cf3d77c3e888230faefbaa6afdf510abbe6b0859a68a7bd6b0279be0109e7a82952ddc5c8bed079f7df624fc4fffe49fc11ffde19f96ebbce8ab769563bb231ddf1ed25a5fb5adefd6c5c7c2ebebc460b9b696b867e17ee2771d4f36b3652cba48187499e8d6dc55f22392a37bde6124a388226362252e5e958faa63a5ef484a6f778dfb5c5161a0831833715f217217ed096853af3778dfedb9812016658567bdb02739795db74f21d67c954277440294b383f960d89ecb49664518e7ae8b325e7d6c29d8c7d871fd5831cbd4ffc2b85e8e7536b84a6a5bb76c8677bdfbd7e1aaabf755a9acefffacce57cbfb7f589bbe95e34a74d3781ac3e74bf2f4f73f64f537e91906176deb30eca1f857fc2bfe15ff4df714ff8a7f3e9ec6f0f9923cc53f64f52bfe15ff8a7fc57fd333c57f7ebee25ff1df265bf1df7e28fe15ff8a7fc57f7a2f56c0a2433248527e25cff991de6f4a08e99a3e6dc1cfc9c9cd93fc49afaf7483e47ee500dee4739bbe2670f23157b226a95dd28697ca96ec4ce349639a40944bfad41ec9ef5c0e37c990e2c2e7e5e6a787045249672abbe92599da96c3d895e02cf57dd897424ef7b0cfdbec52fcc315d9da6697e2ffcaf1bfb4b404bffaceff045ffdcad76b642a1c632a921369416250e2afd369e9bead5a0f620bac50f98acef1999be38801d8028eb55223ff13bb2dd303a62212b8c3fde33f11ab2c0cfa4fb6f4487768bf65791c9d2cd66e0bf5d0b3f031c2ba3b428bc5ca5145249b89fb0355da09640f22914599ac8217b5052b2261cca2699b36cdc28ffed88f60552413281f06f2b9dd67589c9a9c844ffcf1a7e0b9e75e026c5167a9e20e554032187a4faef0cbb0b8b084449bed3bb6c3ce9d3bb0a2d5b6ad5be09593a761a43b82cf3ac1ce186fcb168f1f86e78bffb8d68a9d6e17c95063632330353d011b37ccc0f6ed5b60f7ee1db07bcf6eac8a75e30d07e1d8b1a370d38d87e1e0c1eb60efde3db0b77c36333b0353538e10b6196666a6910cb673e7f6d2d72998999e46bfb66ddd8a762f2d2ea12e97038e9ce4dae0397216e66ddf62252c1717b4abe862cbbbfe907bde5fd611dbfeb9b01a6ac5d7c1ca6477dc713bfc95bffac370cd350760797901ce9d3b076b6b6b814403e1d3af26bb4f582f4f50ea00c40c04cc83b87ee0da34aec0a38f3c027ffcc77f8ad5b0f6ecdd5bc67806e7773a81c0573316ea15bf06bdc10fe6090ef60d275deeb84a66f7dcf35ad851e6d957bef2102c2e2e626eadaf3922d348e9b79b17087e06bf4a3f0d10e2bd728f4577e9da18a246f715c84614ca82919ba2f7c61342fb14b880751b85c000e9881fb4efc5e50ae388c019f7c5209700d60d042bb209f38db5068c7343e53dae2fb65a75e38d89a450225c0567c39a56d5bfc8be82ed8d7d661f10fe692c11c9c298b84751ac288685f779a9cc1bd762f4f8f1a3de7fa3ef7ffdfdafbfff73760d83859cedd2a1bfff15ff8a7fc5bf2447f19f9723d9a5f857fce7740ffbbccd2ec53f5c91ad6d7629fe15ff8a7f598ee23f2f47b24bf1aff8cfe91ef6799b5d8a7fb8225bdbec52fc2bfe15ffb29c571bfe3bf7dd77dfdb7309994b767a262550ee908c684aa4b620488b20816798604b322450b52d3c1f97f343f2bdc9c79c6e692db83f39df531b24b9393b52f97c4c934cc9de61afa5bcccc5a4cd8674ae74dd16b39cfe5c8c9a64f318f279d273292787b5b5294e296652f946f13f70df28fe1bf3428a5353cc9baea5bcccc5a4cd86742ebffee6371f875fff0fbf05972e5df6cfcaff614519270b60e01fd20d6bc557d315c6d7ecb581d01008476eac5b2dde6ad0148cec12e6f558b52c9c43a482a083cf7507cd27d292093ab0e24cb01965184f3e704430675b87da023abdd6579029821ca7976475585b42632a020212263a3e1e7d66330bb0d749f3830d180b487eb052dcd81a55956b2c6cd8b8017eec477f042626c6b1da0fcdc91e85fb9f8b872779bcf4d249f8b33fff3c72cdaa6a54ce8f600dda59c6bbdfc370387f2e5f9e835b6f3dee4937e5284764eaf5fab071d306ac7ce372c3554a1b191d415de86eb4ccc63573041fa0f565b95278ca1cae910b117e8cc54a4c8e4783cfdd1a6205275fb9a85408dd529f6b2b3835390e6363e368df868d1b617272026637cc62a5ae4d9b37c1e4c40456c31a1b1b43d2951b87be149e2e87b91dd6da55e12242605a59ed3b7ef864a8c58a62eb6e39f3a766a6e0f08d87e0ce3bef80cd9b37c3fcdc3c5cbc74b15c175ff1ca53938afa9e85443b22e61076592b4b1cebd7d196b9303fbf000f7ffd61f8e6634f9471dd001b376c82f1f132ff0c11b8ea26b76464d05b2e2156b24204e09489729d5c9bca0db3b3f0cc33cfc0c5f3174a2c9aba4caafa660a4193cf378f337a2784fd09c27e13f413a66bef4c6f49ac4c47844e89b854243ee13e016cbf0e18a6f945d83fa902961bdf65e428e07b1961dfdaaa8d62b0cd7823aa3d23d8036caf8dfb70a82448d7e9ef15924d36f6d9be140fd69e91b7a2ed075d6403d9487beed2e222dc50e6e5eedd3b413af4fdffea7aff4b3a9a0efdfdafbfffa5437fff2bfe15ffb20d39ff9b9ea5d78a7fc57f2e266d36a473a5ebb698e5f42bfe15ff7c5cce0fc9f7261f73baa5b5e0fee47c4f6d90e4e6ec48e5f3314d32257b87bd96f2321793361bd2b9d2755bcc72fa15ff8a7f3e2ee787e47b938f39ddd25a707f72bea73648727376a4f2f998269992bdc35e4b79998b499b0de95ce9ba2d6639fd8a7fc53f1f97f343f2bdc9c79c6e692db83f39df531b24b9393b52f97c4c934cc9de61afa5bcccc5a4cd8674ae74dd16b39c7ec5bfe29f8fcbf991deefdc7ffffd6f9784b70188beb9a1927152909a0c1bd621c9d66192414a7a4957eaa3b4284dba4cc3a6918ec9e9ce6d8c3930e6e2dcb4e1a46b3f8caf6df9d2b4f1a5360d6b5f2a27b759b4257fd37a5d496e4aebcb75709b86d12d9da7b64939306c9cb8ec347e8aff415db9f834cd91f428fea1d1a661ed4be57cbbf1ff9bbff91ef8f49f7fceb7f8a3b936d8eaaefd4d4f1eb2365679c2967cdc06a6832ab85015166a6f6542c51627a3c3da0b9a2083b72474ba8838417a88084555a2b84ef78dad06bbdd28aba0aa5624238919d19f907c41a4048a25e94e0e1f1f46240b041e6b6d4588e0ebcddb2c12d181c90024bb418c0db5068b71c66b5fd9e8877ef8adb07be77688858b207ff82a5f9642098e78f3f18f7f02e6e7e74a994879aa24181b7406da4ef0e9f2e54bb8ceaed2d2e4d434cccc6e28e7fa1688eb6beb303a3a020baeba54c755b21a297de8806f69686b8411794f0a2436834d110381c35b54044e9c09163a19d42aae135bae857c08a2fd5a74421521035dacd2d541b2d6c4f8384c4e8ec354e9c3c4f8048c8f8d21e9677272122b62b9b98e98b5b0b058fad48545f429100f8b58db0cbea387e11f13d398f0e3da0676475c7bc5cd70eb6db7c28913c760c386593877ee2c12b13cc9b0a8e1c95ac26a20f760ba1641b609cf7d4c4db85e5e5a85a79e7c1abef8852fc2cb274fc2de7dfb61f396cd18db50a7c913a386888f898b6a6961830d16c6463a70e4c80db06ddb0e78e49147e1e2858bc1f922e211420bc3c1fd2e8cc136843dbf7f15a65629aa1faa44f118f2fcc26b46ba8b7b92ad4892e93e5ef954ed3105b0bd39c8e384cd81bfe0844f8791a2f89a41b22ff2bd845a1a125e4d9843d8e3d5b3a84256ea4b9f57d3e2e3839ed82e956c63b1c1e74551dba35d1b5b4782bcfbee3b6abaf4fdffea7cffe77cce1da9fe5407b76918ddd2796a9b9403c3c689cbd6dffffafb5ff15f3f723ee70ea3f857fc83e2bf693dd243f1aff857fc2bfe15ffb2ac9cbc345ee9bd9c6cc5ffe0a1f857fc2bfe15ff8a7fc53f3f723ee70ea3f857fcc35f1cffd8823007fc6102daa64c1adbb42057ba4949e3be1500911dd2331e7c7e9ddb4086b151b24792932e3e7f968b59ceb65ca2366d6839fba5cd4e929f8ecfe5551378ae248e4d9bbfe4532a771870b51d4d6bd3b631e4c6e470d8e4574e662e1f14fff233a3f81fb0fffb09ffaec2d1bffe57ff16ce9c3e83e3702c987a5b2a2fc47f25721d09c01107a8455a2487b8f9a1b5a049eca66bb28b080ed452b0609560b03a168d6715599811713edea73584c11fbd482270a41453271be038be46dcc7a083dfe339c9cf919016f4469219c964adc60cc9331565c5b00a5b40cf9235737ebeee75af811b0e1f06aa1ed48cf67a5e8f8c8dc19ffdf967e1b9679f43028ec5ca5794972174cef6a20b58e7c63a9d3dfc1c3d7a14366eda049b366d86f3172fc2e5f2e3da8dcdcf2f7aa29b71ed0447bc8f7d64bf44bf90045314b27dc6443bc96213fc322cde26ac995f62135b257ad2953b2f6af721def7df45a89ce488679d50b5acdbede277a7dbc10a5e8e4036363e06dd4e17efadafada17e37ceb2b51af082ad550e9fe99861e6883220f8e61952b84e06c96605ecd8b1038edc7c04aebffe3a6c47f8e24b2f96df2b21a294073cb2362c41b86728ff0219cf1a1a85ff757bc573cfbe8024ac7dfbf6c2b6eddb620bbc7e22b9c9874aa78d3a09baae1de5aedd7b617c7c021e7ef83158989f83607c6cc188f9e1987a3660cdb58bec073c726554f18eb5c8e4fb30e19ebe2391c8da5aeed11e11f11b64f0fd2a4636c8234294931b89ad4c1e5ff74ed887fa6cafa47d0863437601c475ac72ddf0e046fb8a803bda33e31e4740b755c53e2733cae27ac3381a6f18f915f530bfe302969ff5f575585d5985b7fed09bb05a5f8c63434ee8fb5f7fffe7c6a4f7f4f77f736eb61d92cfb96729fed2b8ebef7fc57f4e8ee25ff19fdecfd9a3f857fce7c628fe15ffe978c5ffe0a1f86f97a9f857fcf36bc57fddc636596d87e25ff1cfbfd3fb397b14ff8affdc18c5fff727fe9180d5665c4e48ba39b42544ee680b5e5b12e7369a54069fdb34be6d9348af9b36cd269bdafccbcdc90188df93126418fb53bdb938e4e29982244d6229699b623f4cfc9aec4eed94c6e762384c0e4bc04de50c23af69ccb09b9e34b6296e6df79b728fcb56fc2bfebf57f1ef3e0f7ef9abf0ae77ff1eacaeadbb8103b141590003648301ec5b1babcd2021cbdda771ceefc2135f5c151a7a46f2b18a4b510cae533827828fbb2602422790bbdc3dac6c83c48c6a1e920fbca35e8ef1e41522d2f46d45bee0c4074eb8220fa92a9509baa8aa1655f9b26cbe9b5b44bb2db6ffb3dc9f100bb49db755646b47f1a9d6cadd28a0b7be06fbf6ef8537bdf1def0dc0e5d93c9793d3e31090b4b2bf0f18fff11780d9da0cbf9dd0b36b8dcf12417bfc416969757b13a942392b88a4be7cf9e85b1b151bc5e59598199e919181919c5ea51bdf51ec67874a41b8d3721e660daad2dbcca3aae39512bde37319e506bb367622b3413c86526dcf36318ae2832616ed12d62d52bb7c6dd9111aca66643eb4393a9826586f0eb4ade092d9212b9fe9efbee740c4c4e4dc1c183d7c3ebdf702f6cd9bc092e3ab2dce54b9e8885795cd4645860b98b342a7f5d1401d32176a8a5bcb7b4bc0c8f7ff30978f0a187b0b2d8d5571f80d1b109a02a5ac6da66eb719037c1d42c0937cae953131370e38d8761cb968df095af7e03e6e62e07f2536845591e2ecfa84a57515439416becf16fab7d22e4217a487b1240dc7b0abe2fe46c0f7b542fec01b1755f7866998f8655d373fa62d536ef62ad2a9eb3ad4f44d6401a23bb6bef03dea230796f8408d6f67cdcab428b4d08cf0a46f6c2dca6ea57f4be08950a01a8829ef7a1cfdfa3c9bb2512b37abd48ea9a9b9b835b4edc0cfbcbfd8adb981efafeffeebdffc91e2986c3ec57fafb5f7fffe7e6b6d9d4e69fe25ff1cf9ff143f15f9faff8afcb52fc2bfe73735319b9fb4db9c7652bfe15ff8a7fc5ffb0be36c954fc2bfe15fff2337e28feebf315ff75598a7fc57f6e6e2a2377bf29f7b86cc5ffb78eff48c06a4bf0610d6a73b02998b97112a0d363984dab6de19af4a4cf9a02cdc74bf29afcccc546d29b8e917c4ec7340134b7414a7ad271b9a4a5b16df272f649e3871973253993ca3557b089733de9669c3ecbe568ee5adad8d2f1a96db9bc6c3b14ff8aff1f44fc5fbc78097ee7777e171e7ae8eb4eb0ff80afe4c44952b1ad1e40ada55f4d3e8f77f039b6b332555b2e1bf47458dbbe682f2377d93006821dc0ec719f58a10beaf98fc408a872d1f952a4b990c92dfc540f801f6893105b6b89c8e506f563e59b18bfe01791d220f9e19bdd5f936bd7e2afbfde83d1b12efce88ffe308c8d8fc74a51431f85811d3b76c2fbdef7416c134691c598a39d10cefc1ae35202604bb793afbc02fbf6ed81d9d959d8ba7573e96a0f4956dbb66d83b5f575e8f7d661696911e6e6e721b68c833e8c74bb83ad202b27a16a834834294fe4b1480e338cfe93c426e6a46181a64f7b54dc10972944ae2b82b314115c1f9f48600d9bf41d2460d9488f4b64392212f8667c00a1ba97f54d1c311dcbb84f4f4ec0d16347e0ae3b6f87dd7b76e2ba5cbc7801498136548ba23546529309b9145a03768a2abb303485c3640f095c0e7ba74f9d834fffd9e750e6868d1bca3cd809dd0e04925a431c68b92dc5d25762432295f3aaf4c316162b901d3a7408f6ecdd034f3ef9145c387f1e47aff7fa91cc87ad263b06e7c7ce79f45e0a55b21c91c9d98bc42647000dc4ccb89740d57a3462917dfc3a40adfa15af6e45eb1a49a7e139af7a155b9bda8ad854ad254452a8abe4457b609f91597b25bef03eed7908903ab134ae6738ed0562d8e8d848ac4ce8e302950ca7db11bf88a4167c253f39e1cbd8c1bf50c79c667b187d7ae53acdccccc0adb71d87b1b1b1e8afbeff5f3def7f49771a0ffdfdafbfffd367b938ebef7fc57faa57f1aff857fc2bfe8de25ff10f8a7f495ece3e69bce25ff19f1e39f9926dfc9ee25ff19fca55fc0ff73c9713928d8a7fc57f93bc9c7dd278c5bfe23f3d72f225dbf8bdef36fe3bf7dd77dfdbd309d2c0a68491167f98732e4b929926b4645fee591b60255d4d0b9cea4dc74a8b27f9cbc70e9bf44d8929c94ed7ac69c34e1327e743cea65c1c73e3da36b5f4fa4a3675696c5b0ee57437bd18d331d2daa6d7399fd37b39bd4d3ab8fc369b531d4d1852fccb87e2fffb07ff0f3df435f8ddf7be1fce9dbbe006d024ff1daa3bf5fd64ffa167e1dbc9a70f5578e176143c166e7ca85455f30500e792dc3e8f5da8385304e20484aa3334d6928fc6c4fb380f9517915884d570684d81b5050cd78e0e41956fa2cce430e48f9bcb624aa40b0c4f18034127b5392402071291c0578a8144075f9f022056e489ba90f4514e337db8f7f5af853dbb7641d10ed7dae1081e13e393f0c0030fc1638f3d5e0aeb7afa4e68175814a12d1bb5262c2abbd656d7b0addd9e3dbb6176660a2626c7a0db1dc17be36363585dabd31dc5b98e34b2b8385fce598591d151582dbf47cb6f18c8c14e995f9c68e53f3690b24ca49875e233bcef2a7419aa7a55b525ac619272367c02af884ba9cdc14fc7471fed31167c97bbc29391acb7124cf37e9b3b8619931e910c2350b05c9c9def36c4a6301def98edfba895f68f8e3a82dc56387eec663876f311ccc1e79e7b0e96969681bb8248b2be62940915a63c1e8c275e01445e9b233bf5fb2e1e05acacaec0a38f3c0a8f7df371b8fee0b5654e6e876e67b0dd5f3d0e9eec15578291af3c4dd3e958c7dc73a4c36bafb9166637ccc0c38f3c02e7cb7dca5953042c50a53457a10c5b95869b1e7315a114821df83cec2bb44fc4bda98a6cb208de7f2229f177832359b97b292995ef8978cfcd0d95ada2eea42a15ee6f6c5fa57d8f13a288c8d8b4afdb50c1cae9f66d08ab3ddad09e12303140bea29830e22b6f1f6a12e218cda11883ad5a35bab1ab2bcb70c79db7620ea6f6eafbffbbfffecfe9d6dffffafb3f77e8ef7fc53f1fa3f857fc1bc5bfe21f14ff8a7fc5bfe27f50a6e25ff19f8e55fc2bfe15ff8aff363d8a7fc57f930e2ebfcde65487e2ffbb8bffcefdf7dfff76c910e95e93916d8972a5e7dc816181232d72db26974b08e99cdb94ea1bc63e7ee4360b493797d3e46b7a5fd29fdbb49a802601b129b6a93c2976b975ca25ab647fdb3a34f926c993003fcc9a48b192ec19c6eea6b5920ec9bfb639b9b18a7fc5ff0f12fedd3fe6bffffd1f864f7deab3d846aed662d08d6544299a99abdcc275d2b1def32deda80295fb877c6cf765aa0a3278bf9a8c642fd28fb282cd58c1268cef07828109e426222f7412db4cd063bcf3518705df1a10e7fb87d073d7547fa95360951f245225f6f699dcb816d6561574a89d176b2566f97a045b0cbb4fe40deb2afb18a81124a84d6245c4b0d0ebadc38d371e829b8fdee4dbe2595fa1c940fbbee7c6392f565696e10b5ff8322c2e2c95b23be0dd606415ac806543c52ef7ccd90270fec205387bee0cdc7adbad48beba74791ed6d7d7e099e75e80f1f14978e9c5e79168e52a668d8f8f61bb3a57a9ebd4a9d3b061e32cacafadfb8a44981b8032bd925e38b1210606ab38f5fafdb074b86a405efa3196d1b6ec40d5a281424c21a6f1230f8affc527a6aa7a646a322a655c2f7d6c72a36d8f241da91331fe507d0c33d5d66a3451fc4c94e56365b152d99ebdbbe19ebbef826baf3f00972e9d8753a74fc1eada1aaebfaf8ed4f155b40c11a13a9e8066d7c3fa3852e00862c5cdb17d6fe8eaea1abcf0e249f8d2035f82898931b8fee0c1124323650c0ac41967be217e0db5f7f42d2eab55a4a388ad21ddd36eb703d75e7b00766cdf015ff8e20398b306c96601dbd6b7f9c410bb0a741d1371e971566198b0195b0ed25e17e4d8b0c6118350b5288c51b68c6414f221ee3788e35e5ccfd8e6d4dda371d5f205b5dece0e9135e97d417b5b68778876b036aba48fe70fda136cc5f682c6d4e7535ca08e77cb7ce47b6d8c078d31d5fbacf62e08fb6cdccb827f8b4b4b70dd75d7e27ed5f41e4eefe9fb7fd0f6d4febfe8fb3f95a7bffff5f7bffefe57fc4bb2d3f3dc38c5bfe23f671f3f14ff8a7fc5bfe23f774ff13f687b6abfe25ff1cfe729fe15ffd258a3f817e7a56315ff8a7fc53f88b2d3f3dc38c5bfe23f671f3f686e6c419833a4cd911ca073f3d2249792be4d3e8dcd25793aa66db3cb016c98400f93ec6db2dbe4e5fc68038f74e4e2d59658695c9be4d131cc06c3fd93e4f13ce2cf73ebc7af9bec94ee376d4ab96b6e7f2ecf72e37276e7ecbb926398175b1a43c5bfe29fdbfafd8effe5a565f8d7fffa9df0d28b2ffbf9c69300a8f555ad624bb0c1069216b7c191023a69dbbd445f3f5451c1fbee636d242750efb0345eeebb43e3dd35f72f7cdcb9ab4213095e4e4f2029440214971d8817d8eaab1cb3beee5b9dc556898e7a82d58e0caa88363a79c9bac46fb6465889074cadba0dea0df614acda0c303f82d448baa01859164bac54e3e853761d666667e14d6f7a038c8d8fc5ca5cc31ccebed275181f1b87cf7dfe0178e18597c15532423781f2ccfb0ec137b215dbaaf52d5cbeb400bb76ef800da50d9b366f868b97e661f3a68d70f2e51760796515e3e7087d4b4b4b303e3e8edf6eaeab8cb45cde5f5ef655b12aecf7c0d72b72c4ba1ea6830944b3825a2246f7ac275bb16b182051c9b7060e1effb80efd2813f31887859e79c684d818e07f38392a92a49231c31e7c4ee11956602095c53ef86521bd4da4afca0eec3e09e3135d3874f87a3876ec38e6c2cb2fbf048b8b4b98e7ddce28ae6fd711772c2009d18475c1b820396b04b151f56474478164ac73672fc0d7bef1b532c7bbb0ffea03656e4e7aa2966b0f68080b96386dcce310da48de0995bc888c57b856971d3870e000acacaec1134f3c03f3ae7da6f53ef6faebbecd5e203ee1ded2f7eb680a13f7167ed05e0500f2de0f6c5f657370bf0bfb086f814ad5ee481ee1c50662688f55ae328c9c0961b96242d35e1fae692f26e215ed77716cf097ef51b4cf0011558d8972680fb400b5b6b26986c63d98aaf6c5ea62957f36c439ca273f686f2c3f8e7039323a026f7ed3bdd01de90ec8d7f7ff77effd9f1b93f35bbae6f6ebef7ffdfd9f3bf4f7bffc9cfb27c953fc2bfe15ff8a7fc5ff951d8a7fc5bf7428fe216bafe25ff1aff857fc2bfe15ffd27dc5bfe23fbde6f62bfe15ffb92327bb46c09204a70e49ce3401551a2b05e64a16b7c98e26fb9a122e374e02db95004892975b4869c1731b8074bf0928a99d522c9ae4e4d6b2cda79c7f922e09b4527e34c55eca979c8dd2e69eb33167477a2ead4f1a3bc9d7b6f84936a7bea67648b272399f1baff857fca7f6f17b6d3ee5fc93747da7f1ffe0835f857fffef7f13d6d7d7f15927696dc50479d212c5c95424047c1c48594e06712b1c61ab1b9ef7c23fd23b4200b5d5735afaec9adb4effe0efae7b8c380061ac7bd6a1f68236b407e3adba20547109b6039b4b7a904462a8820cc5dcebf4eed8c13d30d89d1ebc120cc4567e15012b0889b6a532e2bab8b1b66aab68e919fa0081f0e16dbd7cf922bcf92d6f825d3b770092969c3bc3107d8caf3a343d330b8b4babf0d93fff1cacae52c51e0804ba7e38314860a9b058f8b8948f1ffbe6633036368afa27c6c7d1dfe9e96998999e84c58505d8be6d1b92ace870c4abb532cf5cbbc2f5f2d3eff5e1ecb9f3303b3bc5e218887336ac11d5412a0ad9957a10e1db7260cf45fe19eebdf4ad8c6917c2be739f6f41a623e76cdfb115ee7dc31be1b6db6e854b172fc09933a7616579b986039f8ec6870093b2832d01fd7daaf4e6c7b9c3e5e7e5b94bf0f9cf7d1e1e7ffc71b8e69a6b60e7ce6d58910a0289ceb74cb435830cd07ee6b16ab04296858814e3ef8f8e8cc2b19b8fc19e7dbbe1739ffd3cac2c2de31221311049499e28e76d36be6da42321716d45bd3da23b6a38a78a7950df53f9fe03b40fba58913c4eb874e427da77c2e134709224ed57b4df17610e55ca82b85f1ba8b687e6df1561796b76f276add46a30b63be4fb9a319114467b2f279801b735f86b83fffcb909dfbc1decdcdc3cbcf67577611b427dffebef7fc9d7b6f84936a7bea67648b2acfefecf8e95c6e9efff41fbf8bd369f72fe49ba14fff9f84936a7bea67648b214ff8a7fc57ffdb9e25ff19f1ba7f81fb48fdf6bf329e79fa44bf19f8f9f6473ea6b6a87244bf1aff857fcd79f2bfe15ffb9718aff41fbf8bd369f72fe49ba14fff9f84936a7bea67648b214ffaf2efc3712b0721b412e71720e49c6e4829f4bb05c809a6437055b92d70686a6641d360e39df9be2961b2b25b304ee74a36ed323c586ce9bc6e700931e69bc72fe35812c77afcd3f69a3ca816998f8487926f9d99463926ee968c286646393df4de77c8ee2bf1a2bd99b3e93ec53fcc3c0bc1c9672ba533db97bc3e4b43b969797e1bdeffd007ce9818762a51613880cd822ae1c472dfd6aedb7c82ecb5a70b1c33d431d6e0ccd6732d006f70d754202dd4b7d8ee426eb2b66f5836cae8f9edb303e1209583c3985c786f1c69135b09d9a8f4be1594cfe1cd708fc3d217e10ec75f7a875a3091f3edeeb813a198c55bfe1478c977b16e602f30fdba6757ca5ae85c579b8e9a61be0d8cd4743c52cc04a45a68d9553382286ab02d685d9d90df099cf7e014e9f3a53adab3704a8b812d4f2b9efeb3295f7d6d797616e6e0eb66edb029b366d8491d12e8c8c8ca0b98e88b558e6973b56965770cdc6c6a7607dad87d7eb3dd78ad2c0dada2ab6a87384ac33a7cfa05dae8a916b27e75ae38d74bb5891a92311b0ac1d6ccb675a7c4fe31d62cdf71108a1a78f386f083de60a6d6992d3f419528ae094cff7dd7b76c28913c761d7ae1d70e6ec593877ee6c2454f689ff84c4bb42d85f933dd1f82a5b8e68f7e24b2fc3934f3e053b766c833d3b77620b44df9e8f5a4a1affc752cc2bbb20d4dd02428ef1abece63ad2df75d71ec09c7af8e147607e61318c77430a26cbfb4aed0739b6d2f8c5fd16eafb53ed7ee562fca6aa791692f5b6be6618277f71e22984b93660dad2dec5eca47dd46182e28d04aa50d58aec8e3e30d9296191f4d0fd5e680d5b7b5784793ca3a28d416f5c7fa8c7c2c5b9c3c8b836dc3361bf7395ecf6ecd90d37df7ca4e60f80beff9bfcd3dffff5437fffe7f3429a9bae4b9b3cfdfdaff857fcd7e728feabb192bde933c93ec53f0ccccb6129a73bd593bb374c4ea7ba14ff8aff9c4f7cac646ffa4cb24ff10f03f37258cae94ef5e4ee0d93d3a92ec5bfe23fe7131f2bd99b3e93ec53fcc3c0bc1c9672ba533db97bc3e474aa4bf1aff8cff9c4c74af6a6cf24fb14ff30302f87a59cee544feede30399dea52fc2bfe733ef1b192bde933c9be269c20012b974c4d8a5285a9f0a600a563db36a42b5958fe3c77efffcfde9700c85115ef57f7ccecbdb913921020dce1be8f7007501010147fa228788128a0284114541414414540f02f8a8280802028372a97801c41ee20670284908484dcc766cf99ee7f7ff55e75bfe9ed99e999bd6677bbc2b0d3ddef7e55f5bafb7df395597fa13ac352cc280ad559287d3145308fcdf4514a1476b0516318e550ca3124b38c70dba2da1455663963526cd128a5f8e1f4a5f4ab58de701ba2fa1b358e85ca8e3a2e5496791c95b65079c5f4be507f0ba549ec3f5f12fba76e6d1bccf6bf78f187f4fbdffd8956ac58c91bef591dc68f747a7c770c662b9c934fcab6fd734ed48d44789ed4453ff4966bf4db361869207638749f0639988c2dba321f4ce118802b6ea377398d905dfa5a78fcb906cba8878107966ea9fa9b679f447e1d64ea9aa57339aedf1673bc283c77282f98240a8b1dce870f5030969e6b1e6f357e4ecea5ceae0e9a31e3401a397204b9961b8b14c9e5be2916ac112346d09a75ebe8e9a7fe4bb96c97ce6ca9b06d6419e31734974119a4423db6ae5b4f6fbdf536353537d1ce3bef44adadedac0f4b972ea1fa8611b47cf972aaa9c970bfc78c1d47ed9d5d0ce6b035b3544d6d2db5ac5dcb40a04eef1a0026ebbc321d2747ebd6aca3543a4dab56aee4f3e974ca6b639672de35d2ec4ba4a703ed753553108e03fd72f2ed436c4a4f35b37b91e11378ae4d5b94709bd1fe8a22af46a7d1835f5c4a4d5e4c71431559f27ffecf62b62302d012e0a494e5cdcd48da61c71d688f3df660fd9af7de3c6a6f6f63bde69c960e8b096e344781a13027940f7f6391745d5d597a7ffefbf4d24b2f534343036dbdf5d694c9d4aab6a11dcc52658032adfc1e84bfbb124ed0fb9bc9a468db6db6a4b6f64eaffcd91ce64ef911842e4c9162f022d623d4958af0936267f9adcf078546f9334b1f3bda6ff82d34ec9cc30deab1088706b50de6bebcb582027f1e06690973956bf840575ff77d105137dfe1f7d10dd8005d7d5dfc2e1f1b062ee9ccfedb11ed748df69afdf1c7cb184300beda5adbe8b0c30e66005db2fee74b72ff9fdcff47b5372a5ff85aa1b28b9d33eb2f5467580a5d4beeff13fb4fec3fb1ff4a743d5c7662ff89fd878fa3d2162a2fb1ffc4fec37f13fb4fecbf509b28a2cfa5fa90d87ff77a0bd559287d62ff85c724b1ffc4fea3ca2fd6f7a8b28b9d33eb2f54675812fba76e6d4bec3fb17fa2c4fe0bd55928bd793d75e699679e6f36288ee1440d485487ccf451e7cb19d4429d2c36e1514a5e4ac98af5ad501d85fa11b79d85ca2ae6accdefc5f2157306c5c62daa5d850c35aa5cb31dc594376afcccf3a5c63c9c364a8fad08232fa6ab85fa5fac6f51e31125c5c6ae509fcc3451dfa3ea084ba145b498f34cec3fb17f335d54be42ed31db516df68fefb39e7e96fef6f77b1878858dfd946ce6238d94a5c1021ccecf757df000c4046b21ad19aecad169259da3f53f2f54a1065691b451f2479429c776c458f800b0285b31c784023b0cf2208d6a2340408ea3d87914cb1491629572fd765a14ccad09a4708dfeba9256000e06db9565f459daac1b9b3777524eca04a771bb74763dd22debd7d3b46db7a669d3a60112c53c4085ad4b8b5746ae33c7e02680d0468f194b0f3ffc08ad5cb592eb01430d378de7ca3bb664ae6ddd0d61215373d9d9d941adadeb69d4a8513c6463468f626057da2bbfb3bd9d41330057ad59bb1623cf1d004065f98a65d4d0d048ed6dad94a9c93050abb5b58dfb8d6361250390072132bb3a3b69ad57464bcb7a6e03ce31384c87b104c8a32bab42293a3905bcc15fcc254043fed8aaa1d4d36b719f547f480f6eb04ea87caadf791f3d03f28992426b402929e61f0ba5cff315f8cf0db58fcfbb94d75abf3f000f3ade7c6568ccb80d68871db6a786ba0cbd377fbe37d6ebf418a80c6a984c509a95c76c471a24a5865981fcd6ac5e496fbdf596a767a369ca940d198ce538d9209de5e6db42d031fd45af9d7e384a35677575f53469d2647ac32b7bc182857e4a155a5431b5b9a2a77e51811de35a1e78d4e815197e5246cd0efb47327caff45ffb3754630bd313515e1da64f16363f290375f9cc835c88c148a5db2eedc0712a9de24c2e15b8372005449590ae516d20abf0fae79a754a5b0d4642f3387c1fe5b793942f59bb761dedbefbceb4f1261b512149d6fffe59ffcd7526ca4f15ea7fb1be458d4794141bbb427d32d3447d8faa232cc9fd7f72ff5f2a5f541d66baa87c85da63b623b1ffe26595ea939926ea7b541d6149ec3fb1ff52f9a2ea30d345e52bd41eb31d89fd172fab549fcc3451dfa3ea084b62ff89fd97ca175587992e2a5fa1f698ed48ecbf7859a5fa64a689fa1e55475812fb4fecbf54bea83acc7451f90ab5c76c4762ffc5cb2ad527334dd4f7a83ac292d87f62ffa5f245d561a68bca57a83d663b12fb2f5e56a93e9969a2be47d51196c4feabd3fead850b17baa53a5e4c8aa52d34e9954894a2949bbf549e2845891ac4b8e5f6641ce3e62dd5be62f920e538a34adb58aa1dc5ca883b6f3d6d579c32e28e755fe9676fe595f4711d6c9cb20a5d1349ecbfbcf625f61fef7a25edeaeaeaa21ffff862bae3eff73218c60ff1860592028092842514e09409a6c275805e20c8cf202d5d3e73e0e8708600e2000060b26971dbdceeec590ca0d0e9c0fa0270588a1997823e393a44228e733a2c16da95d561b504b4247532b80aa1ec84e18b48b73b60f561e21cef1f98742c9fb54618bf82d05b10478f09330139396e9b62030a74ca55073e080bdfc1de24a0044e678c35ae0b4002fdb30ce61bc70ffde525b3d0c7948a60e8581c71edf08f1d4abfbbfa37347a7493571e588dc8c410759794ab004a00a810fa91a24b7ef56bbaf45757504b4b1b0371507e2ea7daa58e1d1e07129de2bea104c76fe3c4c913e8d08f1c4cfbeebb37d5d7d7d3fad6564a717b895adb3abcfea569d20663799e9a9b9b98796dcc98513466f4186a6f6ba391a346d2b265cb69ca8693196403c0c6861b4e6200d6faf5eba965dd7ada68e30da9ab334b0d0d756802d5d6d5d2d2254b69ecb8b1dc86d56bd6d0c40d2670784d8c1fc03eeded1dd4d8d8c8e10e6b6beb98410b6de63102e82ee7f080317b12c62ee58dab066eb90a83a6d8b5f4bc8a5519b79c4a5742c31ce54be2488ffd892072dcfcb3a428bf42edc127a718cd5c6f4cec1a6fde1d1ebf7f3ff2045d7bcd0df4c47f9ea2b6d60e2f698ac9d85c1f106502afccf68b9dbb9c36a543084e9c3c91bef4e513e89bdff83a3537357ad7b3be6ea752690d5e7203262cbfadb9fc3e28841941f93175ff7de645fad6b7bf476fbe3ec7bb06b635351f6816d8bc6c8b7c9fa5da4dbe7f60db350064e2a36c23849f804f914f42000ac0d42c97b41db3adc2f73138508579f419acdc00ac6933eb6096c800f3899f133f6153e08b058ceafb090daa72423e5afc4b4a5f03488bfd9a95ff40cbfe50b78343bb3a0a1c296d953191efb6c14428ede4b4baefc2d6256366b94148584bb7ebc42f7c96ce3efb9b1caa342cc9fa5ffef555ab56d1bc79f3e8edb7df6670ea5a06b9124d9e3c99c68d1b4753a76e4a5b6cb13955d28e81bafff79ec769e9d2a5b472e54aee9f08beb7b4b4e4cd1764fcf8f1545757473535353461c206fc77ebadb7e230b8a5ea4aeeffbba7913a4400745eb264097df0c162fefee1874bf82f64d9b265bc5684e7a4b6b6d69b8b097c0e7ff101401b7a19b77d89fdc7bbde5bedaa16fbaf443a3a3ad867e02f74123e04e5b57af7a0f02322a6ede0aff80eac6313274ed2f5bbacbf5b6d55dc87c46977f2fcdfffcfff8b162da2a79f7e9a3a3b3b592764bc36da68239ee3edb6dbd67bee183368ec1ffa8c7eac59b396d6ad5bebfd5dc3e9f14c8435d1d43111f859e8349e7b467bcf578d8d8a81167a1db71d83c9fecbcddbd3f55ffc0dfc0cfc0bfc0c0473b36eddbabcf245a07f9011234632637573f308be4fc1fb91b86d8624f65f385fb9f54186fafa0f1dc5b3de071f7ce0e9ec32fec11c04be033e24ca7f98227abbe18653d8a74c9e3c897576e2c489c3d6fee3a64dd6ff78ed841e629d5bbc7889e75bdbd9b74a3b70cdbc87139171183d7a34af7378c6c0fa86774993264df43e938afad6e162ff95b62359ff13fb0fe74bd6ffc4fe13fb8f9f36b1ffd2754602b04a4954a32a3194c122e1fe427abb1fc51c406f39be72e7a890c241ca35dc38ce2b5c7639bad513895b4f25c656c9421a472ad189de1ac7c4fe13fb1fccf6ffee3bf3e81bdffc2ebdf3f63c2e179f9c0630113eba3e661002804a03ad64931d1fbc8c913c66fbf858da4ef9c006ffbaebfaa10165335eea10f091e433cb9576a28d96ae43da24602ccba8c31f4f93ad8a14d881414c64058c2e96312f8eab43d111ff35fb2292d34029720df094cecf7569a082cf7a4501788bf475933dc6529d548c4e1a0421cc4e0a1481f2721a74817c29c6a8348fa8a71bfefc479a71f0018090015f559405cbb51c0685e45cc512e4b8697a77de02fac90517d35d77dec78027d73b67b29509c8426396143844ab8aea1bfaedd2b809e3e890430fa48d37de98b6da7a4bea6c6ff536b8d29c70dc98b1fc7265acb7f991f136c957af5ee36d703570df109eacb1a991c13f29afe086867a9e9bdaba7aef5c96d67a2fb5c7782f5ad0ee2e0e3767517d4303a75bb17c25d5d7d552a6b6c67b59d3ea953f9a3a3abb3c7dcaf2e61ac60f9bf2001d32f0ca068826e7e95d0df70f2f24f15d81eab25ca70966111496624873353397f92b02a2a81bc8427ea394f4cafa24202c5f1cfd61781e01bca474dd55fdb3729e1ed83cb94a172deaecc8d1fcf90be8f6bfdd4377de7937bdf1fa5c6fac02089a00b0cc7673d32d05a4b234540b55d8ba4b23478fa0d3bf790a9df0f9e368a30d375063620104a5a8dd5cdb09f5a4d858e05ace6b6796eebfef41fad10597d0fb9e1e07ae46d92ffb0c0d2e22c397f0a8083314faacff8addb9868f1160a884edcbe9ef62c392ceaf39e4b705282565e4014f0dffe0cf96e16fc49ff86156b5bf93b6737b0182cce5b47d2a302a98afa4cf722d6d80ab48da47e4fb29bf1e634ecdbfe207c81c3f238df8374b17ea187e0de9b6d87233faf5af2fa6cd37dfb4db6c26eb7fbcf51f1b27d860bee5965b68c182053e0806632fc018f83bacab78313d75ea543af2c823e980030ea42db7dca268dbfaebfe1f6dc607803100c89e7bee397ee9be78f162ee1ffa814d4ef86c11b37fa6e025bbacbff0f710f41be737df7c73da628b2df8eff6db6fcf2febb1f90e86c69ec850bbffc7b8629343e60480be77de7987ff42c7b0a90c1d83c85f48a13981400721d0436c8c603ef077db6db7a5295336a29d76da9136dd745306c1e083f489fd27cfff85440027d053e82536eb5e7cf105df67c05f40a09fa2a3c5f41312e53b4414786534edbaebae0cecdc65979d69e4c8919eff18cb80165c43fea160ffa524acbb906a7afe87dfbaefbefbbcfbd43b591f56ac58c1e701c212913511be7fbffdf6a7830e3a88a64fdf9b376e8bf5b53fecbfbb6e2fa6575e7985ffe203dd861e33cbaf5e13e3fa5e15be5df95ef8d92953a630406bc68c190cd2863e8f1f3f2e4f970bc940da7fa9f4bd6d7f186bf12d6fbcf1067f7ff9e597e9c30f3ff4fd0dfc4c4fe6043e0640017cc68d1b4f871f7e18ff1d356aa4ef5faa41aaddfe2bada3549ac1b0fee3470900002e5fbe9cde7bef3df67d2fbdf4127f17df012947574d11bd95f551fecabd1c005ab8bf06200bef7da0d3f02b926eb0da7fdc7292f53f9ee0b902be13cf1273e6cc61d6f2d9b35f66fd84ae4237cb7dc680c83d9cf85508740ffaa9fcea38664adf75d75dfc1f83e09aa9a3c5fa9adcff774f57ceb5c4fea3f30d2649d6fffceb90c4fee35d4bec3f3adf6092beb4ff8a0058e1465572ad378cb5bf152eaaed718c1612e5c8fada89f7755995ea403969fa527abb8de5a68544d9424fda55a8acde761c89fd27f63f18ed1f0fb4f7dff7005df8b35fd19ad56b838d783760f1f1c141ae015a20b549cf8c309a590560989480008401c5ea0e187034db8aeb0a28c3f5c1468e061c094801e785e50a6323402d6e831584ed12b081b451da00c9992002fdd764a15163ae8034ead8ef26b32fe5b21abc05f48866b9b22491d147b2029058ca0082d9c698714b8c719490841651de78713b642c753f531a58a1cea32400de6cb2c93b4f69ae209d21fae2178fa79ffcf43cefe597b7d1d90d7c1312db1b4f4b81a6c06a8431e8ca5af4c8238fd3a9a77e8b167f805f9fa5759b24f49a6ab72be5a2bf608de2525c3d2cd0099b1a1aebbc4def2d699ffdf6a62db7d88c468da8f3cacf3203067e210c46adb6f60e1a37760cf7b3a1be8e998aa04b63bc739d1d9d5c5f7b5b3b6db0c1046f7c73b46e6d0bb311ac5eb39636de684366b142b8c20d268ca3762f3d58aec68f1babe75e814f701e610d85490b2c4060244ad7a4b98ebafa7a351fae847ab4b91dc20c04e0574aeb258f034219e614780d3d47db31ef966d470e7394bf8b23bdef0f5cad3b1a80e52af05ed01e4fa77458410e63a999ab5c074023cb1be72c3df9e4d374f1c5bfa2175ef81f83e470cdb66c5f1ffc36339398d253a5d7500c9d9614686bd49826faec673f41df39eb1bde9c8d51ec5796f2090a8025084257b755b1d405dd91bad0d62e065cb5796dbcfecfb7d1053ffe39ad6f6955fa48e4b340f9364601e84a7c94cc1fff0d81a4d87f19eb9cef3bc4e7e8befb65699d10a09215caeb6a9bcef32514809ec4c78a0f710cd097cc9700c6720600cb2c8bac806190fbacbfbb215f6e6b86423dd2be4f8e0a3188f2986190023f2a6d92ef7efff598fbe3467a3df18e478c68a61ffde8bb74c4911ff5fbd59b32d4d77f80627efad39fd2134f3ce16facc41500913efde9e3e8e4934ff65e40d7f4491b8ba505c0eaf5d75fa7fbefbf9f4392e2a57bd42f9bfb4ab03104e0cf269b6cc2ac60bbeeba1b6f78562a43e1fe1fec08afbefa2a3df8e083dec6ff3bdef102ffd7e7fd2100b40010007685dd77df8d37f4008ca85492fbfff2a43fed3f2a2da4d8f33ffcc37ffffb2c3dfffc73ecfbe03300b6c17de44088f890d1a347d1ce3befece9ec1e913e64b0d87f5ca9d6e77f00971e79e411bae69a6be8b5d75e2b6b4dc446ec2ebbec42e79f7f016db4d1148a6a775fdb3fc056f0bd2fbcf002fb620026fa734d842e631c101a7cfaf47de898638eee064e1e48fbafa4ae9ebeff03181c1fe813d646005bfad3df604e001c003bd64e3bed4cfbedb7afcf42644a62ff85dbd95f650dd4fa0fbf0780e6638f3dc63e046b627fdf4f4709f414c041dcd3617ddc6cb3cd68c68c83fde70d53aad5fee3ca50b8ff8f2b95da3fd634e8279ef9b0b6e1bb30af0d84e0dd0574146b1cfee25910cf813beeb8433776de6ab6ffde926abfffafa4aec4fecb4b134792f5bffc3489fd974e0b49ecbf78fea16eff3d0e4158acc13d95389d0b2b57398313a79e4a27ba92ba2b3544484f9d445c432d6540e5f427ead8acbb92b92cd4969ea4ed4de7556e3fe2ea7629872ed7e2b423b1ff78d78aa549ecbf3aed1fbf2ebae8a2cbe8ce3befa3cece2e9fadc932425b41e4d74412ca0fe5d8063b1500295d5efe7426adc2b869e1f254e53e300a1f09dfc5c02901461031a043b1e0046c5052bfc93c030000cab4387c5c0028c05f006ef02b29477f77c9806ce8b43959f02d2b0093e97e29608d060458ba0daeb07469dc8215b06bc95fa473f487dbacc166a4cbe2b225ac19f280350cbfe6b28c308c06d314e973b631670c080210c8026b5517a5ac0c6032a442b4a14f2eedb4f3f6f4c76b7f4f5b6db109792341c5005800b9b81ac8a5d2018c92f65e32b7d08f2fb898aebffe666a5d8fb042a9609cb2aa0d0ab6e5924501cb97ea634e81c1001873730cf01ae9bde4f8c4278ff05eecef4975b560bc5a4bf5f5081595a2ce8e2edec45ab36e2db35f8d1e35925a35e00aed9a3c6922d53734d11b6fbe41d3b6da8ad6ae594d1d9d9d3476ec680663358f68e2fcefbefb1e6dbcf146def5b55c3f5eaca05d7575b50cf45ab8e8036662026dfe1a2fdf88e6467fdef0cbce9a4c8d37a68ac50d7d4c71b8c51cb36481198a43b40120a4fbeceb9515cc1787898b18e7302c2b2a4d98e0c98a627c72a3015e46022a1e7352f45942485aa418b0749d1a8ca7c2f5b90a84450a64c659ec341fcf7ee575bafcd7ff8f59d2dabcb92237cd802ad7251fa4867a6c9b614e1a2869e91080489322350a2e3536d7d189271e4f679e79bab7f93e9999dbb82db6068ab90216437bedfcbeca1881c90dcc5d28dcd3d555ab5ae97be7fc88fefad73bc8ed427f0c5b4fd9dc234e2b7ec3d1605036b9003c2462324141f2d8ff241c9f2ecbd62df31f2a48b13fa5d201939ce907c9f01b76c4ba99176650fb6163c2fcef1c6210d7f539d38f73bbc4ffa84952fe4ae741adf099a65f57e3a2c153d20f4b85e454455a79eb115901bb173ec266e86a201be9317574f85730df7df2d8a3e8bbdffd16353535fae524eb7f706cd62dd7b1b17ce38d37d255575dd56380cc89279e48a79d765ab7100dbd7dff8ffb8cf9f3e7d39b6fbe497ffad39f68eedcb97e88c46a1100850f3ae840faf4a73fcda18164237ea8deff438f007ec3e6f2f5d75fcf9b7603b5195248b6dd763b666701b0051b79606d2926c9fd7f61196ccffff06d082177db6db731cb1f405783410e3a6806b329edbffffecc040296bdde60afe96dfbafa49e6a7dfec75a72ce39e7d23df7dc4d3d95d34fff86b7269eca80d0beb27fdc6b61131afef7a9a79e62c62e1c57938019649f7df6a18f7ef4a3b4db6ebbf12675737373bfd97fa1f45277295d894a17be669ec33d2b1838e167b026de74d34dfd0a408e2b7bef3d9d8e3ffeb3b4d34e3bb18eca9a389ceddf4c0319caeb3feed1e0371e7ef861069ce27b1cf6aa6a11e8ef8c190731b81e80cf52f774a6f4a5fdf744e74bb5b91aefff2ba927ae0de23d25c055efbefb2efdef7fffa3db6fbfbd2a9f2f0a09d63ad1d3edb6db8edf0f1603bef6a7fd47a58d2383edfebf507aa93bb1ffe8ebc9fa9f3cff176b6b4fd326f65f9e24f61fbf6e0660c56d405f37264eb9950c569c3e947bbe3724ae82c54d1f275d6f9451493a336d783e44fa6a9c7b227d39ff954a31675b6e39e5d85462ffbd2b89fdf7bffdbf3f7f01cd3ceb87de0bc737150029dc0620265cc522856bfee6bc6aa06264f2fe025c003a73de687083307dd2a79cc1086374d0678b11f6181f244041ff798c508e66bf425d78c0e7ef467bc23acd21af5c0d6c90d060f27189c3db5956c0aa651ba02fa907fdf3c745f7df4c6347301e01bc826be954ba1bfb960033b8dd1430cfe4e98dd12f7c8499478520546c630a849655201dcb3b4e797f6dd7db489f48279ffc453af9a4af30db94e5e6a89830fb953060797f530ef88f521c826ed6332fd2ccef9c432fbd30db6b4f9a412c4899f2c3a4e5037d006831e743311d0569468d6ea6ddf7dc8576dd7527da669bad382c602ed7a5007b5edf00aa42b9006e60d30ac09e31a3477158c09a9a3aeae8cad2c40d26506d4d9a962d5bea87ca58bb661d8d1b8f90869ddeb50cb5777479bad8414d8d2a8ce18891cd94b214686fe4a811d4d2d24a1ded1dde6642a35747078d193b9a8ff1cb4c806f00d64aa514684e01858a8be8ae9ae4ca6d3696bd97046011a9b07f458a708b03b4541acd3e25c7fc3f155610a038308bcd5fb0842ebbe44abaebeefb68d9d2e5defcd7683bb37cbfc60c590c8e52613e2564a55193a7332e8d9f3086be7aca97e994534ea209e3c15ee6e98603fdce05611e9dc27d673db62504aa77c24bfbd453cfd177bff7237af9c5ff79735a4f4acd94de86fd5cd01ac51066daa284e3230a00975c05ecd6ca074f8a9dca38fbe0525787693518b8c4274afa280016d2a60c80aaf80bf1a5e13ec83993d14acefb7fc5cfbaae0f6845fbb21a6c95d2005669bb6db060f90c61e293751964f86e66edf2f2a724a493ebe685b4354335eeb0c3b6f4eb2b7eceac74c9fa5f7afd07bbdf6f7e7325b37cf4c6cb6cccf5b469d3e88a2baee0bf71db1c57b0317ec71d77d0ac59b3e8c5175facba4de628512c2053e8f0c30fa78f7ffce3792fde87c2fd3f981100e0c3c608e665a01884ca116c3423b4cdc73f7e34edb3cf74de1449eefffb5ffaf2f94f048c33fff8c73f9809a85c26a36a12dcc3426711b6f04b5ffa92ef4792e7ff68a974fdc7c6ee85175e48fffef7a3fcacd05301f0e8b4d34ea7b3ce9a59b0eeb86d8c12f85f005eefb9e71e6601190c8235118c84071e78207df5ab5fa56a92de78ff07a0d5ef7ef73b7af4d147f91e65300005c08e85fb1480e8c1561696e162ff3d297730adfff01500b2604dc41a3958c02c8504fa8b1f3900a8bcf7de7bf33a590950b937ec5fca49deff5345e74dc1fa76dd75d7d1934f3ec93fbaa946106b5c11962cdccbedb9e79eacab60ca2c14b63eb9ffef7f49ec7f78dffff7a4dcc1b4fe578324f69fd87f9fdaff82050bdcbeacb8afca29d7795572be924929b7bfc5ea804439acbe6e5338af48b1f28b5d2fd58ea83ac265f69723ec890329375da179ed8bbe1672607ded7812fb2f2e89fdf79ffda3c8fbeebbdf7b797d39ad5ebd9a140b929b671b52a73feea48002f2dd07e45896da6417a093ceebea8aa4ad024660a0424a877acbe5fcf25dd72ddab79c0118e03cc65808d30a5ee2b8661b42ba03b1759be53a3e606c4278bc940efb85631fe80536272700663805e698cb32d2e158d8b450664a7f67c61bc765400ab7dbd2cc399625cdf2f2e614f0c971351022e5b31171cf6dcd489573a8a1a19e0e3fe210faf257be40fb4cdfdb6b3bcacd51ba045e27006011ff05e91088811c374559efcb7df7fe8b4e3f7da6b749dbe6cf1359ae86b018a288cb54db2dca03e104202c803abccdd4512369336f636ac71db765daef09e3c7d0940d27d3da9680010b0010cc23fe222c1db898c052d4dcd4c80029fc220d7d5eb97215d5d5d779e91086703d6db2f1c6d4d9d5c52106bbb25d34a2b989015dd8c0c546de889123bc1742cb68d2c40d18f005d0555d6d9d068e28f00e80242aec9c060c9590e100c052e72c5f5700aecb7ac76bd7b6d0bf1ffe0f5d7ad96f68f6ecd7c9cdda4aaf6dcd08e7080b96f2168aa12aaf544e6b5b4a874ffce267bd8daf6fd3c489e3bdf30e3959c5aae7e65c2a1a4ad3929085ba54ef6bce6bcb3dd0dfd366d2ba35eb99bd0b13ecb846784057daa019fe183c99cb0b4b280c516110a9f840b16d3f7caa3ee66b06788bc7dd554c51ec672d2b0fd805111fda0d74256108b53fa1127e92c19d9695c760e8186d0a337a91d10e3b8f0d50b1dd897fe7509bb6c5f368e9b639a69fd5be92fd26054c86a6ff37d7088421bce8e2f3e8904366e86149d6ff42eb7fa7e7af7efad30bbdcddbeba8b7051b79607c4068c2526d2c357e00f43cf3cc33cc7675c30d370cea17f060c2029b0d58b1a64f9fceeb484f64a0eeff3ff8e0037ae9a5971870858dff810e4fd353c15c80b90d0c2d004c8425b9ffef1da9c4fecb4d07dd9c33670eb35cdd7aebad835e370b0998b1b0c90c5f1215e6ae1c499eff551aac2de79efb7d7ae0817f516fcbcc9967d137be713a3f6b84db216d899228fb878e83ad066064802706b3003871c20927303b085823a3428a41aafdfd1fc001601102981de1b106b36cb6d9e6f4f9cf7f8ef6db6f3fbe870baf89c9fbbf78d7e2b44fa4afd77ffc780161ba01b8ea0db6db6a17e8ed473f7a18b39d02b82cf7da7d65ff615d32cf174adf5bf5f6763903bdfe83ed0aebdb2db7dc42cf3efb2c0d7539e6986318880516b7a953a776638f8624f7ffc9fe5f7f95335cefff87f2fa6f9691d87f62ffc5ca192af61f1982b05483e25c2be4408a499c0e974a536a20a39c45212713556f5c6713b76d51e9e25c8f1adf4a14a39862c771be71a49c45a6af0da454fd85cae94ba758ee35330d24ae132f963e4efd89fd27f65f89548bfd8361e8a28b2ea5bfdd7eb7bfe11eaedb041dc95fd95cb735db0b6fc8e702b000f2c9c7cc27a103b1a98f07569c452eb365663bc875f38054024008f78d014f1a24111673c35ffa24e00461881186171f4ca0db8e34521bb71d34efb86e30c298c26db63468c108cf18c04f28e8976eb3a4f5fb853cb61a198e2ae828a0986a96c5c017b25469f80ac0c8a44913e8339ff93f3ae1c4e369d34d37a11498ac2ca73b9b4e84b86a6030ab3e008bc3f0d9290661ad5bb79e7ef4a38be9baebfe425d1d5dba2faa9f26bb1569b092029fd97e6842191757336c3153172960465353134dd9684306434ddf772feefb861b4ef4ced753adb7e9d1d595a5baba3afe557b5b5b2b4ddd78137adf7b51ded8d8c8e3d6d0d0c0f38b97cd005721b42036ef369a3285c7b4abab93264c184fed1d60b7aa63401a365330960cea71155844e99502b8b9fa2f420faab12e0d78ca634cea815f88e553060c80a5d9e2a09fa458b038cca7f7b7b5b59dfe3beb05bae28aabe8c927fe4bdeb0078c47d21e31026698caaf0b002d66a3f394afa9a9c1dbf43a95befad52fd1f871638941886915f6b2d8e8004868590e03f558ffd871a468f5eaf574f677cfa33befbc974369a6ac54d02b4bdb936e13f799fd830e3769b0b98581a4be4fb5821083c226c5e3a4fd8ea38153e6d8c950f83e5697c5e51865c8b5b466cb937608384bcacb034c198c557e3bc4ffe258d72dedcf4b6305205bb10d807ed85787d644b3ff32a17c4eaf09e2b77dd6421db294b4df13902a7cdd574ffa029dfecd53ba6de425eb7f7e1f9e78e209fad6b7bed5271b31981f847ac36624039863dc379ad7e10bf08b7c00aff0eb676c320f56e69a28c13a847078279d74126f3ce317d0e5deff171abb42d2d3fb7f0903f2d8638f31b80e1b790805395404730026acd34f3f9d015958db93fbfff8f7ff71ea2f544edc320b09fc456b6b2bfdeb5fffa29b6fbeb92a4392f69548584de82e429e96cba6903cff2bc1fa72e6996732a0b42f04f3f2fdef7f9fc146d21e9138fd051019209f6bafbd965e7df55566ea1a4a6b228058bbedb62b7dfef327d09e7beed1e3309b95f8eb701a48946dc877b0242f58f03ecf09c0c89893c114bead98e03978fcf8099e5fd9964e3ef964067bca9a68ca50b1ffa17affbf7cf972668bc57df42bafbcc2ebe450d1d13802bfb2c10613bc7bed93797d840e43b77bc3fee3a68f2abbd43d7eb16ba5eeade2d619a75f71db26dfa55de6759162b68077036065bbfaeaabf92f40f443e9f9228e00343869d224faf4a78ff3eeebb6f1debf6e1af983105392fbffc2f5f7f5fa1f377d9cfa87bbfd87fb155792f53fb1ff42f527f65fbcce38fd8adb36f92eed32af8bf4b7fd770b41186712e32a6554c5c5f297331071ea2fd52ea2e20e28eab8d479f35aa1c9aec469147386718d35ae738f736c9e8f6a53b9528eae95a3a3c5cef7d431c4491f47a7a3f2167314a5ca716338df62ed4dec3fb1ffa168fff3e72fa0d34efb0ebd3df71dff9ca43359522c0afa2540042993410a38eb1a7d37195af4b13057e1181b837eb82da9cf0a0008b289ef33ab182109f1314314fa1bfd6e10be0bf9254f1890c06dd475713edd1f4b832d04986519ede236ea7ce406a0304b8f87f45b8016c262e33aae3f3e96710dd44a60ff0a03c1a48e14404a0094d81665bb14180afc58c087d8b67ac9cde397cad14e3bef405ffbfac974d45147d088a6066e919beb22af782ec7298ec5e90ec022c77bd9a7dbebd595f54ecf7d7b219d35f37bf4ef479e304230fadae0eb08f37269bd30e748f17529d809588a100e3097cb7a2fd66ab99cbaba7aaaa9cd5073730303b2b6da6a73efb31587589cb8c178aaadc118b9de86c65a9a3c69030250a679c4484a7b9d6c6e6ea2b16346334b0fd874f0821de724c423b317f9d327f317b08c293019f961d11827665b3e902c8eb8aa08350a3d307b2b9c39aaac010460018c66f9d773fc71785ad3ac9b6fcd994757fcfa2abaf9a6bff37cb1ceb23e687a343700ed89fda0ccb4ed6ae62980735c6f3ec7d0ccb3cea0d34e3d85d21955a7cdf539dae6b53f33f17f0cc052e029954e85d1b4bdb6bdfaea9bf4e31f5f480ffcf361afac8cee8b2ec79562944d4200be23738cb4bff09546fb0bdf87e8b48e66dfc3f59cb06c51fe7d8fef872d15b2cfcc2fd7716c8623141f6432058a5f0da7671fa6c16300bb428f25942ab360b90a602ab6206028f16b103ff420aec137196b87804595bdab7e98405613cc456ef78750b9c6fdd6be79f7dd77a15f5d7a214d9c38c1d0bf64fd0f9781f03fefbcf30ef5a56063f2b0c30e2b9a26dc366c346313fcde7befe55f3f0ff5cd226c6c22ec0fc6a9b9b9b92aefff01bcfacb5ffec27382cdffa12e60173aeeb8e3e8e8a33f9edcffc7f48771fb14a76d71d3e3fbdb6fbf43b7de7a0b8334b1d13c9c457cc9d1471fdd0d2cd113fb2f4706ebf3ff638f3dee8ddd09d49702901cc27e997ebe58fba48fd0f19ffffc620ec334d84385951200268e3cf248fad297be9817ae5724ac5f22e5e84121dd89538e9c7bf1c597e8c61bff4c0f3cf0c0b0007b9e7cf25769e6cc330b023c4506abfd8bc45defca2dbf3fd77f3c43cd9e3d9b1e7ffc71faeb5f6fa3850b175022caffeebdf774cd38b4738fec3f7c2e9c372a7d39f756d574ff5f8e5462ff600c441873ac8d439d992dae0078053dc5bd1cf476fcf8f154a90ce5fb7f91fe5effc3e7c2790bb537b1ff64fdefebf5bf54fde17389fd27f65f6efda5da4534f0f69f9a3973e6f9e68970c3c27fcb1dfca832cdbac2e9cc3a4c295556b17684eb2ad586a87acd3a4bd51d359e66fe5279a2fa10a7cdc514ab128715274fa1712bd58742ed8baabfd4f7a83936eb088f5521432fd5b6427d286670e671211d8c9acba834c5ca2e762eaace42e913fb4fecbf9c3c85c6ad541f06c2fe67cf7e95eebce35eeaececca2f4bfc816b808edc10d8c60e80201216cb2f9f280017c8770dc87175fe941156cba202f66184cc12b096a5ebc3df000ce4068c32463f4c20961b1a1bbf6d285b65e86ee3d2577d2edc274bb78934e8c064cc31c7d1d70be9a39d1f7acccf23a10af5b8e75c35da020c726dcb676b1a3d7a14edb5d7aef483f3cea1830edc9f9a9b1abc7ee498fd4a85d0a378a23aa84608ddf1d9807011702c843874e9fdf7e7d3ac59cf2b261b57036648819c18e864ea2045d981e5a7751df413e5a0ef3683c4c076b5bea58d162d5c42efbefb1ebdf2caffe8bdf7e6d3f2e52b68c99265dc96dd76df95fb3575938d68e2c40d68e38da7d0840913a8a9b181468d1aa5c31236309006a11b5300ae612c747be42f7f000fb2f4dc983a436eb776971c425beb41e9a4654b773f10a312cb2d5966deda62954e63057043fd514470b6adc7d21bf37163c6d1565b6f454b3f5c4c8b177f401d1ded3aec9dcb602c6619d3655996f4cd62fd5236e0f2b5f6b6765ab4e803da76bb6d68e2a4492af4a625a14bad3c7092392ecac6d57c48904c7c9abd0d88a6e611f4c8238f5187e7eb1c0d80b45447b57ff2d2a7023f4562db14f8150e1faa6dd4b7e590df0f004af9f7556639e20fad90bf70f31377f33d7cda089dea6836298a5aaf7c76c20034c77f755a0e81aafda60f62a5c007495b5c630d9063db702e51eb0d036d43becf1f0b43a7702e954e71f8d07df7d993264d9e98576e21198eebffd34fcfa23ffce16aea6b0143c721871cd22dec5254dbb08909c0d545175dc4609f77df7db76068cba1248b172f66f60c8cd5b469d39891b11aeeff31f62d2d2dcc7875e18517d2dffffe776eeb701030993cfbec73b46cd9526af67c3d7e959edcff57d7f33f4292fef6b7bfa52bafbc921e7ae8a161a39bc5447c09984e702f8b8f3009956bffa5744ed249bec1fcfc0ff68d8b2fbeb8cf01c9f0f160b9d87aebad63d93fd86b104613fe1700c3aeae2e1aea82fb80d75e7b95c16600926cb4d1c61c4e3c6cff2261df13d6c128fd8b4a53ac6c11b4076be2dd77df4d3ffff9cfe93ffff9cf9062212b2660a5c17d23d83b274e9cc87f93f77f54304da1f2cbc95368dc0af5013f56c07df31ffff847baecb2cb388cdbead5ab291125cb962df37cfcdbec5bc0b03475eaa67cbf8df7509052f61f752e4aef0ba52f74ef142ea31aeeff4be99ca40bd7558efdaf5bb78e9f2f2eb9e412fadbdffe36e4c1c5e508d61ab00ce3390476dcd8d8e4bda71dcdf7736176c8e178ff1fcedf1feb7fd4b9c4fe93f5bfd8b8f5e7fa5fac4f85ea2ff53db17f2a7a2eb1ffeab57f6bc182056e5443a23a12b7c3a52626aab185f215ea74a9f4a5cec5cd176e63b8dd71ca89536721038b4a5769f9e58c6f54baa8f695330f95a429953faa4da5ea29675cc2c75175162b2fceb1595e256352ae7e14d2bbc4fe13fb2f565654fb068bfd63d3fbd7bfbe8aaefbd3cd7ef82c9cb735c00692f35e0ef18b0e9467ccb500a72026e848d8a684ed8ac2ed71d526bf638cb10fa23242659921b9cc9b047ccf3941a843f33afe46013304442579387ca2ae17e953ba9d0c48005b8ceebfb0b9082317e972cc399271b035d0cbd17dccebb33976c8ab43de715f24fca0c91aa3eb60b621548b0f9058b61a03808a369c3c914e3dfdabf4918f1c4c5337ddcc3be7b5c3e9e2b25236c02c76de5c17d5054b06082c455ebfd12e00b01c8b1984162f5e4ad75cfb67baebcefbe9adb7de65562d94e9bac15ca15fa994ede5b5349311375f81765cf2813138c3e032b275e837354ef2328dc73d8d39ce321bd6b871e368938d37a23df6d895f6da7337da7fff7db8acda9a3429b222d767f5096bbce84f8f250e0b56af5454a0e8b02df70203565884c1ae7442a75bcec0c210de0e2f9489162e5c427ff8c335de4be53fd3da35ad7ec84a491a00f43c3f9473f8187aabcec1161c4e7fc041fbd239e77e870ed86f5f2f69a70272593acc9e65e7e9b6abfb61d92ef7df75054588709236b5b575d277cefe01dd76eb5ddea6582e606bb375db738e705591629fb37d7f257f6dc30f291b0d6cdb0c572a7ec47103a640aec50ac0633ec394f63b3e1ba069afb8a6cb869fca1a7e17695d5d4f5eb847d7003a193ed264c8923c61e1360a68d50ac0ae7ebb2c055693ef64f497c7c0b60bfa1bf1e164b4478e116ef4db679e4a5ffef2e793f53f220d18a610660d2fbefb5ab0b90180449805cb6c1b5eba634304bf801620d27015b07f9c7cf24974fcf1c7f3d885a53fefff1f7cf021baf6da6b86050b5931016002a122f16b7493fd23b9ff8faea79c71091fc779fe075b0236eb12668fd272f8e187d3d1471f431ffff851c9f33f155effc11473da69a7f50b9311c29b021c61b23b85db87351a9baebffce525c35ac701f2017bf16f7ef31bda7cf3cd4bea69d431a492fbbb70996081040819ec9cc39d694fe9f0e59e0e4f1912f63f14eeffc1e87dfdf5d731ebecca952b2991d23262c448663a3df6d84fd28e3bee1899a65cfd28a477c5f2c6d5bbfebcffefcbf51fcf7c58dff0cc0760f1707ebe2847b6d8624beffde5eef4c94f1e4bfbec33bd68da4ad6ba707e48b5ddff0fd4fa1f377f62fff1da9dacfff9e9a2da57ce3c5492a654fea83695aa27b1ffc4fee3e60bb731dcee38e514ab3375d659679d2f198a658c5b68a974b81eaeaf9821156a5bd4c4c529c714336fa13ce6e49b9f42e584cb2a641c51790a8d5d54d97114306acc0ab535ae42159a87626d2e546e385d9cb98cea5f1c9d8b3a576a3e0aa58f330651631d4e1b555e250b70b16b51790be95d62ffd16d4dec7ff0db7f4bcb3abafaeaeb68c9e2a5f9e9dd209c1f830e0c2082a5f39acc2aaed4292024b4471725e02dd9a037f54636f2ada051798c2f7e3dfa9ab44bda6187f543d763e936332043806056815f485068bc290049f8690c162ecbe82bda838f946f32cd987de9a66379734a3eb84b801e7c9d197fd2aaa76e8a59861056adb63643071fb43f9dfddd33e9a8a38fa271a34771f836dbca319b936db99ad146905b7ec9459441014e4833602910954dadad1df4c0438fd12f2fb9c27b897d0f2d59b29ccb16d0988251b9c1b8e96912708d8ca54b416833244a31338fb0a629262487b284f071f50d35b4f5569bd3470f3b984e38e133f4952f9f489ffabf63e8d0430fa22db7dc8c6a336966ccc9681018002c8ef72208bf2e0bfb231e634bb30515f8f83a56ec237d2bf2718b9b5b8fa4bb2dc7a8ac040356c56287c6d0ebb825083e1d6610f388702d3beeb003ebec5b73e6507b5ba70fb0547ecc0dfc8cad6d4121f6f47955dde2c51fd2c2058b68afbd76a3319eae03e2056d62f0151502fae4f75fea00c867c2f80de895d75ea7c58b3e542c5869039029fec356edb0f3ca0c98efc4174a884b22ca63d7630027511eb89387ceb0f3601c28f0b144796051352696efcf2c5d9f2579c4af08e396e849787dd0602a6983a3c30d5adaa7e29a8066cdfe5aa69dc839634cc56f39a17c3a01337059e679a98f025fc7e3a081bf98dba38f3e92826e24ebbfd40500c335d75cdb2f9bcdc2d871c4114774eb275eba2f5ab488bef39db3e94f7fba96de7aebad4820df7012cc098071efbe3b8ff6de7bef6e20ac38f7623dbdff5fb060015d7ef9e574c51557d0dcb97386050b5931c166e6134f3cc11bf03bedb4933f2783d5fe0b5d0fa729d5f6626d098f456f3cff83bd02216a7efad39f7228cce110f6aba702d61ab083ad5ab59a595d274d9a9877bdd85a06192ecfffd75f7f3d837ffb43962c59c2e17fa74e9ddaad9d60e2bae5965be8873ffc21dd75d75dacf3c359708ff0e1871f7afef7491a3b768cf719cbacc451be1612a51bc5fc8e992efc5d0473326bd63374e69967d29d77dec9f74fc35d3006f7de7b0f33b2004808a05c5892f77ffdb3fee3fee4e69bff42e79e7b0e3df8e083ccd096483cc18f3dc0160966b7cece0eda64934d0ade7327efff7bbefe07cf7cdfa11b6eb881192787fbf34539025b07f3eb7df7dd47fffdef7f793d1c33660c872b346528deff172b3f2a6d6fadff89fdf79efd47b53559ff8bdb45a936172a379c2eb1ffc4fe0b953754ed9f4310c631e6de96624a514939f23daab3953a89426d0dff0d1b5b21638d3349c5245c6fa1fce52872dc36543a4785ca2de5c84a1946b97a53481f4a8d8b99b798a333cb8b9aa362ed2925c5e6368e13299437aa8c38e77b4312fb4fecbf3fedffedb7dfa53ffee1cf94354224584679b6008e2cb5494eaeb179aed3e09a9cb7d4c9209ff4d5e89b658c013e79a1ac887cc618332ca10ff0d29f94c1d0c5edb42c9f790ac7c23ee3f7c5acc755808394c13a85d07902ae421e019e49d9fc5dc272b98ab9c5175d9ea5c741d272fb745fccbc389712b010faabc3a4719f99054a8f939df6ea463a95d771b3d4d0544fc77ef2e374f6f7cea2dd77df956aeb32e093622628805e5296cc83cdff47c90ac0510a80854efb102aceffeebbf3e9daeb6ea6df5cf97b7aeef917a8bdbd93bbaa802f3aa94b24af6100fc023397028590df57cb185b2253371508cbb500d8cbd188914d347dfa6e74c209c7d169a77f8d8e3aea08da6df7dd68a38d27d3e83123bc9715694a7b1dedeaeaa04c3aa5c15f61b055e81303a86447e5abe0d347702796eef61cc3befb0880c5dda5e0c3002c525a4329ccb7a3ae7893dbd8d8403beeb83de77bfe859718d861b237d9c6d841cfc9b5b48ec9fc112bd8fbef2fa00e2f2f58d0106ad2d5ff2cb20ab651891a835c16a0a834010c366efc78ef8522d8139ef0ec3fc76107a1b7489a4a0900cca13c13a7c0bfba6e00fb621fa3ed5b250cd8ae4c8014c3cde037c0526507ac6f79eb2305615c8396933f5e482b3ecb077fb94108552b62107c762dd7f5fda1f81a094f08ff206c85bebf73ddbcf6fb3e9064ce433edd8af815946e93150296b9c638095b96ad4328ae58b18afeefd3c7f8a16b92f53f681b003e7ffdeb5fa9bf041bce60dc0a0bdaf0831ffc809e7ffef961cd7a152573e7cea5d9b35f6190c9aebbee4ae54aa5f7ffd08d9ffdec671c62a9bdbd8d125122e1405e78e1051a397214b3629932dceffffbfaf91f637fc6196730f315001989c417e8ee4b2fbd48cf3df72cb5b6b672f8bbf0865d2109cf65d41c0d85e7ff1b6fbc894353f587e0be6bca9429b4efbefbfae7d03e80ad2ebef8e7cc3a08804bb22606b26ad54a664cc118edb3cf3e1cd2388e4f8923c5eeeddadada99c1f3f2cb2f63b0c07007889b02161b30c7e17960f7dd77f799a7450693fd47d55f287fa135b3d0b5386da864fd872e0270f58b5ffc82419bb08d443fcb17f863f8178413c53d3764bbedb6e3bfc9fbffde5dff71fff6fdef7f3f79e6eb81405ff1fe0b61d2fff18f7fb0ce2244bac9a839d4eeffcdf2a27c74b1f6949262369cd8fff0b8ff8faabf50fe6a59ff8b959bd87f62ff661dc3d5fe2def41bae0ee5594c177db7ce845e94b4528a78c9ed457ca39c6c90f29b7fefe1ea3bec8df9775563a2f858c2e4e3d95e4893a8e9bafb725b1ff9ed793d87fdf4a9c3a31a4d75c733d5d7ae955411e57818964bcf1d098d661b4041c80348e6654615080f102cf0407640136d0df39ad66c6928d7e61aa0a5892c8dfb007a3919c27fdd7525fb8dc941d0aaf873cba3e2e5bf75f186d74213e508c7479f8659719a24bea34f39aac5f922fa7eb22cbca036708a3176a64201909902cb821e1e6a2280ea5a600083ee82c05c62b9b54b7bdef96cb0091fa860c6dbbdd96f4d5af7e858e3cf2306a6a6ae4914ba748831982f063122891411f9a59c88efcb19a9c74392420cf7f2a436bd6aca3871f7e8caef8f5efe9c5175ed2e3821ea439fa9ca3d54ad5e0705b1d4535e6f52b45e974c69b23867ee9e196f08216035ed06780c9c058356a54336db6c52674c001fb32c3d50edb6f4fa3c78ce2eb295beba42a8259b9783c1972e6b0e2c8980afac404f3e9ae95149b6284f38b5190abd9c35498373b8f75a8a858a52e57e03b64dc8c524a3174458520046029082967e9eeb84a0fbcb9c489b415848e742da513084de96abd47d6d6b62e6f43e4f774f5d5d7d2874b9613743b9773fce1017cd2712c06407118405bdb80a5418cde3fe8fcb7bffd759a79e637a8bebe86db01d021da68693d13dbf7c1670083b9960fd6633239d7a665cbd7d029a79c4e0f3ff438831c71ce71553bc8cda9747a1ca56fbedfd2766e324ee1ffbecf115f6405e024c760fe425e337cab9b37deae5f87e9e3700c7b8130fb5ca85cf15fbeef367c81e31a602ae8a466e4129f2b7d347db1d91e5f83aceebf7cf1fda3f6c14401688bdbadaff9762a63a6f3aa70b0595da71abb5f5ff1733afcf0438842e34234bcd7ff9933cfa2db6eeb3f0016e4fefbef67f620087ec1fb873ffc81eeb8e38e24f4440cc17c9d7efa69b1411385a4d8fd3f3697c102834f22c5056c1fdffce6191c2a322a4c6421499eff0be72bf42c0fe0d54d37ddc480c084f1aa7764d34d3765369f238f3c927dca707ffe07ab44a1f0537d2500d522941d04e1061f78e0010ee99684d42c2d53a74ef5d6c499b1f5b7129d696f6fe7cd6df81e84e04da4b82034eff9e75f40c71df7e9926993f77f3dcf0f66c3dffffef774ebadb75222bd2f071d741003bef7dc734fea2b194eefff01160248f0b7bffd2d25d237b2edb6dbd15967cd64e6e4912347526fc840dfff17aba7923c51c771f3f5b60c27fbaf346da9bcc375fdef2f49ec3fb1ff9e94ddd3b4a5f2962a8b19b0c289cd8d0788598079beb725acb8e1f36189631ce1be84cf176b475c31cb0ae72dd5f6a8bae3186d54be72ea31af9bf5562a9538a6de90b88e2a2a6d9cf686e7b5d43847e96da9f98cd32eb30f3d7182c5da91d87ff1fa0a4962ffd56dff6d6dadf4979b6ff736f1e6f99be3b2890e808000a720fe46bf3ece63ad920d7d0dd4b042ed708303bf1c08d759647eccfcf82b20a5940e9525e1cc1c69bb59affe4819a23bfc31ae49fb7dc6ae503fbacd89ebe601c6c26d76757eae930250038029b666b2b252164966295f8da762adb22c00de2c05beb2b2346a54231d70d03e74da695fa3430e9d41cd4d0d94e270833995838123c13fbf7796668862a00ae98fb45ae6c15101dd1cb43b432b56aca57ffeeb61faf5e557d12bb35f27852102cb8f6db069114907007e516316b09bb9ae621392d07f6af804b0e1522693a2d16347d09e7bed46c77eea283ae9a42fd1470ffb086db5f596545f5fab0227da3a80a225e55adc1f6eb345fe3901b629ce2e2bff7c881dabe0875286c614fac40160e991d7f3c1b0b51876d8adcda17f95881bf1a9ccb7c81a226d0d4096cccc06409c9d0aecd8d5402d4313f1b1d3699ab2d1c6b47e7d0bbdfabf57393c88d24e5beba48648591ab46313f90cf76c971675766569d56a6fd36da71d69f2e429800db2f25bbeba1b6b73d438baea837f994c9ada3b3ae9b1c7fe439ded593490fba3ecc1097c81361c4bb723efde45fa67305a218fb0e32970512ef021c82fac7bc23e25f363acc7961a64b63561b772f560c8d89ba15ba53ef13ffe3c883fa28045cb0c9b2ae509a0cc6426f3fdb465e5b1015a463de253c9181b09d54a96c19c15f2cdc26628c05df80e15ce54f9ce491337f05ea0ef9a07ec4dd67f6290cdfbefbf4ffd29871c72086ffce317fb3ff9c94fe8dffffe77127a22a60084023f6732a6542285f4e3f5d7dfa0ef7ef76c9e938441a1b40034387bf6cbbce9bcf9e69b47865f12499eff0be7b1aceecfc0669e471f7d94ce3bef3c7af8e18713b6845e94d5ab573393db76db6d4f53a74e2db9b60df5e77f804f01b4e94fc13dc9c9279f4c4b972ea5dffce63774f5d55727cc6e31254a7f7bf3fd1f007137df7c3387e19d33670e25525ae09f719f327dfa3e3461c2f84165ff71a49aeeff1186f7924b2e61c6a6e47ead6f048021f818b0bcedbcf3ceddd8dd0a49f2fe9fbad5f1f6dbefd005175cc02175137ded3b01031e188c972f5fce8c5813264ca09eca40deff174b1fce93ecff959681b2ff42ed882bc9fa3f3c9fff8ba50fe749ecbfb40c47fbf70158c50a295661d4713979a3cec5318ca872ca1dd04a15a99cba8b49b13cbd655ca5ca29a49c71f2965b4fdcb4bd21617d2ea5af8574b01cdd2ee438e3da5521897288959655ca21144b139536eab89cbc51e712fb4fecbfa7628ec582050be9d6bfdee13df8ad54f5ebcd790639a502561b08404f9600015ccdb44201e8c06fbb1b6cc8dba131b44da002151e4f010d846d3b6c8ba6fdf3a2addb9c33405e61896c93ee033ec2d02500045bc2184a9b7df0890623184c36dc06934d0be010c08898c5877c308d0213e90f306fa0b1b203b012d89dac9443a34637d28e3b6e47dffdde99f4d5af7e9976da6907aaadc9504a034f84b9c91c83bc7e5bc6cdae1b8020007510309bc52011a2aeac436fbcf90e5df8b35fd2357fbc81e6bd339fc062a44029ba3f80c6d8ba60a942f783ffb1fea4547f35b006045ce994057c0bd5d5a769a38d26d3e73e7f2c9dfe8dafd1c9a77c99661c72306dbcf1146a6868f086006c4ac4f5a4f2ab89162bf4b1a922c0925592012b26e0408f85302dc5aebf1fecbed27a541e051332e71ac276a25219651700aab90e358f1a493befb423ad5bb78ede78e32dca6505406533304ff90c712f06a39ca5f42fe55d5cbe7c192df9700987a61ce995072ca3ad8160d1836e2a88b4508523dc74d34dbd0df9d7e9edb9effa7d5386eab22efaf329fdb60c9091cf0a46be3f0cfb41f123b606780a88ca7583d07bcc6c2560565d862293d3215475dd661ef12f96e1931dd7cd4b9777cf12f6affac3fe4dc20e6ac6425bfb3c6987b42bbc4ef9e033199750790cec4279e974b04e9863c8ed0ffca19d5263807c9974860e3c68df48969ae1bcfe5f7ae9a5fdce2683f008083b71eeb9e7261bcd650a36373176d818de72cb2d19f85348cab9ffc7e6d2af7ef52b3aefbc1ff28653b239125f1002049b1dafbffe3a830b0b81b086c3fd7f6f3fff7ff0c107eca3b06997f88abe11d83e1808fff5af7fd18c19337c9f321c9fff01b24168a4fe14f88b6db7dd967ef9cb5f72dd6d6d49b8d77204fa0b60e6f8f1e3698b2db60cee712b10f3f97fc1820574c20927d2dffffe37ae2391f8827b4aac89988fa953371934f61f5706fafe1fefd9c0840ab0e6e2c58b93fbb53e1630233ef9e493346fde7b0c66d960830dba33a387a4d09c0ed7f7ff00b14167f137d1d7be173c2bbef2ca2bec87a1b30837dd1792ecff454b62ffa5cf979264fd8f2e27eef54ac7bd1c49ec3f5a12fb2f7dbe94f4a6fdfb00ac9e185b39931175ad92baa326ac9cb2e32862a936143a8e5b5eb98a5e4a7acb08e238ac72cb8f93b612c75f487a5307e338e352f9cb1dcf620eb150fe62cea99424f6af24b1ffa167fffffdef0bf48f7f3ce4bd446e8fd4155bb31a99e1b3780c040820e361b64fcad1c7c2968163d9b4371962428df3cb7123da6c86fe72757a61bf7234d38c82fe04659861e0a44d661f192820ac325e7bf042d836fae7e61cc9a832b88af147e009fe1c39aa6d290d5e70740a5733f408e88a414c9c05750040910afa6c019091f336559a68bbeda6d1d7bffe15faea495fa003f69f4e23473653ca72382c1fdfa028548a063c48e78c81d61df6fb8a506c1a7a216387bab35eff3abd87ff7ffeeb11baf8e757d0430ffcdb7b21bb5eb5cf11f09930e6b87a181c7f36539ab9cbe5f8768ad1c8d5c385be5ab6438d231a688bcd37a34ffddf3174c619a7d1278e3d9ab6d9766baf4f23b83f36871504c0c655402e15d32c96becb7cf2a742b6a8d2002c97628906d42893e8d9034a5f48a5f5a850811a5c132a0bd7c44f28e93e56ac71c0e6799bdf7575b5dedc6f4befcd9b4ff3de7b8f14815a60eff9002c4beb91d8b302327eb864b137ce0eed337d3a656a6a384fdc29f2c52bafaeae9ed2990c3dfbec73d4b2769dd66d370052895fa3c00733684ac2af6af0255f3242018aefd19e8114c0cc51fe4903b2cc7648b9963ec65ff811a9cb0fe1ea06a1044d3fe61af96c03cc65fa635d091fdb3aadc96a880fb375c935ed5ba85b7fc8efa759b61c9b6d143096cf8a2569fdfeab9093ae66e0139027c66bcf3d77a10d379c4c51325cd7fff3cf3f9ffa5be6cf9f9ffc62bf0782b903d8e7e5975fa6dd76db8dc68c1913992eee3d3a585f2ebef8e774c30d3724ec423d1000d730961b6fbc31ffea1c923cffc7d3c1f035b4efa9a79ea69ffef4a70c0c52ec9689f4a5803561d6ac593c075b6f3dcd0f4f3c9c9eff9f79e6bf1c02b03f05f765afbefa2a8f7d229509d6ad175f7cd17bfe1b45db6c332defc75e71e71e22f7ac988fb3cf3edbdbc09e4d8954260061cd9e3d9bc12a008b872579ff57fefadfd1d1498f3cf230dfb727fea2ffe5adb7de6236cee6e6669a366d5ade0f93e24a4ff4ac1c5d29f7deab50dade58ff1162173fb8c1b35f22fd2bf0c3581ba1ab5b6cb1851faa1752cdf7ff95f8f24a9ea792fdbfe2f987d3fd7f5c19a8f53f4ef971d226f64f916912fbef9e76a8d83f03b0c213d713845ba586d753a32b5749e3961347ca31ccbc0dfd1265c431a64a1c58a50e28ae546accc50c294aca75443d714c95ea689cb12ed7b117d28b72c72f5c7f62ff89fdc74d5f6ef971d2f695fddf75d77df4c2f32f33880292d380025b6ffebb4266a59956647e5c6cd44b7bac08ee190d26c0750160b9563efb8988bfd18fb2dc7c3ad16ef6a63f69af7dc22c83abfc0257c002babd10db1cbf50d902026330999ca74057523a4458ce314018ba3c30f1f861bb7c669b80a1c60dd5c980073b089197b2d5f8721d5c568ea3a33535d5d3ce3b6fcf21f966cefc361d7cc8813469f258cad80ed7c9a43caef75d8f3b8fb36ebf6b22500c00969a180507c31c381a08069003002e4b97aea09b6fb99d7ef4e38be9f5d7e6502e2bf39ee690835c8d8cbcede6154f9a1589196cac148f3808b35cca3223515d4d86a66dbb157df9cb27d277bef32d3afa98a369f32d36a5fafa0c65525e5eafdf695b816e529abd8acb1296ad12ea6e91e587f95300960279f2d07cd1d7c1d6e5fae01bd567d6054bb2a97171bb217d82f45927a7f03b1631a39365970276e9b27be8e3e355429501d45c056e0c7c33c65a00990a88c4002c6177139d33aa52e393f3ec54a51d3d72246db7fd76f4d69cb934ef9d79cc60e5690083aa6027aa5809bf17f83e57cf4d676717cd796b2e4ddd6c636623e0f097fe7c95eea5e6aee37fe3c64da0e52b97d3cb2fbfa26ddfe2f6000ca96c396096421b4cb63bd2c77ebdc60b5e013731908a945ff199b0743bb0696afa394967b1df517ed37c692c6c7cb6c1be25e58abf0daf79645c27f3be026d11763f527e583671295456fee0193edc68b7ef5fa51eb966805b795d1106415d07fb3edf17231f98b31c66cadb6db75d92f55f7f5fb56a15fdee77bfa3fe16b04924e0ab9e0942df2d5cb8909e7bee399a3e7d7a411016a4d8fd3f805ce79c732e3dfcf0439448cf05002cccc97efbedcf7332dceeff4ba58fd33ef887ebaebb8e7ef8c31fd0dcb97359d713e91f01080b6c42adadeb69d75d77e5cdbab00ce5e7ffd75f7faddf0158000f25ec6e3d97d6d6567af6d96799f918ba0b89abab9276cd9ab5f4873ffc8119f7de7efb6d4aa467827b4c8433861f0158dc94e4fd5fbcf42208517af9e597d145175dccf77e890c8ce0fe043fb05ab26409335e17bbf70ecb707cff0ff0d539e79cc3f716890c8cb4b4b430131698dcf6da6b2f9fa5b71aefff93fdbfa165ff71cb8923c379fdef69fac1f2fc9fd87f62ff85a427f69f3af3cc33cf0f4f5ca189ad64620a4d6aa1efc5ca2995d64c538e42976a6b29e98db4e53892703ef37a1ca52b765c6edfcd312f3447e52c22bd399685f4b8541bcaa9ab2f9c879c2b6721adb41d51639fd87f62ff71a5daed1f2f926fbaf1af347ffe423f1c9d6cd69b8ba1a537ef1d1de60adf7d661337005a99ed154082d4650204841da55b5bf1d775f3cedb617b33c378e98fa9d366bb1cdd96705bf3b049ba4e7f9c8d7e4bf9020021cbca4b236d445e09494666f996020709e82a4421a3c7167d02086434edbdd7ee74ca295fa1d34efb1a1d72e881347ec21866bcb2f8a380273cf61cd6501d04ed0c8382bacdbc3781390638382e439ca8abcba1050b16d3ffbbea0f74d56fafa115cb57808f060d22662472890133a44149f86b5bf9fd536391a274aa86fb84b148676c6a6aaaa369d3b6a0134efc0c7dfbdba7d3111f3b8c264e9e40998cc5fde17eb98a2fcbf2415c9afdc8b27c804c1849e3eba43f4706c08d021d24d7d0ab3c5d348bd51362a93caa2cc7ef2b80214eae8b74044666e671bc317439549e4ca6eb030cb95c0ddaf3c766000158e1b1aa087ca50af2fb25362aa037b1514b86186367eb63d89deb9200bf58871964a74048e3c68e6786a3279e7c92d67a1b290cbc7205e4a64533a11105f6a6f424455dd94e5ab868111d38e3401a396204979ff3e6279556ba6bf6d767abd3e714d04bb5b7bebed6d3cb1a7ad47be1b5766d0b091b1a72a45201a8d3d57ac1e049327c1e519e0f14901199fec4980f1fe4a9c70ee03692f195f126630c0c7f46867f75dd7c9bf79906ad10c855fb40a5e3a47c95f85acbcad35732f229f0a9f66b960ec56afa6d5da66dfc258abe7f09ebb76dd403b02203de7c5b4e31e0a7a9a98966ccd8df9b9b0c856538aeff8b3c5dbffefaeb2991c12bd8d4c0aff2f7df7f7fd6efb8f7ff00b58059e8473ffa51c2f2d1cb824d0eb0611d74d041bcd1315ceeffe3a40fb7212c7886f8def7be4737dd74136f1a253230020620005aa64fdf87d2e954b7eb43f5f9ffce3befe410af890c4e81ff78fae9a769ead44dbde7c5ee2197a2e65e74182c7b575c7105fdee7757f57b58e6a12c08d10b9b9a3871226db7dd767c2e79ff57defaffda6bafd1cf7ffe73fafbdfffcee399c8c00afc0cd8dd10e26d8f3df68c05c21a6eeffff1ccfdd0430fd185175e9880afaa44002a0643eff6db6f5f95f7ffc5f296bb56c42dbb980e47e54bf6ff92fdbf52e5c4394e9effcbab2bb1ffc4fe7b923675d659679d4f6548dc813337b7c3690b75b094a38932f2b002956bfc9246f2c77146e13492b7dc3ae33857b35de17a0aa52bd636f338ea7ab1be174a5f4a27e2e8491c89ea5ba9f2e34a25732ff9e4bc795c4c2ad519335da1398d93df4c5feed825f69fd8ff60b2ff0f3f5cea6de8de42ab57ad566943e979935c15a8cecb581a75e6d5e1badde62eaf3c012869e62a57a7f7cf7bdf539ad9ca4f6f05300a016e0910418e5dd7801f99371a14318696e587c3f28159d21769b34e17162ec775bb8d93395e8e2edb1f2f2b34a629152e306501cce07a1b7e35dec6df01f4b5af7d854e3ee94bb4dffefb32f0aaa60640b39c97ce556c502ee5cd814c842adba2d2f82bcd7ec5d821dbdbd075e999679ea34b2fff7f74fb6d77d0ba35ebbc3234330f1011966aa7aa4e43d1fc9080ea234c3e6a1c1578a9b62e43db6eb7051df79963e9eb5f3f990e3bec60da70c30da9ae36cdc018068f61fe296c1f0648caec8c153107d4fd9c02c0694096158c8dabe74b95edfa73a274c6f5411f4e2e4b3927abc055cca4e468562fc5fc06569e1c83af746838c1d7382e335d85db53ee1a52ee5ad3df12d63d06165a01b88ca79f2c056c129be23949a931f455d425e1cb43ba49de4bfe4c6d0dbd32fb7fd4b2ae85cdd73526dd573fd5080dbe0a808d2b57aea2a6a646da63f7dda926a3f4507da2816f3e488f11628e6f57a3468da437de98c3e0086e841580a04cc62ad30e2da34d79a0442d696111a47c9d60609706a446aeaf3a8f9fc608e14a86bf92724d263f2be42fa53c47331bcadc703ad66bd7f7c328dbd6653bc618d83a9d5f260536273e537ca9a3eb92f0896405004033ec62de7d8901505565380c7283591dfa910369c48866ea368fc370fd07502401600d7e59be7c396fcacd9831838fe3dc1ffff5af7fe58d91242448df080058604838e28823f83879fea7bc32a29efdb049879083b7df7e7bb2c93cc082cd53b0e3ad5dbb8641138d8d8dc3e2f91f6c3d08d793c8e015e8eef3cf3f4793274fa6adb6da8acf95d219b0dafce217bfa0ebaebb3e0977da07027f0e10117cc9942953aad6feabf1fe7fd6ac67e8b4d34ea7175e783ef871562255218b172fa6e79e7b9eb6de7a2b7e2f65ca707fff0ff0d549279d948059ab48f0c31bdcd7e1073be3c68d2b986e20eeff8bb541f2c979f3b89854ba6698e90af9f438f9cdf4c3cdfe0bb555f227eb7ff2fc1faeab501b249f9c378f8b4962ff89fd4b1a0e41183753b14a0a2966a972e29c8f4a536cb0c2cead54df0a294aa1fe15734071ea2c67ac0ad553a85cb3bc3879ca990bd3080a5d2f570a194ca1718cea5b29838992a8b471e6a4948388539f99b690432d24e5e87554fd85ea4aec3fb1ff52e7079bfde3f8adb7e6d22d7ff93b65115e4c6fb2fbe0a7544a129a0dcadbdcf70150b8e4aacd76a987d9a7f4a63c33bbe872f27491820d7dd2697c70809c33da2b21bdf05dd878a47ed7cde780b2f475cb1c037d2da5c15f7e79000ca4d3794c30b8c665ea76fbe0015c73f3d96d520690cb076be0d8220db220cda683239b8ff17d9f7df6a42f9ff405faf6b74ea3dd76db99c66f308610010c41020154b2f346086c4296e28b42d5f8289411c5213672fdb148d1da75ebe96f7fbf872ef8c9c5f4cc53ffa58eb6ac7721a3bb87f977480f9f9a36c634b9ea98ebf4c60a0009c7a594a2346236a1cd37db843e7ddc27e8cc99dfa4830fdcc73bde981a1b6a75684187d25e9fd3966688e2909629f22b0adb874591fd2a6847560016642898a7d308ab89f950a0389e09be9e43881c843993396640a0c57db2fc506d8af92ce7aa344a97c002a4422d4ae8bd20e49c3732a920541cb7c7d1ba1e637e2af10f03266c27c461e2140b124e6abbd7403852a714e395a514c4f581827a2e00a2f2c67cb3cd36a3752debe9a597667b7393eb0ec022ad1e960ac129738c3231c78bbd8df3edb7df9a36de6413ef528ee7c82aa040960fc0d2214375931aea1ba8c3dbc89935eb596a696963162cf44d422fba06bb940f2ed2fec1d2e117a3d65fdf5751be1fb2b4eef0f510735ef7be5b79e921e2ab4c3f29f508b85542a4fafed8b033d32f0ad84ad24a58424b5fb3f3f43cf0c502128bba7f7323c641f286cf339b1a056d577db5a8bdad9d0e3c605fda70ca642a26c365fd4f00584343f0521dbfc4c70bf54d3c9f25e12520e139c72ff76fb9e5563aefbcf3928d913e1680b06a6a6a689b6db661d6bda17cff5f4c4a3dff0330316fde3c3ae38c33e891471e493699ab44c4af200ce40e3bec4023478ecc6333860cb5e7ff471f7dd4bb6f7c891219dc024015186a36dd74539a3a756a3716ee70da73cf3d9741c909f0b3ef04f71bb0ad69d3a631382eea071490e4fd5f50de3ffef10ffac94f2ee07b8944aa53962d5bca6023dcb76cbdf534aaadad099eb123e67638bcff7ffcf1c7e9a28b2ea2152b565022d525f0c3f0bdfbeebbaff77e383de0f7ffe5ea68295f9becff51c96b7d6dffc5ca0ceb52f87bb2fe0fafe7ffc4fea3eb2e943eb17f8aac3b4acc6b3e002beea41452985295951a8442831975bdd04084db1695ae54d985a41203899228452dc7904a9515555e6f39cc729d4539659adfcd85a49451171acb3846d79b6d8eba661ec731e6a8fc72ce4c1be57ca2ca32253cae51d78bf5b3587f8ad59dd83f15ad2bb1ffbeb3ffce8e4efaf7bfffe3bdc07e22802ae8b26d616e91fa2d8349c8a8db2a505fb797fea1fa511e4030f89ed2e1ba90d2077311f9ac2e6893006bfc39d41b3e02841080575e7b914083042cea7e63276029b2ac82bad16d8c55e7f2ea467e013ee46d44b90a5881076652d5301310001d234736d3b19ffa38fdf047dfa71907ed4f63463752269d62c015002bfcd752000fc532a466c8b274411431ef61f406759b1cefbf342d5fb19a6ebcf1aff4ab4bafa0f9f3de475442ef5a8a720ec6cd2187b2baafaa3ed13735872a6020d233578ea5c670e4c8463aecf043e8d4d3be4a9f39fe389a38612c35d465bc4dcc1487706370939737cd61d678e499bfc81b453ecf7d54c81a2a04bc32fb1b9e2727a700545dfa97d0981b6c12328b15403ad92e4e9fedead2f92c927081298578e37991971b4ad7150824dbd9c51b5baa1f299e936c2e1bda20200ed506c015eac60980f4c0aa15d727f4a6dfe817f1e72178291f84a053946d81bd916627e3ab0c24245b5d87edd4d5d67b2f42b7a0a79e7986162d584c1462afb2f4ff58435c352fb69de67201005abb7a35fb8b8f7ee440d639b2c1cc6493551480a500600cb252664c23478ca6679f7bdedb5c9e4f1c8a9354bbc90958bd48fb1bf121a6dff71903b5a43488357c6f42463e06ff09eb9febfabe8dfd4a8819d0b2022095ad7d278e4d1014f49ef3e8f930afe12fb35319e5caf88a2f841d68c2bd804150d80a35609141525edd294b81b51ca953ead1e55914ccbb151a1be993290cc4f212a5bd39cc651d6f732debe9c596b4fd0edbe46dfec491a1b8feaf5ab52a01600d11811d3efbecb33cbf7befbd7741bdb9e28a2be9ca2bafe0f06289f4ad609d7fe1851768ead44d69db6db7292bef707afe07ebd029a79c4273e6cc49c0575526988f77df7d971e7bec319f3561283fff63e33861c01a1a828d6684d2fcc8473ec2e0c1285d04dbd5af7ffdeb047cd54f02d03f98b0708f5288816520edbf90f4f7fdff9a356be86f7ffb1b878806cb5222d52d6d6d6d1c66b3b5753dedb3cf3edd9ed54d893a3794deffbff6daebf4cd6f7e9341f58954a72c5ab488f5146161210379ff5f4c92fdbf64ffaf58ba64ffaffc32cdefd5f0fc5f6e9ba3ae99c789fd27f61f55970fc02a5458a173719d467810e238834a9d45a5697aea50a21c619c6b71d217aaa758ba62c651285f1ca7d2d3712ad55653572aadab90632b25e52c82c5e6336c80e5ea5b250eafd478996594332e89fdc793c4fe2b97beb4ff55ab56d33df7fc8bde7c732e6fe6e774d828b92ee0023ec6479881f479b946285336e98d3a1c1d661047ae064291799363593e8b95257519e9a59fb651af946d0213c2f3c1200823dc565c7fc2f5eab6008823be8a74dfb92eab3b3adf368e19b8462af418be38aeadfb8843877f69b7edf6d3e8dc73bf43a79efe35da68f264aac958cc0e25000bf2fba480570c7ad265042d35fa04f0886b193ecc65008a6be508802ac51494e29083afbe3687ce3fff22faf30d37d3b2a52b48ed9d2906239ba980903e472980265cfdb114804a5119a9c06f080f86368e1cd54cbbecb23d7df7bb67d097bffc05da7acb4d6944430d63d4eaea6a59070008015003fdb3850549ffdf62062205845163edf8fd07404fcdafd2a36c57a70e8546d4b26ead3796b55c3640555c87662b62e6364b15954ea575d9606dc872f847eeaba5803bd96c1709f710405cbe3ed996cf8ec5baa0198e548849d707cd40331196d0077331db9606f668fd48a553e4ab99ebe809eb1d3fcb75e4698445547e3115896f9b96aad7667dcf2976302816ebbcab433afaad23dbff2e3a46ac03cd239a68b34d37a3679f7b9156ad5ca5ebb01500ca557e429599cb2bcde17a89e6bd3b8f76da7947fe057d1a4c66acaf124633a8935b25c5218d0b1011b19e3536377a2f643be8c1071e669d77d9b688755ee9a73041e9b09414f82f9ff18f02d09230ec09c314eb9739863aad80a884694a18a814a82ff00dccc645c61a80bc528e9c477eedafd21a0c6819bed667f9d3ed0ee63100b6fa80576993d176b3ee6e69759f453f7c3f06df685b7e3bcdd0affe4c623c1d01a7e90bde97f11b8cf3367ff664df39dcd7ff84016b6809d8ad66cd9a45bbefbe3b4d9d3a354fb770edeaabafa64b2eb984bf27d23f824d7db0ea8c1f3f8141588518844486d3f33feeb31e7ef861fac637be41ab57afa644aa57303f77dd75b7a7c7e3398cd8507dfe07d02c01600d1dc13dce33cf3c431ffde8611c5adc1400b410f2f4da6baff57f689048df0b42cd020070f0c107e7b175c691e1f0fe0f7a79fae9a7d335d75c93dcab0d2201e01e6b07de85eeb8e38e9ebf698a9d77a8bcff7ffbedb7e98c33be49efbcf30e2552bd821fe080f9f1a8a38e1ab0fbff72ea087f97e364ffaf7bfa81b4ff52692ad5359164ffaf72a9b6e7ff72ea087f97e3c4febba74fecbffb3548793fb936a4a70652a9f89be231d2c4491b95afd071a13ce5185429094f5ea16ba5ca958dd66265c4395fea9a595f6f4bdcf1ef8dba8b8d7ba9fa8b95556edef0b572f4a7d0584439f59e4a62ff419ac4feabd3fe572c5f49f3df7bdf0709f06267e5b3a4c8c6bfdf43230c9f7fd342015b9545c1783040c508b165876e700410c0801b5d86e477745ba43d26100a22c0040104481fd2c208a3998cfc31d0e5499b8519c6ec830009183464841f74755db611820b6d17161cbc0866d08115001a04ac25c0251562cb6636976f7ce3543afc88c36984f7b247f554b59d34d843956f93a0132c0aaea98f06f7b8ea1ab3099962b99acd0c781f15062fdb95a5e79e7b812ebdfc0abaffbe8768cdaa16af8d365fd71d67b62685dfc179054c51a0230d38b14885df4ba9311b33ae993e7ad881f4ad6f9f4a33661c488df519aaab01800f802b05be2166284a51ba26c3401a80951c57cd9b430a94e14847014e5213c561e80036c1b8777576a9efae0236e5725d0cbeeaf45e74aa107faa6d0c204c6be62b47e95ca79756f2b93adc9de3bd746b6f6fe776a4d319fe4535d876f01d2fe4706c8258d07ff4a1d3db98e536a7bd6be91403b6b28e0a4f4779c464aecfeea640466a2e6c1e59f5afb7c4097ddc923468bd270cac42e734f80f35e718c4e630bb943ace29fd61709ca3c6c2b76b228df2d4e539b4c7ee3bd1673ef309cad4204d8e3269c56225d918d8a66dc322d7b07180f25ae99fff7c9096797e4db1abf913c2e23fe4f069ed4f748c5057b37581a16dbf7df7a2066fe3c73158af788a6dc5d4a6ec4dd9950086244c1f3966b84aea8e85d37ec30c0b68b254b99695178e10fe0c9f9c0632bada378a3f73c55f9b3e5efb24061872184ee59fb83fdac7ca39cb0a00a4a8438060e217d917ba41585aa434d9bf64dea41d394967f87d01d33270d1ccafdb22a10c1d01351a2060b4e7bd79ef7b73bb2e6f0e553787f7fa9fc8d0913ffff9cfbc8967ead2134f3cc11bcd89f4bfe0fee0965bfec21b1e90e4f95ff5f1dfff7e947ef2939ff8e3924875cbdab56be8b2cb2ea30f3ef8c03f37549fff13193a0230c0adb7decacf63a65c73cdb574cf3df75022fd2f4f3df514dd7befbdfe71f2fe4f098011b84f03103491c129b7df7e1b5d75d5550c9e1b6eefff1f7bec717afdf5d72991ea9737de78c3ff3e10f7ff52665c49f6fffa279f4825f65f69d9c9fa5ffa9a595f6f4b62ff89fd8765a8d97fc53b657d611c71d217ea90992f8cba2b96b658d9e5d4153e5fec5c5c2957598b8d4139ed88aab7d279e989c42d33ce62d417f51632beb8f59563b871ca944dc538e97a2a89fd774f93d87fef4a4fed7fc18285b478f192207c1f0521f4248c9af4ca073c19e1b1c275308849eb286ff8eb4d7e536fc506cd1b60f39aa38fd3cc96148011043c45864fb1344b0c83c4388c9800397244061040c01002f4e0bfa18f6bf6438306184080b235204000620092f861c06cdbaf43c6893420c252d82b0613a5ed341df5f1c3e8c29f9d4f477fe2681a31728406f8e40c90861e07caf7538e5b24c48b1b3ab024ad0a3708c8cfaa552df4975bff465f3bf5dbf4f7dbefa5b6d6360655f87d55c9fd79e1d06b0cbe4a31e00ec00909c78791aaafafa19d77dd9a2eb8e05c66f2da69e79d38a4625a871a6c6fef60e0078053609cca66730a7a64307ad93a441da0170ae4e13078a7a3a39dc131dc0e3db62817ba944aa515884787f943184022057c01c04c01ce5cc588d5a5ae2124215ee2a3edb535755e5d399ecf4c4d86fb8d6b994c468186bc6bf85e5b534b4e36a7f40fed77b20c58a90188ccfb279b02965278fe0e505500cc717438371d024fdb0d03cf5492a12196523f61b9c218a735cb9980fa140399b24901ee75135b856b44b6c6867afad2974ea48f7ce4100ec9a96065aa2c9741572af425b3625160d27cc59babfbeefd073df4e02364f9613bcbe90f6cc2a5adb6dc9c8e39fa48cdd82420a17038d60068e5334411f9fe4bb52d0851e803390d5f23c02701958a7f419a6ccef1bf93e1f77c962ca2c047e9eb228e30661923e0034d35b05098ad1824a7dbc4be54fb5133ac541056d2ca2bcff79f56c0a0e5b30eeaf630688c427e9f0c00999b1f86d0115bc147a7c11ab564c9d2eed335ccd7ff44868e3cf0c00374f1c517f377d80c8ec1a8b074e9524a646004cc08975e7a1983b14c198ecfff18036c327fef7bdfa5f7de7b8f12193cb270e1423af6d86399e902325c9fff13193ca2fccd353e3000e1dd2ebbec72bafcf2cb18a89c48ff0be6e49c73cee1708490e4fd1f3110190ca500b926327805ba0df6b2f3cf3fdff32feba8a73218deffe3191b20d78b2eba8812191c3277ee5c5e0b21fd79ff5f4e7966b9c9fe5ff134c9fedfe05effa3ca8e9ba7a792d87f796526f69f9f6630d87f9f3260c5558842e97bea340a95231b2571c4dc6c32f387af87cf173b17a7be4aa49802f5d4e87a92bf589f7ad25f487f399c72caadc479565a5731e98d7a0b4962ff54f47cb17371eaab4412fbcf97f7e62ff41ee6d6fac0272bc45295cb05e0207fd35fc052460840b94e5297ebfa9bf6c2f0226c424e91c5dd0ecf0fda90cdfaa006c929600106786986174b97a136fb6d1f54e00314b4389ae525fc31251cbad0520df4cb01d04342360a68823468c132c68aff7a45d7d4a6e890430fa0f3ce3b97f69abe37837cf42b100dd4098d4505081d97c14c8e0637602ed294ed7269e9f2b574f51f6ea00b7ef24b9a3be71d1fa4042628865849784757c68714f8ca02f80ac008c57e854400c48c6c6aa2134f388ebefdedd3e9b08f7e84468d6ca6b1a346d0fa96b51c220c4384f9a8d7e1070198ca64d2be6a60ec04600556298719ae3aa9c37b01e66a362980a9007cc2982a26299bdadbda384d4e33610164555757cff93bb39dfc1dbf5eecf2d2d7d4d6f248b47bc7f5f58d1c421060b0cece0e1d0e4f01835877b25906d90988cfd5a030e465bd73f26f7a056cc463e89581b2010c03200880308439c477c5fc93d32c60f271f8d397eb4e7f8a007cd8ae49b12ea9b09b8e66fe72b54d2a908e1f4ad314c6e129c63ae803ca98307e0c7de18bc7d3a4c993d5183a590df8b18cf908c0a1ead861bd5abe7c055d77dd4db46e5d7be90eb8e1fef0ecf29c1e79d4c7a8b1b9415f500c6baefc7303463d2209a198ef336cddae94a5804a29c33f8a1f543660fb804dca5b4bf3f5ced64049f6756639c2bc67f85759bf730204f4048048a9d3672bb4022017196d72b5cf161f2f402dd2eb84638487750d3de836b486ff14962bb9068096adcf0b1814362f65725a5d0fd6a8850b17196353990cc7f53f91c123f7df7f3f03af162f5ecc9b7a09cbd0c0cb5d77ddc9cc1fa564283fff0370fef8e38fd3a5975e9a000207a9c0a75c78e18591e1b106fbf37fb2460e4d4128428003162c584037dd7413fdbffff71b4a64e0e5c73f3e9fe704329cdfffcd9f3f9f7ef8c3f318b893c8d090db6fbf9d7f08d1d1d1493d91c1f0fe1ff77277dc7107bf174b64f0c88b2fbe143b6d6fdcff572ac9fe5fe9340369ff85ca49f6ff7a5e763992ecff27f61fb7cce166ffa99933679e4f0328b2891525bd6538e1012b47314aa1e92a6d63a17ef795b3e86d896a7fb13ef5c73c57ab0c54ff4c47536a0e064a12fbef9df2fa5b0683fddf70fd5fe88d37e6a84d782b6089920d78ae3b6884bf316e6ebe9bed77436d16d000c4d6e016eea73e175ee471641be0022e5736fb8d3c1605ac2c00ce98e1b1703d6d84b08abab910b09865758ffbec8323c8409c1b6d9263698765944b969517a690413d299bf6dd772ffaf105dfa7adb7d986439c61e432b6cb6c4caa1f1ac466c21882d38a7d2a0294c521f52c3bbfaf960a1dd8d191a3471e7d922ebbec3774fd757fa655abd67845a6037626d2cc5a6ed06f8593b119c065d969636c00a24ad1a69b6f4c5f3ff52bf4a94f7d8236dd6432358f68041f145f4fa7530cae02db55737333757575514d4d0d83cd6c1dc652c06f5229c04ac24ea52ab71918863e753273558e32b5191fec96f1ca93f9e6506929c53005c0164050690df4c2713a9df247895cc5ce84af00d7643bb38a25c8fb0e20565757077f471bc1de05fdc9646ab8dd60e4caa4325c2f4059293bcd65732f5837d506a502a929dd487b7d507ae470bda21b64ce631ff9b6fef48f0a00a4d9cd6c15aa12c7d20ecc258f53ce6143e1709c32ffaeb4559fb3e49a0a5d3a69d2240653bdf4f2cb1c1e52013895cea76c61c60a2c90f5d61b6fb461d1a285b4e32e3bd2d65b6d491c0154b5d64f1b64d3e13b756c4f4bb71bf6d3d0d84cfffdeff3f4fefcf7553f184495f2dbe983979422e8fed93e9055f49d34300a7fe5d71ce27fd8162dcbe7f8b28db594f3e7149355d8d7986bb46dfc159176882fc7957426e33365897f337d97f4c7d2b626df5db159d7cd0799e93e91e15ffd76eafe8befe66bbaaf14d107f1db3eaba0794fe19d831fd868a329b4ef7e7bf7ab7ef744fa6afdc7a6e4f5d75f4f890c3d696b6be3d04b8f3cf208cd9e3d9b121978c19cbcfaeaab74f4d147537d7d7dec7c43e9f9fff9e79fa733cf3c937d4f228353a05fefbefb2eb5b6b6d1aebbeeca21bc4506fbf33f427fbdf452fc4dc944068f00243067ce1c060a84990813191801a31eecf0a0830eaa0afb1f08010b0d42f1422f13193a827739afbcf20afff861b7ddf2d7c98190bebc2ffcf6b7cff4d6ce472991c125471d75146db1c5e6345424d9ff2b2cc9fe5fef94d7df92ecffc797c4fe0bcb70b7ff8a19b0c2859573de94421d8893b712893b607d557fb9ed2824bdd13e37b4b9548e94bb784439eb5279069374035b54383fbd3daf90524ea4b7eb2b75de94c4fe2b93c4fe8b0bc00173e6bca319982c7fb33e8f2545da60b6930478616cd21be984ed4a36d66501053311832e283f4c97d947a927a7d952f8af71336082bf500740057cceb6f3da978b089fe5b3af68b08739aed227132ca01b16f4434013148009fc1b15738e5c03b0e5fd9b38713c9dfecd5369f32db722db55ac2f2906850048c2bd55ed0c03ac18d361a9d07651e02b3760ba110c8beb5ad4d5e5d2aad5ebe9ef77de47e79fff33baebae7bbd17d76026b235804c013e14eb8de4d3f3c7cc4d0aeae18770b35c6a6caaa55d77df9e4e3ffdabf4f18f1f492347d45353a3b719e976797db0789312ec58322f004f65d235febc80ad8ac3ef791f7c6f6b6be73a017652200b620014423566735dfca21de0aadaba0cb57b1b460060a4d30a5cd5d9a1f22084606b6b2b8786aba9ade1ef0a3898f2d9d600fe02cb5557b6cb0f85c77a8b5079e90cb309a15d005b01b4e5188011d2a1113348e7e4f80340990aa998ed0686c1090074f0119d04288cc757423852efdbb2654503097b542659ddca8dfa280d0fd4dfd1c7cc32c67ae530f8ca62309ca5c61b8c47184f57ebbda5c649c647f90e97f5eb739f3b8ea66d33cdf301aeb6490388e9588a50cc556103a1d5028842d23bfe7e37adf65e92abb08f5641e63dc550a64b65fb70b82713278ca5238e38ccd34b053252a026c7075fa9bc3a8f453ecb933a158089242f5f27631dd1402d3fdc9fdb3d1c2bc09bfe2f4ca43ef1ad560088f5fdb1ae4f8d6bf7340292b5747b2c1917cbf27dab0f82b514804cfc27ec8fac2044ac0fbe0afb529319d1f08db6669833efb5badd8719fd54ec730168eb8d37e7e485452c25c9fa9fc8601484f779f6d9672991ea11846e43a81666788c2943c5feb11179de79e725cc574344fefce71b38ac66215d1e8ccfffc91a397405cf828f3efa68c206596502b6ce37df7c93fa4aaafdfd1fc2f1de7df7dd94c8d0941b6ffc33af933d65c28254e3fbff59b366799fa72991c1277d7defd51792ecffc53f6f4ab2ff579924efffaa4b12fb8f7fde94e16effbd02c08a1273c3d73c175706da308bd53f90c62552eef844d59db7b1d4070b42b1fe0eb5974ae1fe543a3f51f92a5d1ca36cb0bf24b1ffdecb172589fd179765cb96d1a2451ff821dd78231da011cd044479001f2b3f6c95613fb271ef1f1b7d728c0d7f5ba74b85c21c8637f0f994abd856e4630aca07c02aa7c30fe293d22032c7003774035cc971681c5c0a81b3749ba4af92861997ec7c981400026cc73aad0f7ad0ec4ab57535f4d9e33f4dd3f7d99b6a01ded1401d8b721ce84e638f2a1257c02efe3f149ca2f9f317d2b57fba917e76e12fe9b5ffbd49b96c008851731acc9b02bb704f8d728941270c6ab11caaafcfd0a11f3990ce3aeb5bb4ff7e7b534d9aa8a3bd8d19ae3a3bba18903466cc28ce0bf0535d7d1d7f4fd764186cc5602c00a1bc31c1afef019c8220140a18ad386c20005a3a342034c54edb2a6499a358c4ec940a4fd6d595f5c7cbc9b16629908f0655210fea04d80309db5a5ba92653e3b30889ae034ce2b380e9399470890865887201fa42c8c314a74d295c8bab81458e625ac2d8a14c663a53cd513ae4ba3e88895cc58435e845ecdf3856212c037f81b1c758f03890cb40354530e5928a6269f3b805a04283452fa888430edab6435b6fb5b9673fff470d9e4e59ccf6461ad40674a2b27965fa121a10f3e1d99e9da1179f9f4dafbcf23f1d6a333f8469c12eeaffa13fe94c8af6f5ec76f498b11ad06905fed068338316adfc7e587a7c8401d009b1f809e80afd10a0561810ca7f0d9feb33ebb9013b1f19fed63fa6e8b963262b0d60b255a17e5daebece769053ac7ce253b55afbac73e2ebc89590936e1e0056ecc9f78b79cdc86faff9f181b21a7ccb7d4ea5fcf15bf0fe425ab56a35c59564fd4f2491447a4bb0d9fad65b6f51a53218ed7feddab5f4fdef7f9f4181890c0dc1fdf8adb7dec2cc2d51e108e34ab53fff279248227d2700e45e75d555de1ab1aedbb5a1fefeef9e7bee49c20e0e71c13a79f3cd37d195575ed9a375b2900ce4fb7ffcf011a116717f97c8e093c1f86a20d9ffcb9764ffaff7f24549f2feafba24b1ff7c49ec3f5e3e3b6ec26269cc090f9f3365a8185da5fd18c8fe8737d77afb6550b5ce775ff6b99cba4b49787ec2d72a914af325f65f5c12fb8f2e3fceb9fe92d75f9fc32f19186062029434b3157f376e1264a39ebf5b06130e91cf96c569757af985b56cac931ff64ea563608d6651514c44c1780bb395c978227509888bebd00c2dfe350a8115b40828404052a4e75a3e8e8025ccfe4979c6471861fcf08c46a831137880ebb96c960e3bfc503ae1c4cf536d7d3da5b83d2ea534534f00d9605e1c2a5bb8de9c064fb8b462c50a7ae0817fd3295ffb265d7ee96f68debb0ba8ab0ba022056202e04b232f3498c600aca981d3f3a7c0339695a391a39be813c71e495ffffa49b4f5569bd188e606aaabcdd09831633813804cb99c4b2d2dadfa7bd6d3a92e4fa7525edd9d5c6f5a3363a19d3816d006fe028c03f62ab0518906767629c016c03b78f105d629664dcae6a8b3a39329e1bbbc3a101ab0beae8ecb05b0abce3bcf23ecb83e208beb25351f1d9d1d5c17be67bd76ae6f6df1ea9110845ddeb55a3e9fcb75711b00f8e2b1e0b248874954f326faa9d45a858a03ab13da897e5b7a6e88fb9cd621f306b9986b984b869d6b509ca5f5014c7796b0d4a594aee1d8251e131596d156402da2806d2eaf2e9ccb515d5d9a3e77fc71b4f7f43d189467a5c2f7001683afa45d1c82d0566c5b4b972da7871e7e9499f7b23947eb600c209ca5ca4118ca695b6f413becb89d57a666c60a81a5c4de4d6b564361e5019c24ad6505ec7d0a14180037650c2d2b6099f243f901dc264059ca0778996c563ed0b4d05aa3cb465996d4817993b930fcbdd8a9f8332923efba1580d2702cd7c4179af5fa40577d0c70555a8727f5c7ca0a310e1a7d59d7b29ee6cd9b4f7d25c3f5fe3f914412292d005ffdf2979750a532d89effb13977eeb9e7d2bdf7de4b890c2d019bd0a5975e4acf3df71cf58554e3fb98441249a4f704f7fbf7dd775f248bce507efff7d8638f7bf701bf4cc02bc340c0be77cd357fa49b6eba29effc607fff0fe0e45d77dd45890c4e696868a0de9264ffaf7c49f6ff8a4bb2ff175d7e9c73fd2d89fd972f89fd1797de9c0f3b6ec2b81584276f30bf3c28a7ed3d45c555326e952223c3df0b95d71b73d717f31f6c0c16be2e12d5e7b86daa740e8aa1598b49b5bca04bec5f4962ff83d7fedf7a732e7fcf7575f92c27a4f3e434db89a45561db6cb5594ee4b3a590090810108191de32814e1a682180256623c2390d483041537e382d23bf63b649985c34f800601d337c9cd97f0657c95cca79cdb2628e9000be5ce3d81f430137789f9c664acae9b0617c59c0651ab480b463c68da133ce389d36dc688a62fd624010c715548c3caa460dc6cabfcde0906b96c166a3d9a8181a6509400c6c42696aefc8d26bafbf493ff9e92574e6cc73e89959cfd1aa156b303a5e9d190d42525d50432063c7482b7fee2d4bbee093a3b1e347d1273e71141d73cc91b4f1461bd1a8914d3cee08f5974e01409261a01400523806f0a5cd7b61053007c2f049584130240194b3766d0b8f535b6b1b83a8a04b0c2073108a50316a01b805600a7ea187726b6b6ad5f5b6763edfd8d4405d9d9d0c8602b3155e90a1c14d4d4d1c06112c5a60d85adfba9ef5abb6ae965ad6b7282097979eb4ae00100546b28ef64e9e475c63e0108f83edeb555d6d1da7c7a0f3b0d86a2cc1b294cd766966ae1483af4407d29974489f06af7f3745fc03b33769309263840564a08e956ff396adfaceba6691025f2903d036653cfc75ab4cd9c9d83123e9a493bfe4fd1d0d03e3b2006ab3ed7cff6c022f73d099d64e7ae2896768c9e2c55c9c8483ecde31ab7b4f2dd59fc6c606da7db79d3c9daa0d18a02c69a0065eba01b8537c8af81c09efc7a1061de533c4179a802cd3cfb91400914487a47f8e715dfc93abc1ab02da12bf9cf7604d6ac384814d26b395e18b05bc25f3671b60b0a0207d5e8fb3edff0d80535cae845cd4655bb256b8012b96abd9ea04a86b8264a5bf32d2b6f61b26002b59ff13492491fe12dc073cfcf043f4faebaffbe786f2f33f183ec0fa851f692432f464e1c28574f1c517d3ca952ba952a996e7ffa1f2e23a91440693606df8ed6f7f1b2bed5078ff376bd63374ce39dfa3f7de7b8f12191e02b032ee85ccb0e083f9fdff82050be98107fe95dcd70d62d971c71d8b5e4ff6ffa2ebeb2d49f6ff9424fb7f43f7f93f6e3d95a449ec3f90c4fe8b4bba540273d3c0944293143e3f985f1e94d3f69e28adf9377c3e4edede92be98bbbe98ff4265e66dfa5590bf9274e5d8465f495fd697d87fefa78dca97d87fcfcb145d5db7ae8566cf7e4da593d04f465e09d92600297f73dd0d18515cbde92fa10579b31e9be9ba3c5b83952ccab7110172f8400284ebd3a1a6a44cd9a4d70d62c094d469d62f60272e3b6487922e156a07d2e774da54280c9d0f1ad3f509b397f447c0108a21caa62c18774801765c30ef5829722c876a6bd274e289c7d32ebbedcc643e60e9b17d288e0271f088bac2fb6491a5411f005e919de3eb8eabe786598350b7574ace6bbf95a6d6b60e9a3bf71dfafbdfefa1fbeffb17cd79eb5d2f7d1641e188b3b9d26719ffb4064068600723b900a8d3a0301e2f6250cc8489e3e994534ea6dd77df99266e308e9a1a6b69cd9a355ebf323462c4086fd366358d1a3d9299a2d24e9ad9a7b2def7e6a626c5666629804e9643443a3cce239ad535809a00b602684bc2f7d5d5d733480a6c5960ba028849c01d0871d0d050affae0b5b9a3b38b1a1b1a9879c95728af430075f1fcb8296aa86be0b2b34e1735d4377202b05b591a3403601858b7b2d90e06d0a43470a8d66b1b002d69d67f87c33aaa39576023150651855d4c613c1d4b8792541a0bfd01a0d1ae4dfb401715d3d1ea0ec372edee365bc93daad5f3f0865644bd02a6d48a493e64d151402a865e79e38670818ea3f49899c310f28fc341ba7e884b309601f406b63115929034888b02bd87be826a0ce3e2a6594b996dcc3b3cf4a07de9b0230ea51bff7c2b5fb760678eb2a35cce628632d5625b23276d182cbdf9e65bf4d4d32fd0273f7914b179f29cb81a0049ac4fdc1e647114131333c671e8c01c83b6f6df7f3afde5e63b68c1fb1f68c092cbe11121c2e2c5b53b01e39fb20105ace273c8a74149e2178575d004888a3f22ed8b6cdd2bd3bf99002f069b692094e91b058c25215c4d2095b401c7390d9495f29819cb3b4e6bff4fc65a20fd22cd5a681b0feb18470ec389366ba0574ef747ca71b55f578db01428cb58131cc37fe73da0ebbec33fcc7ef9553afae8233c3bad49d6ff612423478ea4f1e3c7d3461b6dc47f274d9ac49f891327d2669b6de6a7db64934d7c9b84aece9f3f9f7509ec908b172fa1975f7e895e7bed755abd7a35bdfe7a1256ad2702164c7ca64e9dca7f31f693274ff6e66782374f53380de602d74560c3007e4010027bc18205f4e28b2f32bbd48a152be99d77dea66a9773ce3987aebdf65ad6c3a1fafcffe0830f0ea9104bd04ff80af88f2db7dc92a64c9942e3c68d27acae93276fc8406bdc3fe2bc08184ea0a318ab0f3ef8805a5ad6d39c396fd1bc79f368c99225acaf0b172e18d44c28afbefa2aebf219679cc1fd2f57aafdf97f280bd6c4cd37df9c757b8b2db6a0d1a347f3f75d77dd957f18035b878fc2f39a088023788ec10f57de7f7f01bdf4d28b1c4a6ed1a245deb3e45cd6f7442a97b09fc1d88f1b378e76db6d373f0dd64930058bbcfbeebbfc17cfd7f0332fbdf49277afb2983f3896f5b25a65f6ecd90cc2fafad7bf9ef73c1496c1fefe0f7672e1853fadfaf92847e4be1af768f021d05575bf66d1061b4cf07d07fe221d047a891fe1e15e0e3e4404f76e181bdccb4197f11de0a5a120b84f3dfbecb3e9861b6ec8bb9f850cb6f7ffb867194c7e1e3e65ecd8b1fe5ab7edb6dbb2ffc43ce079033a8c39307514ba89354dde23cc9dfb36bf2ffcdffffec73e75d5aa55b47cf90aef397015eb7356fff87230c861871dce7d2e26c9fe5fffd597ecfff57edaa87c03bdfe57da8672cbec0d49ecbfffea4becbff7d39a521280554ec18526abdcfc3d2da7d27a87527dfddda76a6847351bbb1bda801b2c8e29b1ffc159df70b6fff5eb5bfc5f3ea7343b8b6cfa8743070adb94009e2c135420faa899a3cc907c489f4679c2e022e10675da940624b856c0f4e2dbbfaa5085e6421e17a1fbbadf8cbb06a8c035f289c8d89a6c340cc832ae0b80cb47681bbfb8b08c6b127ed0d1a034ff971996cba00c669cb2145065ecd8d174f0a10751ae0ba1cc78103573553017e8bd2a4321405ce9858454d3402d805b803071730a0084c48b162da1ff3cf134dd7bef3fe999592fd0f2a5de5c5a699f7546daaf98871468468d93edd7e5ba02ccc098289622241f337e141d79c44769bffda653637d86c1182debd652269de63211621080a86c579619abb81eaf5d9d9d596fbe2deaeae8e273080fd8099628ef9f0a4f98e3171d994c0dfffa6e44733339b94e0e2b88fc0066b5b5b5525d5dad5767865fded5d4d5507dbd0a33b8de7be907805793b759c661e5bc0f368d50eefab6f5def9260647b91a2805762d004a6a6a6a99a10ac02e9c072b16d265bb5cef5a46e9bc5796800cdbbc973760deeaf25ed8d4a5d20c0a63e6264b015d1cdb61862fe630d3c0140e8d470e3392a909a760b2ad7cdb33ac91aa592cfd3f1fe808481218a858cfd5f882014c88e0182b0326245b6cd36180a16272d3ac59a4c2105a1caad0f1c3813a1c3a300811081017b3a391d2cb91231be9f08f1e4477fced6e5ab7ae2d083bca55b9acd74e289421da04e6b4ff3efb1c1d75d4c71814a9accad12024f2c13d2401305d84cb747c1f667bdf37d964639a3879027db0e843d5074b31a031079db647d2804cd53f6340fcb6b83ec8485ae968e02969a0abe96bc8f0390cb2729c00cce406a15ee13bc5e7922edb35c05952b72bad3480595c9e2e5f18b4c0e6c6e90c064369830f7c953e892f260ad800013cf38e333abcac2be9503e428e9a615e25ac61de9cb93e38d7b72de3fa726fd35bd97d0df55486e3fa3f9804b6818dcbfdf6db8f5fbacb86f384091362e7177016f2418e39e6687ee18e17f0fffad7030cfe79ecb14729917802a6c90d36d8800e3ffc709e13005636dd74531d92b8b460bd96b9c0dfbdf7de9b8e3df658dee0c2fd2842a23df0c003796c03d52660c042fb8e3cf248ea0da9b6e7ff77de798737d4077b8825005176d96517dea4c3e6b20036e3ea2a36f464035afcc8e1871fc67fb18129c0150010a00ff025d8f81b4c827be75b6eb9858e38e208da6ebbed0aa6abf6e77fd71dbcbf222e47e07f0f3ef8609e2be8347c2836a701be8a23267800fe5bf419fe17e0ac279e78d27ba6bc878f138927f011c71c730c03ab3027f03785fc4c945e9b0072f82bac2bb04b30d5ac5cb982eeb9e75e0ef3f7da6bd50918475bb1669f78e2897960bf9e4835beff0350f5cd37dfa4c12edb6db7bdb736eec23a3a6dda34065d95730f877554043e2490c3b4de2ef09ed1d7d1fbefbfcf3ee5c5175ff2819e8359e4bee8bcf3cecbd3f3c1f6feff81071eacfab9309f33b06641e7006a85ce36363696cc8fe70cd3afca3387f856fc2007cf1b780e7cfbedb719f42a3f06a9f67bb862f769a524d9ffeb7d49f6ff06677dc9fe7f754962fff1f30f27fbb7162e5c383c9eee13492491441219f2f2e69b73e8b453bfe36d20285617015da535139519f64a4436f105a82521b96c0dac02338aa3198d049085ef285b0001f8003ce36fae5b01db94b0c290014010562ad2f586810d26130cd7a9c15626a8c65cbc85c52a65b0c49000b40444a63aee8310e49a09c012705ad6c9e95f7dda9acd070c560e7dfad39fa44b2ffd05358e68a60ce3ab1c0550f10733973f2196f4d161c00a42b7295889edd5e13290a52bebd042ef25c17df73f4877dc7137bde5cde1ba75ad64b94ced43c246a4e652834a006a7134ce847c6c9706ab61dc14e024652b00d188114d74caa95fa1fdf6dd9b36df6c135ab7761d653229aaabad61500c7e4506262b8c1d42043278cdcbd9ded149a3bd0df28ece0e15063093a6f5ebdb3864a080e85a5bd531c05760164259f575f5799b27d96c8e6abcba183ce5cd1f837d529addc751753a5e1f6d5bb115010c964e2b46248426443bd1b6f56d6dd4d4d0c06382f1c8e979622890570eea4e67323c97083f881674b677f02fc7314900183170c85561f51446ce0af4c2b80956faaefb600ba8af7be4ea6e37b06e6474ebf2a52f18b0040443c4e3cd6928b02581c5e098e7292521366d1fd0c4002db15dd8504e872df41412e3acd8cfd20cbaca7a639aced8794041d48b21830e607857ac5a4bfff7e92fd2534f3de79593d1602dd58e1403ee02e0a4aed29bc3769ab6f51674e75db7792ff236240509cae93926065c29d08f3918a47da20285ad6feba273ceb980aefbd3cd7e1d3c06dd1e481cddcf9c0f901260a86a8f627d129f6902b5983d4ac0a69a85ca64ca92392101ad1a402a012b0998cc67cca2c0df30d6cc60f4f3430402d86a804ac9689bf86b35bf6a9cd04e2714aed06409149d90747e5902f69289b12cbf6ddd364f650dc0184bc85b1d66718b2d37a31b6ef81d8d1f3f8e86abe085f141071d444351b081894dc819336670980573c3b8af041b2b2fbf3c9bfef2979bf9457c129a2310d8f1d65b6fcd6c1efff77fffe7b307f5b56083e8b1c71ea3db6fff1b6f3e579b00dc73cf3df7d05013847e3efffc1fd34d37dd44834d0080c0a6dba73ffd69f68fbd0506882b60157aecb1c719c002c685b7df9e3b687cc9a1871e4a575e7965bf8f596fc9f9e79f3fa418db44c4ff1e76d861bcf1fa918f7ca428cb506f09d6c4cb2ebb8c7d30d86c1209a4aeae8eb6d9661b06871f78e0817cafd21f0270c055575d45fffef7bfabd2af5c7df5d5bd064aae26012003be05e15a07a30010b8c30e3bf0fd344072718156bd2d00ae3ffdf42cefbee96e66c71a8c802cd83e1850318e9530460eb4c097c35f55dbd80bcb159efd70ffb6fdf6dbf7cb734658f0eee585175e60a6ac471f7d8c59ddaa098c8ce7af6baeb99636df7c334a24914412492491a12c2519b01249249144124964b0c8a2858b691518b04c6613cd4e051100812fc22e639c674620221f042060274b40539af54436ea513e36f3910640af30638dbf896f05e00e75da55ac55b229afdbca2c36146ce00b10c0d575fbec56528e2edb35400ac298253c5ab6a5c06712facbd58009be6ab0b7c89831508781389a158b14f868fa3e7b51435383d76e86b0900f88f28310aaffab706c9666b509fa9ccbb91cb20de0abae6ccedbd079979e7df625bae596db68f6cbaf504b4b8b06606454b8b79c438a7b4b006a1a8c917335fac262908d62ffb128e0fc01f88a18f035d67b31f7c94f7d9cf6db673a8d193d92ba3a3ba8bebe9e5a5bd7f3afcebabab2dc4080225ad7afa7e6e626deb043d8b9e6c62606d680650aac586d6d6d5ede5a4e8feb78c192a9c930eb11d8b098e10800176ffc3aba3a7c300800595dd92e7ec10ca62a5c07131618a9c07a03162c306b01c4873251cf8811cd0cee60962b4781a2c06cc5509b5c96afd564d431006235995a060c593a101d87d303b8cb2b5fe6b58bd9bd6c065659f88bb175151809f3c26367eb1915bdb543409ec12c96408754884d360c0d46c3382b6024f17cd778fa8e94601513c0257440d8b304b088b1c6799b14bb9580e384a14ed1d2a97abbb29d3cfe0a6c94f5aea718e077c4911ff56c611e2dfd708557665a6332d5fc85813c0a1096a279f3e6d34b2fcda6a99b4c212b8556d80c74642c95eb9015662273493372a9fa6bbc17ad7becb93bfdf9865b18e0c8b36e1921500d1c9d6bf849d7f067c2aee686ce933051890f63f6b6940f06f5f3c33713f9e15e7d163e0df8924d39d7688315f2a3da2ba87a1dc767cb123f27a360fae59c6e0f691f2cf92da32ef99e07fad20c8879602b63dd907a656cc86c83d12680687d262def18a14f3ffc70d9b006600d25c15a808d0cfcc2199bcc279c70026f18f5c726b3087e1d0da0d7fefbef4737dd74b3f7e2fddf0cc41ace8239c12fceb11902760f6cda353434507f0958ce8e3bee38de2c9a356b16330f20b44db5840a018b1a3611076293a82fe5f6db6f1f54c0320086008683efc05fe8ec408188b0390b462180bf962f5fc68c35d82c7bf1c517aa9e55e1c9279fe4f04adffce63729918115ac89f0b560ff38fae8a3797d825ef7a7a04e00b04487c11239d819f17a229813ac49606cc49c00cc129789b3b704fe4de604eb21d6c56af22bf7de7b2ffb6133bce250903973e6d0f5d75f4f8349a09bb837f9d297bec4c04ddcdf0e3460086c59f88085160020b0a60154586d7a5c4c00b286ed010803f0e56093e79f7fbeaac057f29c811f77e05e1f7a1a87e1aaaf04cf9d7beeb9277ff0ecf3e1871fd21b6fbcc160ac679e99c5633790ba0aa65230b227924822892492c8509701799a18480ab66aa47f1b4c94743d95e1d4d76252090dfd5019b7c4fef325b1ffde95050b173188857cb69860135c586b2c8385ca642591746ef83897f3415cb611168bac202421bea7cdf275fe9cceebc866bd31060c03d19bf2c2906532c090065c497a66987103e62cd9c067e616e99f3019a986f86df60109fabc6b8e8f6b84fe7224549a628041fe742acdedc103321ee0558833838d8af2197a5cbeaec2b35994d26dd7a1bfc86630d1fcf91fd0bf1e7888fe72f3ed3477cedbb47af53a5d92cdac55009e00cce2f7cb0052709841cbd5c00b4b30598ce248096ac452e910d6ef13c71e45471d7504d5646c6a6e6aa4d6f5eba8bebe81c68d1bc365d4d6367a1ba05d0c5c41d840006ed6ad6ba1912347506b5bbb77dcc9a02c3007a15d78b9a2c05335d4d1d9c5a028002a327686db0c80566b5b2b3703e107c148855fe80160d5dcd44c2deb5b18ec34a279046573596af75e7c201d1acd609f748ad9b844773804a1970eedc066185e94d80cc6522f1e1176138032007bba3ab33cbeb55ebaac579680045578b99c02ef306b4f4e8daf9e6b9b82106ce4eab94c69c22bd722c1bff52b0e2b5c591cd7e1860f5d034ca619acc0c626601ed70e28d45c959ac153cced463e8b1d83a952da0e01f2c929c01d033b1d15220fe39ae5709002b70a805d986f6e8febfa7341fa3a40821f39f460fae73f1ea2a51f2e53e1005dadddca90f3dc94f2a329d695bbefbe8f8ef9f8c7288c2ba502e367695816ca80bfda61876da9794403ad5cb19ac1609c4c032ec54b09204bfc0d544242b40a8b9e2e3e08eb47817fb355a16432fda5743ee8aa62f97202f0a82edbd2e3cf4c7ec290a5bbe2b841e84337c46ea53be18708943e491e4b03b57cb6ae106b976500b1c8cdf76d3e73a0663f14662ff1b1b6d4ebcf7530318ee1c3fc9088baaf7801fec1a2c5b4fdf6dbd05095e174af8397dc605f016821bc41d49ff7ff78f18e4dab9933cfe40da21b6fbc918128833d6c4a2502c0d5f4e9d399cd226e68abbe92f1e3c7f38637369fc14271db6db7550510007a81b69c71c619bdbee13c50f68f3edd74d38d830268814d3af80d0036f7d8630f9e836af19b08a18dcd6f7c0048b8fbee7be8f6db6f6336a16a15acab7ff8c31f78830f2c62c9f3ffc008fceda73ef529dfe70da4603d461baeba6a5706fd9c76dae9cc0832dc04760c303042e50220d01f5248df012ec5da8c0ffccac5175fc440e06a90975f7e9941d23d0991554806cafe1106f2273ff92903ae0783204c377c07f4156c42d528b8a7c347f444319ddeeee9f3dd341804e187c1b808501e5837fb42fa42df717f77dd75d751b5089eb74e3ef9643afef8e34b3e670c84fd4b086a80ed605310f8dc279f7c829e78e2897ef7bb00aa015039d400ae8524d9ff4bf6ff4492fdbfe12789fd27f60f49cd9c39f37cea6719c801a846251e4e0e2971be4aca1d87a1346e89fde74b62ffbd27d8447fe8a14769f6ecd71818219bed024cb265135e0048b2a92eedd3c026612cc93bd680011320e087b7d26c2fdc069d57801b1c0acb08c3655b010042da0531c37e31c841030ecc76481ffcf0584479e007ceab432e3238c1fba40ce08284deb2dc200462de8c08f0c112f087b4d926d776bd0da15de9339ff994b74954cf8015b42a25a50b38c4d2d00b4b014d9097396984c0cab5bc97de6fd0c53fbf8caebfee269af7cefbd4ded6c5e00f4b871b1406272bcce1c380065bc1b42c3b609a217f78f47c10033cea1bebe8c019fbd2c78e388cc68f1dad188ddc1ce76ff0fad0d1dec69b72b5b5750c0241f6543ac360acc6468414ece079a8d121fd00b468f036c8b0a983f3e974ca6b7b3bd53728f054ca7b8100e62aa4efeceae2170a1ce6cc1b2884a60308070c48006e81a9aa0be0ad4c8d6a6f2acd002b08f281810b7f119ab0cdab23e37d477e305d419f900fe93106d85040db00fe497b7d64362f0e2f68eaad9d37b74ae76c2ed3b68c50983e28c8486b0279226cb8bb5de71f87199ce289d6a192759939dc824529dd51611a019712e62a80d0848d8899ce105ed0d3c5147405a105adc0fe7cb09f662313b6375b879df4c336ea3ac50730f394066ab25eab18813c940cd8f2c6bbb169242dfae0037afa9967c9c929a0983f7656f77e5b9ac5abad6d3d7df2d86368e48826ca0bd9883c6ec4588933d06399736dbaffbe7fd0d20f97333852e9880eaba768a938ad1f0e95c8678e12b092ff60a3fda1ef5f4df094e89e80a27ca0931113ddf0a10212b584254a87fb236983f8752243af837ee9c1f3af890e9acc82e67933fcaa3f4c06f84ec2bbe60c06af3cb098d4a943129aa056db08356b9ecb7b20f40add65971d69c71db71bb2f704a5fab572e5ca41f78b7c53e0afb1c97fc92597d049279dc42196a25e2a0fd4fd3fd89e0008032bd7c30f3fec87671eca82f1df74d34de95bdffa167def7bdfe3cd3b8095ab4564e319c01bb0260c3463027cd292254b1860834dcfde9481025ffdf8c7e7d3b3cf3e5bd5fa0e3dd869a79de8f2cb2ff7eeb13fc3214bcdf5ad1a65dab4ad19ccb8fffe0770f8d855ab5656e518e37e1d3f9838f8e083fb9581308e949a5b6ce2c32f0c56c1b309805758138f3aeaa87e67bc2a25d82c3fe080fd593f109e7038ac899813803ce16be0e7fb33745b1c5f06bf02b0347ef00480dc40cf09de118c1a358a7f7cd6dbfe63a07cfbb5d75ec3e0d96a17ac8b071d348319d200141c4ccc9c003522b4ea7efbed478b162de27b916af72f7806c33ba7030e3880fa42fa42df018e04d00d6d1f48815f957b38f8d5a6a6a69279aae5de0e3ef790430ea14f7ef2939ede6ecad10e00ce14a6f7be12d8f3cf7ffef33e01b756ab24fb7fc3afee423254dff545c970ea6b3149ec7ff8d51d250302c04a24914412492491de168061fe7aeb1d34efbdf723c972fc507ec6063c853f44fec6bfc9aac2c00a83c1248f398a82c5dd8480f80c2bba6c0184f9e1fff4399ffd4a5f178c84b4c36f932ed76781d11bfd290df472436c5902de62e08706a4a910846e3e284003b16c6967a83d00ae6023f3888f7dd47b483f9099a44887fceb8691b10538a339749c602c5cd7a2b973dff636c42ea6fbefff17b5ac054b542a0f1c648ea3f44b0f70c082a3c16baa99729d18a49473b27e7d3beeb4bdf722f748da6acbcd299db268f5ea950cb4c2062cd883007419357a34ad6f590f548962b7aac9f02628fa8bd0825d1d9ddc06807300a242d75ad7b7516d7d1d83ac9a9b9bf9a55a6b6b2bb3532104e5ba96160eb50190158726f4ce016c9549abd07260b8c2d881f90a802980b4d6af6fe53c32ff1d1d0aa8a5c295e5747843afcf29a53f120e112fcb54d838522c5c007ab1aeeaf9f1fe291098a598cc30a66078128095807b7cfc8802e0285056087ca5e781ba9d2a0ec0aa4cdcc862ca7f78d1e01d4603da0c9462e099f122dd1576365b856684609c38c4a437ced9ae9c37ff7acc48811fc56685b10ce7996d4edbb7023e3212d0fbcfd6c02b5b91dab14f70f458ab726aeaeabd8d907174dd9f6ea0f03bda3088480d811a9f8e8e36da6df75d69abadb6d0cc74c18459c6ffa3078738dce1534f3f4373de9c430ad695f2eb501fd7079ca9b10ad8b0208e6eacb03df9c0514b406601e82acf77eabf8eaecc11d63e3df626f315d7a3c7568e6dc34fe4317419e02ebf2ef92e80ae10682adc2ed4ed1ae02a1ff42579f3fa48012ba2ee8b095623ad2b8e0e49698ac9d885bf1b6e3889f6dc6b37b6e1e128831d8075da69a7d177bef31d0ee7536d1bfda600183661c206347bf6cbbcf13c54059b779ffffce7e9673ffb1903cfaa754eb0b663e366f7dd77e7109103bd91b47af56afe753cc2510d7679f0c107e9d7bffe75d56e7ce29e111bb4e79d771e9d7efae90c161ce8904ae5087477a38da6789be39fa49123473188a51a99c6962f5fee87ac1a4c32980158bbeeba1b9d75d64cfac637bec140a76a65b850c0e4197c9bf8c20b2f0c6910166cc07bef4f3ff8c10fb8dfe68f0eaa490048064b596b6b5b55e8ffdcb973194888e7fdc12e8f3efa28fde637bfa96a4648ac8b471e79147df7bb67d3b7be754655fb8f62827b4e09798db507ef89de7bef3daa664138bffdf7dfdf7b1edd90068320ccf02db7dc42032d0018037c8530b7835157b116c0efeeb4d38ecc8cb5db6ebbd3b871e35867c18ed6db82e7b35ffdea570c8c4f24914412492491e122fd02c04a508f8924327c25b1ff44fa4b56ac58457fffdbddb464c987791bec0200101615d9583799590408a1925b648603c44b14c70fcd17fc2add679fd29bed79a1b34c40801b803a04b090b7794ff9e1aefccd7bcbca03109001183059ba842146b132a57db61d332c56b80cfecb6c2e5680a93141161c7f4ed8b61caaadaba1cf7ce6ff68975d7750c9841dc664beb2280fefa17022b60f1e59d7b29e2ef9d56fe9afb7ddc1ed4dd9b55ed90acc2521e0045892c70e635979200997f2996340f4a3c05a5dcc2495c14baf4da6d0d74efd0a6d3275131a35b29999af369a32855abc0d673cf8a32e6c74d5d5d553a6a696dabc970c38df860d694593c37530100b800880762c359ef50df5d4d9d945b5991a057cca390cea02d0a92b9bd3e7152b187487e7c41b43300701d4b56ad56a06df34d437505b7b1b871f445bd0af166f8cd026e401e0078c5701602cc720aa2cb32dd90cbec288b577b4538d971f75f99b072e717b049005a62b3091a9f0763e091183c478c87dc63541de50b40c3200168b1380f4c44ea0f760384bf1386bb0654e01623026960e7b2963aa5ea859cc8ec60c63290550e3f37a3001c352a1331d5dad027e098b95ab99b830af1ce233eb2afb619f62d3d8b163e9c1871ea1451f2c514a6df65bf7ddb6c5c82c0ed59975ba68f2a48974c001fb50269d3200588a41ae38004b81edde78730efdf7bfcf79edb598050bc2245728033ed0b2fc30826c977a5c7c5629ed5f52122a5403a8c00ca65c851a0f37eccf28f063a6bd4ba8439f714bfbea5cc8bfca35d5de545e199601d632dbe918e02b4758b5ccf45600b0c3b9ac664733d7093b04ce825d0beb956b00467d9f1562ea723440cf4c837c1b6c3081f6da6b770e773a1c65b002b0b05178f5d557f32ff4b1513418649b6da631c8e6e9a79f1e92202c805a7efad39fd2673ffbd9010f371847e063b05187fb80d9b36757c59c7cec631f1bd46141c09ef2a31ffd68c0016d8504be02c0ab6f7ffbdb0c8ac4fddc60150111eebbefbeb460c102fae0830faa0ac8027b5ab56a15fbe8c12483118005e60f3069fcf0873fe08dfc6a06238b407fb18ee3872f6054196826c2de16ac81279c702287f64368d3fe025ef5e4fd1fd6c27df6d9c7f3dfabe8cd37df18507f0216bdbdf6da8b990907b30074f5f5af7fbdaa4140f019575c710587e11dece36dcab469d318a40326b5b7de9a43cb96556f1870b075e1470b3d658bed8ff7ffbffded6fd9670f9400b404c66330ec02b0345404cf23f0bf9ff8c42718b88b7b3aac93bd01dc9c326523bae0820bf8192391a12bc9fe5f22890c5f49ecbfb054fc0456cea056167ea6fc7a12191a522d739ee85e6149ec3f91be929ecc395eb2b7b6b5f9cc533e5b89b1f16f02af90c6fc504857f336eaad8085c6dffcd7c00d61770987957205a0851084f2c2d3647522cde012541800bd0c1615006f1ccd9ae5b3ad582ae49d8c165e723348415e52eab6da063b96f4df7f216e591a87e5046000019399782d30f4a4533416a10a1cd2bc4a6e77f62b396180b15c3bc721df3062efcd7b8f1e7ef8df94ed028825a5c730c523c0ac33aec36955884487c12ccc18c56005d274d82e83461c0daa01239460e752a91aae7accf83174f4278e6440c3086f3300989551a346d282850bbc97ba35d4e1bd4c45e8b7152b56f278ac59b39a1a1a1af92f1277756529eb8d23c050488bbabb3a3b79bc3bbcbf18e39a9a0c83703a3ada398c1dc6b0d3bb56e7958f74321f605b6a6969a1faba7a4e03d6aa11cd8dde8b8c0c83aa00bec24b8d96f52ddcbf110cbe7069d5ead59c9fc30e7a9b0178198d74a8bbbeae8e27a68bc7d1a5da4c2d8fa90a6d473c36a81fe02bf1bf9dd94e1f7825ed03584d235d743a35698a31cded91efae16b1741f00864248419f950eb6910a589c804bc33cda29c584d6e98db9d83e83e1c036e5e902e68d0309ba81e5b2cd214fb68becb4024162fc54484387e786c15da4816eb6e29a62ac5b4ad912ca4a7bd70e3d7406db840ac5a959b44895c5f6e19a1ec3cbe9553d7bf6ff3cbdeae063cbb54a335f6961263ccffcb6da723366f112d0278b660353e1431508cbf7710026e57281df100324e533c507e5b4df1290a8b05c59460f7ca0a91db0923906d0ca3cb675bde2cbc417db1a286582af7c5f4df9c0a73c702d37c0ed06dccafb2e205a0a80af396903910fc6e27619e032db006cf9a16be1e37428439f95d00a40af78b10926bc44068760a319a172aebaea2adebc1d0c1bcd22f0513366cca0ef7ef7bb0a903c84e44b5ffa125d79e5950cc21a6ca096e38e3b8ece3df75c1a6841c8be679e7986fa5b7af399efb6db6ee7d078d528d8d0fafdef7fcfcc1883cdfe0acd11fcdfb6db6ecb2165c0b0536de1e6c0fc02505e227d2798f35ffef2970c7ead043c3190ef7cf08c3573e699f4e52f7f7950034fc302a62b308d9c7bee390cfaece97d4a7fbeffc3fa8d767fe10b5fa481961b6eb8a1aa59a3e2c83df7dc43efbfff3e55a3e07e1a600ffc9801a1a2079209b2affc10f4f9c0030fa4cb2ebb943ef7b9cf55ad9f99356b16dd71c79d91d7aaedfdffc2850b692005ccc7679e7966bf8672ed2fc15a81fb533062dd7cf3cdfcac0ba6d69edcb302d88790a228332cc9fe5ff54bb2ff97485f4962ffd52f89fdf78e540cc02a36a871062deec0e6b1600c902486d8bf522d9bbed5a07bd52a89fd27d257d213fb5fbe6c05ff5ad291cd759210592e0321d206438b6caecb77613fe9c6bc648598b488f20007b291ef18ec27027c127614db0ac04d02620803baa45cded0a720ac96885c136019e93ad517270024e8b43efb0a0560006e9b919f5ffe18802c6194c1b10a0ee632bb13d2643275d4d8d4a8c318ba1cca8f81583226406c096acbb5542837b0e030d024c780a7d9b3dfa4458b3ef0ea4f312025db95f5927429f08b6661e2bcdc57b93d518c422e293088e5d5cbbc427686507ace51e305f05157b68b9a4735d1e11ffb081d70c0beb4e5e65369ec98266a5dbf8e9679ba01b013fa06d62900a81a9b1a981dc1b2bcfedb69aaab6fa4d1a3c7d2e85123a9b1a1c17bc9e0fd6d6ae651acadabf3f2b57bc723a8bdbd8bc01086a1696868f25e14d63100a4c91b1f66faf25ed2d6d4d4fa635a575fe7a5cdf17c3288ca51e09f8eae0eee1742dd49f84147f777f4e891dc9f9ada1a0544f3cac971a83c8b813e0ac89152601d6fdc30be0a8496668007ea13b6309e6b841fb414a04be9840e3168051f7f0ae51c80225e7de6c7470c0664650a6f677ec2fffaca87ba014b9d308e497dca061826a8c04b3ed8c9624a75846bb4bd7f5d1d1d5aff5d1e4bb06259ae0e1309d6302f2fb38f39148481d476d8d9d1292a9ac794079b61b6b34c0d43a8ec7486413b1c2a32e77a757a69b2def83b38b6940e335b56171db0dfde3471c2063c3fca2fa5f43cabb974344851e983cb36f0ce3bf369c9872bf83b10552edb1f5b65d1e1e3ab9e9e6cbfc336d4dcdce0e5eb62a63820c49c2c42fe615c5206404fb351697f691963e1fb409235d30d8046867f1380a9df0639e7060c85c26ae5336719802984f314c4a509722ae4536d0ad67001cd0a183665840ef4eb30c2483a1a2ce5339ae936fbc05bc9c77e4e6cc10dc6c4007c49fd6400c0fc3274b9cb962da715cb570c09e0e35017fcea19e106f1ebeb2953a6d06014f82b304401ac84fe0c76c1060836080060ea2d26b2febeffc79c0094f3a73ffd89468e1c490325f831c3fffef7bf7e671de92ddf874db96bafbd86aa4d264f9e4c679cf12dde080713c660026d8a947afe873f0423c435d75c5b75202c6cfc95c36e943cffc717800aafbdf65ade58c5f34c2532d0f73e087d76f6d967d3f4e9d36928c82ebbec4a77de79171d76d861bd0668e9eff77fd8ec3ffffc1ff3c6fd40cabc79f3981968b00ac21bfff18f7fe41f8a5593c0e600b8fa7fffefb7dee7ff550520b9afdf3dc35702a8facb5f5ec221ebaa4dc0f80690d8e38f3fdeed5a35bdff5fb366cd803144426f4f3ef9ab74eaa9a70e89e7a75202bbc40f8df07c85313ff7dcef7398e138cf294883e79a1b6fbc916ebae926da679fe8f535d9ffab7e49f6ff12e92b49ecbffa25b1ffde913ee1208e6340e518d9401b64b219327c2599fbf225b1ff44064200f859ea6d60b7b5b6e5b19aa8cd7d52e7486ff87b9b4a8ab5c6c9636ec152eb1879b1b19ed3ac25265b8a7005311b8ab0e9186083a830540ccce8eaf2c15512e6ca075c49478c0d7a69abc9e082fa4c8618d9d477f4790833b4900234e50cc01577410053924e986c04f0a53f9a0b49853cd32c49ccfa64ab5a5591f9b661f9002cfd3f2b4883367cb07831757476713ac7d19005ee0b134f91696ac18d8f02a7b83ea0435aafaf311c4b31660122b1cdb6d368cfbd76f7cee5687dcb5a0e47b972d52a6a1ed1cce301f6a87a6f73004c56cdcd2368cdda75fcbd757d0bb35ce17b5b7b97f7374b6d6d9db472f55aeaea72bce31c75652d5adfdacab4ec00aa64bb3a993509e700b85abbb685f570c4c811fc929341375e37c14405661bd48fefe81b403dcd4dcd1c4e102fbb00b261d6294758ce6c6af57419fd056b17be3078ca5661e31828974933982ce795515757ab40555a395d1d0acf04a808de2a5f2c73d0f33f143002f940448bf23f11e286fe4596d30b12e999ade0824b01cb504ab3a601c084f963f62b3bb03d80e0184485b14fab4d5180b4ba3c7dcdf87a4f3caf000f627cb1a1019db1bdf400cb753020cbe639825e80f90ae5e69cac3f9ea83fcd20b9b4578e4b59009d5c4b838e2cda6cb38d69934d27717d324c8e9b538046e9b76f7b0a74b6aea5c5db705e40e4daa1a1283ece121a116085e611a3d4e059a4199db49f73035091e4094068396dbff9a1f9f8aff66b92c7d2e77dff4a94175e900165c24668f8bf3ca0950645390673146c0cec53e2037d3f6e94e587640db5c9078669b02ccaca196558da0ec4b73b613082e9c799254d817a99f92bd426614a14df2ce02b017941d6ad5bcf6b5818a496487509ece5873f3ccf7bf97e320d05011004615f06b3607300ac2bd8fcefcdcd9081baff074bc2f1c71f4f0325f05360c01a8ce129b1497efef9e753b509c04800007deb5b670c28bb475f8a692fdb6db72dcf4335856505b3c7bffef5afd8e993e7ff783275ea54d66d30a00d05c126f3600f7f0690c74f7e72016dbef966d45fd297efff70bf3590be64b1f7fee295575ea1c12a0037bdf3ce3b546df2d9cf1ecfc0cd430f3d84aa49fac3f77fea53c7d235d75c43d3a7ef43d526b8f7fbeb5fffca0cee71a5bfdfff0fa43de299038c8983397474a502f0d9e9a79f46bffdedff6350d5cc9967d171c77d262f0d7e5c803568e6cc99ec7bc08c0ad6e5c122c9bd5ff992ecff2532542499fbf225b1fff8120b80d55b9b65d58a268cdbaede6cff504156f6763f7ab239db1f635aaa7d96b1691d3e1795ae92faa3bef7769e42f97b2289fdf74d590329d564ffd8845fba74b9626c22c594e28302240414030eacbc70802c029cf2ae65348b8db999cf480803c8c46009cdd4624b79ae9bb7d96ee93ce6e6bfe4950d77014391715d1d06802e3384a08015248da3810c8ab14ab7cb550c5fdc06522c2fe15b089f410b9bff60f549a5827364025b24dca1cb600b007d982d0a253b71e6c962361ed00fa5ac14b31159ccbce4f3c578d76d72fd78858e8fddf2c142966e13c6c0263d9e8e06f6381a2ba4da387ad468ef85d62768fcb8710c6e42bf0080193d660cb57a2f93468e1c4d1f2c5e428d0d4dde266d13b3626d30613cb5b7ad2727a758a83e58b4d01bcf0c83b0da3b3abdb48d0cc6caa433d4505fcbfd0778470032189311239a289bed62062c00aa301f388f173300cee4ba7254539be17959b7ae854138000301845593a9654010da69a72c66ae62562c0688a97084280ba10bd1cf6ca70e3588ef5e1e0674692091e364555839b2f342204602717ac974d52cb8799f01155783be041894b6787c30aeb96c4e8f9dda60b63463188052004529b1c8c966b963a9b4cde02b80b00450071b4bebd08e60c84a67d2cca8c08c57b5355eda0e1532d4512c58d86805600bed01bb966fd35e7b329e7e2896324781ba2cbcd46ba69d77da4187b9a33cb09142430190294050c58ad5d6da4aefbcfb9ecf90a7ec3f603f2b26d00e84bd9c30619c0646994039f299ad0494e91a802631520158f98831f829ddf694011415e0918050890220544a87206493d7739712b62b9d4efcb6b443d268371180ad280077099320d7abc341e6cc31d5d749335ff9ec57863ea92e192c82728d817d0a6c464699cc7a8875c4d323d700c58a5f56b6aa2dc5cb02f63cd2a0cca51f2eeb77d6994a65a8dcc79423d800bce8a28be9c4134fe8766db0deff6323011bced8441f8c02600b98af100eb25ca9d6fb7fac1ba79c72ca80320861736bd9b26505af57ebf33f8063e5806cfa43308f608442584c6c5e552283f1f91fcc3bb7dc720b6fc45583208418425c09b03079feefb9606e6fbdf5d61ecf716fd97f6fc88e3beec81bc78355c07c75d555bf63c61291c1fefe0f0c5808f5355082fbf2871f7e987a2a0361ff009e8201ab9a044079b0419e77de0ff9beba9aecbf501dbdfdfe1fcfaf5b6eb9255d7ef965de5a7938559340df1f7bec317afcf1ff504fa5afe667b1f73e6fa004a1072b616b1b4aefff715f8b3506403430a68179563ef3e7cfa7071e7880d75100af4afde86038da7fb9f5477defed3c85f2f74492fdbfbe296b20a55a9ffffb4a12fbaf5c12fbef5e961d27516f20c0fccdef32f3f487f447bbc279860ab2b2b7fb91c71e5341de9e489c792dd53e7353b1589e9ef431ae5d86d3956b8389fd474b62ff815497fd3bb462c572fe2bfac66004bd896f8518aa84d18af4776138918d7705eab1fc305b2421058d0d748702dd765c371f1ce538f96c2bd8ac4fa7fdfa052ce518c005f9ee6aa614dd10038061f980005324ad6300c484bd4b8e4d0d14062dbf5e0da8e07110362cae8b182804940040022d2dad3a8bc52108a9c45cb99a398b83167a7fa76c38c97b08cf908f7d2035a6ba481f50c6d56b361e4b03acf83c8743542011c578857e286056f38846fad4a78fa10d264ea09a4c8ac14c0040353535d306decb3db4b765dd5a063bad5bb78ebaba720ca402b066cc98d1cceed5d2b296468d1a411d1d6dd4d4d8e0bd20ace7f3b96c07b70340a8c6865a2fdd3a0db0b1f93a42d1e15885742466d1aaf15e36083b8febb5b12653c3e00fc5a4647969532aa49ca31871000e022352da4ef9080ff413650324545ba7ca03eb15d28271094020098ba7424b923f9612c2ce154012806fe6a72708acbc69cf67b61ad01b6d5785e5431b10ae110c5608f907101c008000543133980eabc9a0375b852814c023e7d5601930901169a0a311b614402abc38e5708596d233d60fafbe143321d91aa4a518b3d802522915d650d70b7bc235e810831a2dc5d6853099d3b69de6e94f5ae343194ae4fb167526ab6c48b717652d5ab0d0afcb671e2be14ad9c42d057eda64938d0dbe3255ad2b4826c3c7f93e82d4cb6361a7629f85b08edaf7a8c29dc067699f87ba1882a9ff0a40caf7c9a84e83475d3760caca69264033fc5f1e5ba1fcd51d4b6b1014834b49b16bf11c68c0963908663effb400528d7b2e5fc7a58d1a08c97e9c0206afe0ba5e3b5cd7f7c5d237f17bcc66a7c350757976bd62e54a1a2c3254ee63e20ac22880d1e5f0c30f8bbc3e98efff1106e5739ffb9c5ac3069160e30ea1302a0d4f54cdf7ffe8db39e79cebddc334d14008c233cf9d3bb7e0f56a7cfe0718fa8f7facaed08300a6e0d7ff6084820cb7e77f6c3023dc1298f6aa41c064f3e1871ff2f7e4f9bf6702dd4608dbde08c3db1bf65fa944cdeb21871c322859b0b6dd16cc573f61e6aba1f6feefc8238fa48f7dec633450f2c8238f94fd03896ab07f84fdaaa6f089005f01c062b241569bfd47d5d157efffe13f7ffef38bab8e551780e52bafbca247612bfbf2fdff92258b692004cf4b71d99c92f7fff1cb1baef61f4792fdbfde97e4febf7249ecbffb75f36fa13c89fdc7cbd31f3290f65f148015a7617137dc4c2528544ea13cfd21e5f4c13482beae3b4e3dfda5a8fd250381b8ec499d71743a9c26ca6ee2ce755cbbe8a9d34fec3f5f12fbef1fa9b43f5d5d595abe7c555ec83d01f4f066b7ebfa61a038ac1fe9b0806e007a52d018050e0090825fb4b941c84161a9129092bf905a561e3b15e9f4825f10908169bfc292659900000a4272c939090f28802dd9b8973a0410606b462ed2f9248dc90813a4b7fcbafc3a50af34de7575d845dd475785685bbd7a8dc654587e28ad48511571ad8a014b31f26ceb6d404d993c99415302ac22d27d772da3d541bd7e24434e6769861e15b25063e2bcb97568fffdf7a1bdf6dc896a326920b518380656a08dbc175bab56ade6977cebd6b770b836b47d9df76209ac4778b13462c428de5c0728aaa1be4e01b8ec1c3535d610b011e3c68ea2faba34d5d4a408b8b3faba5a0dac006842b14e0184d3c5002b9b329eee003885b2ebbc7a113ab0a3bd83c1360dde8b47005504b005f00cc6baa6b64601b27487511e7439a34156e837333069dd40db314459616b127089d88f4579fa455a07fd4f0c762401c1b8463ee0f11c57319bb91c4a9218ccd6ded649eb5bdaa9adb58343f175e5dcbc7492d6d5f39827bde8eb2d0a74467c35c6288b30800012e6b20aa0a60157acf796cd612031feb88e7902300e65e4b2ae1a63ef9a309ba5b56f40d91d6ded01e3950e3dd8d1d1ce7d022b9aade704e7500fdb8dd89bd7862c80778ed257e80bc24d4edd78631a3dba89431732d031e899d2770ac213e22ff22e5dbe9cc35102d003d099ba18ba07a0fcb090a4d9e99067336fb346f927e53f55a84b2b000c51007852c041c7ffb068dfea1a2104890c064103b89ad3d7b326904bd7630294cc7085e2a3cd075adf5b681fcd2c5a9615f869e3be2365078044cb007089e401dc5c030c266dd265fa2c5b8ee387f914bbf4d704c3479b6dd327547d529e6655c311c003cb97af649d90769792a1b6fe57b37ced6b5fe31077a60ca5fb7ff46debadb7a6c1249ffdec67bd767f9c7a22d57cff8fb03c7befbd370d94dc79e79dd49fd2d3b105cbd46bafbd4ad5225b6cb125b3b321a4a4c8707bfec7dab8d34e3bd1c5175f3ca08c6e220016de75d75d3e9062303fff0fa4602e7ffce31ff7ea9a514defff001201206230850b85bff9ddefaea25d76d939effc5079ff3766cc18faca57be5211f34c6f0898f37a83054ba43fdeffbdfdf6db74f7dd7753b5c8d4a953792d38f5d453bbd95635d97f4ff25672ff3f7efc783afbecb3e9bcf3cee3f751d522085b19a53f0361ff6179efbdf76820e4b8e38e6310564fa53fec7f30c970b6ff387525fb7fbd2fc9fe5ff54862ffc5eb4aecbff765a0ecdfee694151e8be52e945a46e7f53a402852c24711523aa5d923faacc42e9cb293f5c76a1b2e21a5b7f2e08fd516e6f195eb97a19377d6ff4a352546cb96d2cd5d6c4febbb74bf2479559287d39e527f65f5c2ab57fbc185bb2f84315ca4b8f492a14ae4fbe9b2c2a3eb0440395721a4c21202d88a35fd4e7e9bba9bf46db05d8110e1be8ea3a388fb42be8b4026811f9ec32a475d8ec8325202b9dc71f53cb001c08eb95067e98009a006416b445c05d38efe8b22434a21f5ed1520c4d0bde7f9fda3bdb15c0c50a402051a240226803fa9ee3d2b7da622a1dfad183a9b63ee3b5cf87a711f98030dbcfeb0f8d1e1e2536d7890f18ae0057023e65f49851b4cff4bdb84d239aea1944d2d5d5c9602cbca0c186cbe2251fd298d1a3a9463314353681e1aa8119af9016402ab065353436d3840dc6f10b3080a030f760b46af1f48b813cb92cd5d5d7533a53e3f5a3c1ab3f4d999a5a6aefe860d04ac63bdfd1d9c1409a7aaf7c840e040403803e80a6700c162e05c4717d300fca16200a8ebb345b168e3bdadbb96ca4857ea484cd0df9bd3ea6520198cf36c022a109217ff02ccb1ff362e29a6013d61fa8974d6bd6aea737df7c871e7af83f74ed9f6ea44b2ef935fde8828be9fb3ffc09fdf04717d2453fff85b7e9f807baf7be7fd26bafcfa10f97aff0f4264b39d7f2c65983f20444d49b37da96005ba0e7363387091a0b73ce60175bb10d31002e93d1e78485c8e231c507f38197c300cb81950a803a0064302f9d1d5dd4eacd634a870765bd00fb138e738aeda81d202caf6e84be948d3e8018b96ed7d5a02bc5d697d5a02ce83dca6f68aca311239b14a609baa3dbac8cc15cdb2c3e86be23741dc0851c98d0262a85aff381586ce3168df33636c0e666890dea2a6c03c41530e9e5fb3c571ba965843bb5f498fa0c7fae02be06d64e3e48d531fcaae506a15529e4b3953e1a7ed4d0777fbdb414708bfdb8919fcf394e00f40af9561f7465e503bdb85ca3be6ea27da4d808e9f4d246db60c132d736bfbfc2a4458a41edc3254b792d1baeeb7fb5ca273ef1093afef8e387f4fd3f7e858f8d85c120f0cd471f7d0c7dfdeb5fef766d28ddff030470d24927551cb6aea782cdb70f3ee839cb407f3cffbffdf63b74fbedb7f588ada137e5d0430fa5ebafbfaedb46dd707dfe0750e7b2cb2e1b701016ee0d6ebbed369f11a69aedbf5ad7cdc99327f35ceeb3cf3eb1d2f787fd575a4e313d060bd6b6db6e4b83413027975cf24b9fb56ba8beffdb638f3d62eb5d5fc8fffef7bfb2d2f7c4fee3945d6cacf0cc7af9e5bfa66a9169d3a63120f9d8638fe5f72b61a936fb0f4b5fdfffe37e0f3ff400406dd2a449540d821fa7dd78e38ddd18d406cafeab4176dc71a7d86907d2fecb9564ffafb80ce5e7ff7225d9ff2bbffcc4fe8b4b62ff9595694a62ff83c3fe2b8a3510d731c42d23fc57246a50e2d6db93f645d5639e8bdb8e42e9e2f62b3c69a50cbadcb6c46d5f549e4a74a0b71c6b4fdb11a7dc62d72a719ce5d6d553296453bd2189fd27f65f8df6dfdedec1a1e564e35b6cd532984d24d49e80a678035f6fd8f386bc0e05c5ac44f91552547f6c735126f2597778535e1fcb351109bdc52c30028222f2995f1c5daeabc119e647c25b31b04133af489b1dcd3ce36ff6ebfe7179461ba47d92c7ac9fc706d7f3427ce1a24bd9ae0e7aebad39dedf2c39b5694a0b835031e1b06a39cd28e45053533d7dead8a3e93f8f3f45afbf3e8759ac9ca8322c5261062d832b49754b0b40419a09cb6bc71e7bec46db6c3b8d962f5bc2ec5475dea7adad9dc68f9f40ab56ada0d1a3477b73dac9ec459dd92eaaafafa5d6f50a08b57af55ada70c30d396420006300c0bcbf603e7db07829872c5cf0fe025ab96a1575b477525373334d9c389e264cd880468e1ae56d246d4863c78c6696abe66615ea12a101c1c6d6d0e0b5df9b9f5a6fe394413f6e8e01381c1acd56e038e859ce1b9f4cba86d219c592852e363434505b6b2b83de52b529cd10460cd4003888c167f86b1383af9895ac04a0caa5f26f6803008b02ec004cf4e2cb6fd033cf3c4bfff9cf93347ffefbde982f675019c616cc3d688f8d9089b575346edc18da6cb3a9b4d5565b791b193368d75d77f5e6642c4fa62bfa0f1b44bf7ac33db8811daa3082029eb1fc50740a20437e883ef11300e041ebc14496cb01a8a380712a4c650787296440564e8536c477e4839e8d1e3d4a017e728e0a67e8d5a1e62eadd8cb2c65f3d021809c2018abfafa1a9547db2eeacae55c1a3776ac37766369ee9c795e5969df67a98feaa88c97f28144cb972fa5f6b6566d240a9415d609e51bc8005f914aebe9d28891233cbba9a796ae76c377ba249ec37503562880094ddf813a193828e1f7f4397fcdd42024f15faef6db0c58d3003ff149528f8427e47973ddc0b752e0d7c8f091247e577f67463252a052949fd67591c12e287948e7f1db6e5979212985855000b0ae91c71f2b5d66da644134d7a9d098485b2ddd564b87266d696961b05fb2fe578f20ccd2cc9933f917ea43fdfe1f1b63bffded6fab2a5c4d94209cd9b9e79e13c98431d4eeff77de7967da65975d68d6ac59d4dfb262c54a5ab46891b7b93fa9ea9fff67cd7a9ad93eaa4100323af7dc7369ead4a991d787ebf3fff4e9d3e98b5ffc225d7ef9e50c341e2859b8702187220c6f72579bfd57ebba79c41147f05cc695feb0ffde2ad714e8c717bef0057ae9a597a8da057302705235db7fb88c4adeffe1990acc974f3ffd348748eb6f9933670e039b8a31a355cbfb3fdc33605dac06c17b18008b76dc7147ea89f4a7fd87aff5d7fd3fd8683ff8e0030eef3a103a1e96175f7c919e7cf249fad4a73e35e0f62f021b1ca87b0809275d48aac5fe0be51fcccfffc3c1fe7b22437dfd2f2543edf93fb1ffde6b479c728b5d4bec3fb1ffdeb2ff74dc86460d58a974c5a49201ea2b658d538f79aeb795a0afc60b52c869542a7d69d8c524ac577dd58e4a17b9de5e90fad239c76d832989fd27f66fb6a31aedbfb5b5cddbbc6ee5b47c4daebbaecfb4e283975c05fe30c34ef166bdb1a1cf200abdd19ed2ac4402b8b28c0d7b011558e6350181791f80124836fe755d39a9c7ce8f022ced95f6b9a1ebb84672f34941d82c191f09292865e1bb6de5a3c12d2354963aaf4647ca24e99b01d6e036bb16bd3df71d5abe7c058d68da50b795a814ae47b5c8d16d7768d75d76a06f7ef36b74e18597d2fbf317938facb2f2bb497941c0ba0b33fd5878413e9166cc3890525ed95337d998af353535312b556d6d3d8d721032cea6fafa2606bd6cbde516b47acd5a1a3b76348d1c35dadbc01d49ebd6b5d2dcd9af7b2f9866d16bafbe46f3e72fa0450b175167571703aa1c66a872182405604e6d4d8643212870d1e674e081fb7b2f64b6a209e3c7514353bd57ee086a6f6be76e373436f8e1eac08e8530872deb5a98d18241222905fa62862cefa3801f0e8371785e1d97c161181406acb80ab05393a9210132d99645a526c2ea367e543a08a1b62f001bfff3c42cfac7fd0fd1830f3f4a2b57aea6b63600c41c5f9f24fc5e96c3dfe5bceb2a64e53befbc478f3efa24fdfd6f77d33efbee4d9ffbdc6768efbdf6a0f1e3466afd54d53060c8b2a947a20d5db140693b4ea9f0974e36ab0070defcc1be323569d607f60300b10174558390945ddca694a733ebdbdb797e009acba4330cacb3f85a8ae7054c1b00cb01a025230c5f9175d4b864bd790530b4a141b11ce117bfe29dc0f6c600bcac628703680d210b11b61160a8b1e3c6f8634bd4dd5695fd92f63d36ad5db386d67b3ed03fafedb7d860f9dec2fb032003eca6655d87efdfc80dc2af92df96c04ff12704ae92eb7e3854371f8825d7f1015889c33a0a00cd9846df3feab204f095b283721dd5741f44cbbe5340aabaae948e53ea8afd48f914b82ff9ae87c2f08d14f84249e306ac58b25608931698ccc4b7db262b96f487a201a7d043e818c0aaebd6b678f3d89a777db8afff0329d8643bfffcf33d5fbf59c13443e9fe1f9b64a79f7e3a6f94e197efd52860a7044b405f30e954e3fd7f737333edbffffef4dc73cfe970b8fd27cb962da5050bdef736f677afeafb7f6cc6dd7ffffd540db2f9e65bb0fd140bcd369c9fff0168018bcc3ffff9cf01652bbbfaeaabf9634a35da7fb5c961871d4e5ffad297a8bf64a0dfff7df2939fa45b6eb9859e7df659aa560160e3cc33cf2c2bcf607eff0700f68c19330624b4deaa55ab181403407e21a996f77fd502a607b3d32f7ef10b0608962bc3f5fd3fd85df11d4c8303095616b9e69a6b18745b8899abbfdfffe39eaf35f4acdc1f82f72478ff574c92f7ffbd27c9fe5fdfc8605efffb4a92fdbfd2ed48ec3f5f12fbef9eae37eaed699e4aa43fed3f3603566f3b9d44fadec8cc4df728194c735589a3e84fe9ad3a2b5930c37d2e3506bdbd5854922e91c4fecb91386dc56621c2c449c827dea0d74002476fbefb2029fe5fc0b822c02a6cdea7a4cea072ffaf6cf693000234a84a36f2859545151f309c9066a9c2e67c5a7f97b608700062862a14462abffdbadebc36510040c05f1f286630cb085b962b75ba6e5eb82c855851e5591aac00362370f6b87a7c183a60b934efbdf9f4f63befd2261b4f260ba0150047c88ad06583fb4b231c2c1d4eada636451ffff8c768cedc77e8ba3fdd4c2b96af2422016604a00b5767471d9666c4b2f468733fbcf6645269da7d8fdd68ead4299c062f646a32196a5ddf422b56ada186fa060e0598f12aefeaeca246efc509807a9b6cbc31f773d5aad5f49f27fe4b4f3e358b9e7cf269667a58bfaecd1bcf1add7a0d78c1ff81a3cb626e3aa923d541ab57aea1f9f3e63313d43df7fe8376dd75273aec2333e8d0430fa2c91b4e623621806c724e96379900ae02d8040c620845683128c8a10cc21c7af30186ae34330bb90c1602e82ae7b599c1440c957119d005a08e2b3012d173bb040a8e473894a6c08d6600dc01882443afbffe26dd75f7fd74e34db7d0fbf317506797aa1b21fed8765c805f002271552dccf61594e572583d87162f5e4af7def30f6fe3ed75fac4278ea453bef615da68f2248d4d54ba649113d29f7c61551273a468611018c6af060c55c20aa7ed040c46699b6d12fa8063305fd5a46a349398abc034ae62d062f62aaddf604ecbe930910c1a72736a145216871d0448ada1a15e8f9b45192f2f8075087309d05087571f696027420a66730adc077d05f40869b95fde80d4d5a469c2f8099e7ea7bcf17639bf1a4f57f0914a21054265c1ff757a6d584f2ad462965d8e1be2c0326dd4d407b477c488266e2bd10a05a882ba19639d52282c70cfe970898a5d0a63c9e14eb54e49f840d3e73aaed890e3835b712cc044065019736afa692ecf35421052100ad1074f69ff28bed30f4b28ac824a31d4f81b605acb5803a45e57daaacf65b53f9530838acdcbbb9e73fdf090e2af2534a8a5fbca210f05786500b3a44ef1d5b64caaf7bda5753d3360f55406e3fa5f8d6d0218090c58c564a8ddffefbdf7de0cc8ac4600167c2f36ff073204516f48b9f7fffbeebb2f8300162c5840fd2d4b962ca1819438e3f4d65b6fd16bafbd46d520175f7c91a79ff1d9814486cbf33f40eb6007035bd940ce19583d109ebcd4666a5f4839bfa4ad26c1ba70da69a77acf5c53a9bf64a0dfffe139e0c8238fac5a00d684091318d40860f27079ff07400f42430274dbdfa06484e505a8a91800ab94f4c7fb3ff8d68160cd8c924f7ffa380ee759890ce7f7ff279e78223dfdf42c7ae4918769a005cc6fafbcf24a4100567f8f7d4bcb7a5ab3a6ffd9c136d96413eaa924efffe3cb70b6ffb8790a1d974a1f4792fdbfde97c4fee34b62ffa5f3143a2e953e8e24f6dffbc27b017113c63957cef54279aaed65435f4a2165edad31886bc88379dcc3ed2ed771f5e47a5f48252fe4c2794a9551ae934cecbf6f24b1ff9e8bd96e80695ad6adf337e17d2614238d842034d1dc021491f07d12f2ca3118af248d6580a0c2ec53665d960618f869e4237518402b01259009c8b20216aebcfe9281fe0f8f05e9d089d8d817c095ad4054d8b4f441665e1a0ea525a009dd5e3f441bc00c24fd5021ee147b8b45ab57afa6279f789adadabbbc3a888123ae8a17e8330e598eed0378c8c57730bb00749666d00efe8d1cd148679ff50dbae082efd2c69b4ca254c61bab7486c1455e8de4024442b66e27712842809900beb15c8b1115683d804e871f7630a56d05c000a00120bc55abd732d3547b7b1b039b52298b468d1a49b535355e9e8d687d5b96eebdef413af5f4b369e677cea5ebafbb91e6bcf536ad5fd7ee959fd6008c60941900c5d83acd4ec640148b311d5d1d595ab46009fde3be07e89c737f425f3d6526dd76fbbdb4beb58b1c2bed8d539a323575cc9c95cd65152823a500370c944318494b33013120c352c02dcc13b337a5781c589f397c21003c69451d65911fdeadd407c316fe50e803788b6264b2a9a333478ffe67167de7ecf3e897bfb882de7d67a1379628cb9b47d7e6b94c61ae381424da98d63aacda87762beca1022901f80416b179efbc4fbff9f51fe8ac993fa059ff7d9e3a006ae3010fc044aacd98e1fc0feb06eb9c4d0ab4077d73fd0f69c04b8da74b0062757674309850e612601909fb09a62bcc413a93663016986e52daf66d0dcec3717b473b8326311f78e90f80135efa41179077bdf712107f477bfae54d909727c320348c537b4717db4a670e63504b1d1d9d1cc2b0a353b14cd5d7d5fb20ba940ed389f0981dedeb69dcd89194ae496bbfe5cd899bf5da2561f2520cf6817b81cea07f281b0c80ae2b2038c3eff1cc06baacce391adce770390d0d8dd4e86d8c2ab0a6027a05fecb6270a40290a5d4fc5b29fecef366ac33be3f337c4b4a7f4cf01119be92b4ef14b092b011f2b86850a838244b03b27c9640a4e5313059fd747855096fa8c14d7efb887cd6ad6ee02bdd7e29cbf7c13aad0a0f2bd76c3dfe1a409b4af96b491e68d6ecab6505802dad6f025c430b5bd6ada7752d3dffb5f3605cff6dbb870c78bd2c605842e8c1a830777d21d572ffbfc5165bd041071d44d5283beeb8131d7beca7984d70304bb9f7ffbbedb65b4920605fc9bffffd2855a3c8580128f897bfdcc2cc24032908bb7cf6d9df6576964a64383dff4f9932c51babb3193432500210c53df7dc4303217dfdfcdf573273e659ec8baa41faf3fd1fc2c002e8548d029f03d03464b8bcffc3bdf331c71c3320e049f80d8430ed89f4b5fdaf59b3862ebdf4521a6801d3f7e9a77f837ef4a3f38a866cac4486c3fb7f8095afbcf20a3afcf0c369a005f759582ff163b362d25feffff15ea4a3a3ff7f24520880568e24efff7b2ec3c1fec392ecff0d0d49ecbfe792d87f62ff83555cd7ed0ec08a1a80a8092b7792e30c6c298751e9a4c639d79b12de7cea2fb1421b5a51d7a224aea3ee499be29e0fa72995ae278b58a9bc6154e9408a597f6fa26de39c4becbff23a13fbef1ffbc786f6b2a5cb1984407a439b3773c176626cb093062471d97ab3376fb35c03b76423189beb0288923053c89f33424bb9ae848633804fea82fae8e33c1b36ea25dd4f015799402e0121487996ea88bfe9cf659afdd365771b230190e184111acb675fd1f9059c6233d0c7ce0bb108e014c04f4f3d358b162f59ca8029135ce07734ff0b7f473effa3430e028475dcff1de3bd3cfe96b761b51b034152cc0ee6482e0d78b21810a6186402bdc8d464e8c003f7a5d1a347523a93e1b11c31a29932e91435d4d7519db741dbd8d4c8bf5205f00680a00d36d880d6b6aca7df5f7d0d5d70c1c5f4f8638fd3ea55ab09f816cb51c012f2c7d4cd9b43db008aa8b9b034684925751c8bd9b39e79fa39baf0c24be8daeb6ea2552bd768c6a4b40fc8cb6454a841307301c8011007eec452568ada38849c666ab215531ac0258026717842cbd63a437d223ccf29d80cd1ecffbd4e3f39ff67f4d8634f51475b17c3ad303eac77b695cff4e603f6525c0a423ec25c264d9a40d3a7ef493bedb81d8d1ad9acc6cbf55e9a7575d2230f3d41bffce5af69f1e2c53ccf9c214f952af0f7a4c1403a2bfa82b08f3e08cdbbd6d1dec940c5fa867a06431183c4d24c650fa632b0a8b57b7f01d8c36434d4d5d39ab56b390d40599847003200f0c37c213c54b633cb7a8550726bd7ad6520688ec154f5cc8ad5ba7ebd57fe7a2f4f0d879084a02ded5e5dac035edf01084b7bed6d6969e17c63c68d63c819ae6b2f47e43384e5033bf9aa572edaee84d443c105950d59112c68006201d998f6ec99410d96a3c150f9c03e1f30eaba248109390d8a011b94f695c23e85bf608f02c3986bd4671bcc7ec25af5ffd9fb174049aaea5e185fbbbafbbce69c39f37e30030cc3c0c0f092a7a01030a2f888a249c0a86034e845c1787ddcdc98ef9a1b6feefdabf78b9a7bf3a97988d1a889cf984014214105514110146490911998198679bfcefbd55dfb5f6bedbdaa76efb377759d3373bafa9c536b68babb6aefb5d75e7badb5abab7e672d61662dc46c7064f310c7162e411867f4d33e5989fc9e9ae9f84b59e64c60ac7e99f195f8e9b847c700622016ef119c1d0be36c0cb0120aa45ae53d81c615c6beabc17090c47ba17971a95110c90a083d6fdea7e87ca08099870e1e9af60fe5b9b2ffb702e15f9f23cdb7eb7f7c5076dd75d7412bd2bbdf7d2b9c7aeafa4c6de7daf53f9699ca830e1d3a08d3a1665dffe303c13bee687e292a9b5ef18a6be06d6f7b2bed43c793e6eaef7f2cabf9c637be11f222dcaf3173daf12c8398d7efff66100292afbaeacacced9be5ffaebe8d68aaf7ffce3efbece392f9e478138272a7b2264cade0ff769fa9deff5b16fd465ab76e1de441beb27ead72ff0f7f5763b6a2bce9f4d34f87f7bce70feb00f3b3d1fff3a4458b16c1873ef4a11929b93d55bae79e7be0a9a79e4a6dd32af7ff678ad0b77cd42afeef3ae7a2e2f99fbb6ff1fccf3f5ef1fc2fdb9885ff17fe7f3ca8f0ffb9e9ff9300588d8c6eba4a98ae31fb0cafd1245d4ed4a8edb190192ccc315d0695369ea9abe9ca951658d282f44c906ffe5982c8741f40997ded796699bfcbcea6834c6d747c2a34131b64a3c05af87f762afc3fbb2c33e1ffd8f4f011553a0bf4036d7ae80d103f4c27008f7ee84fed6492d58a1fbe730613053250442503659271252e1ba5334611684097d22ae907ee524d34864b30e88b4b16e2ab64949e32015d66f6ae580ebd5e5cae90b35731880b654262d0944d35dd2e34741a97c6d2173e0cbe228019020934980cea400b2578ecb1c7e1fbdfbf0f26e8a1c5d4ed08b359513e9e886fcfc21e78cb8d6f84bfbbedd3f0c637fd7624e7703410cbc1e32b8da9f561904e086b4f5c03e75f703e019970269841a8b3ab9bb26761bbc58b17411b66281a1aa2360b7b17c3ddff711fbce33fbd1b3efe177f093b766c8f1ebc5455c62eccaa84eba6014493854e6c85ec8380537a5d358c4355841304d0d8f1ec4ef8f8fffe4bf8c427ff2fecd8be0330a91402c96a350d42431b45a48cceee8559a76a610dba1774132f040611404ecf4510800c334cc93893cf4c104275c6c76af0e31fff14fecb073e083ff9c94f23db0e69fc1888224395854c93b223b66a95f1aa775137fcc14d37c017bef8b7f0b9bfff1bf8f23f7d013ef7f9bf851b6ffcbd682dca34d2c8e8307cff7b3f84fff9e71f81dd7bf76aff34b2a74d87842a7948d98f223e9572458128a339a88c63256823e01b90fdd2670360876bbba06b0181b1464646e0c8913ee88b1eee0aa1cacee1f9f1f131e8ebef83a1c16102720df40f40ffe0207da6d29dd13a752ee881d19121a84e8c476d0700cd16b37161f6b2c181c148962a0193d02ef19d32d745728f45c747c7c6c9d77a225b28951578ac94411dc87324925b952914f44e7609d9e268a552824a5b59630e2501bae21f456476250d4c9531d08d815834921153e9a88e6f0c888a639c06b8726c531c92f52be9180cba14696864d4920c90d5b18b6d32c05291d1311ca3aa335b050c3435caafb26cb4de5a3e7ec5fbaea11302a2961220a434f610fc8c99d36a1c3fd566509f548e638659665013c75606d8d2dc3480eb685fdfb4aff9e6cafe9f37e1834d06bcccc7ebff534f3db56999bfb2d2a5975e06e79f7ffebcbdfebff8e28b290b63b3e9e9a79fa68c1a53a5665dffdf7efb1dd0dfdffc52342661bcc08c4eb6cf14bffffdbfff11e879f3cd37e79a05ebce3befa43284c742adf4fb7f26e9777ff73aca8e9895e6d2fd3fb4d597bdec65d04a84d9856eb8e1c66965646905ffcfd2374d369cffeb5fff7ac883eebefb6e30e569b5fb7f5ffdea57538122cd20cc18f7b18f7dacee9a65b6faff54c6cf727c2a84e3af5bb78eb2f1e671fd6712de4fbbedb6dbe88fbdb2d04cfa3f5e6be59195d0ceb4da8afeefe29dc5fe8f27cda5fd7f2ae367393e159aabd7ff59fa36926d3efdfecf4285ff17fe3f5d9e85ff4f8fa6ebffa94ff21a2d90cd2c2b9fa93d58970d8f6791d33c7e2cf298641b47d6c099365ea37965e5eb325053c663096ec74a535dffac6367b183a9ce3fad9d2d57165b9d2ed99b673336d0c2ffd3a9f0ffe9d14cf83fa6a2debfff1077522f24fd109f79f1bbf9e20b84d00041e1317a308f19b41074a541000197f293327eb8cfed4d7e2c0703b3ccf11908156b41f3633ec40b14688aca5499a02e803a5014521d408cf94c5610bdb87c97d0f2c465178502ac252209951907b8ac56896017c81d33087de98bff083b9fdba5321799cb997169713ca52b2040ce29eb4e82c54b176b20448d5ea63e42a980350ab1111290e505e79f0bab57ada01b41a5b202516ddfb19b00342aab510916f6f4c092c58ba1ad7d01dc7bff83f0671ffe08dcfbbdfba13a11467a6d8fc6c30c3a6a7e4297b4931ef08fca76a3de4382a0a06d29a885d499b3281310a92d80a3478fc03f7ef99fe093ffe7d354ae80ca406266844095171ca38c4715e284997c185c4670a6400132741a20b5def8164a9811f895542f1c72c773bbe1539ffa6b78e4678f28a01501c7aaa473e0a275a1b26b05a451264c9a8804c78c4f6f7fc75be1fd1ff8cff0c2cb2e8595d1c381b5279d08575ef91bf05f3ff85fe0f7de785db45eca5f1014f56fb7df095ffcc297e1c8d1015d5ab0a47db8065325556e2ea4d28d1313e304bc52d30bb58dd7b43a43ca5241a097e83302a0700d464646616864183a3b3b286bdaa2de853a1b59a0d6abd20623c3d18d475aef12653d1b181aa6beaa9c1c4193221f1925705fffc01054ca9885ab9dfe021853f86349012cb589a51bb12423da2f6a759cb2618554961065c27299658e379e10988401f5014b5c26655305f03fa957ae9e12bef8d6d686a532db8895b49a11d44a83dac82769dd95ee380b14798fce44c5b1cfcc24087c5e4bcb718efc5b9709e4690a064f81f2633a2f8cf2b0469c0d0c45f07a0bcd3756904c00a61cfbf065966a3563b8e4f8ac8d9be74cfd35208b4b0822bf40970f159cdd8bd74083c1f01bda5868668d0bea173506a5456d8e1c3e322ff7ffe371ad7cbce8e52fbf86ca64f968ae5fffe3838d56f88b7b93302319ca355fafff1104809958f2a0871ffe191c0f3aded7ff98bde8c1071f80bce9965b6ea587a43615bfffd3fd0d33d4e6996defd0a143f0ecb3cfc254a959fedf2a7b22c69e77bce3ed70ac3413bfffb96da363c77aff0fc1bfad44787d72edb5339315b159fe3f95beaee3788d72bc330e6621fc4d8fd7f42c4f33f6ffac7c714f7ce49147214fc272bc1ffad09fc2b9e79e5b777c36fbbf49795cffe31f84e4993192e9b1c71e83ad5bb7b5ecfdff99a6ddbb7747f3df0aa60cc5fdff6c54f8ffd4692e5fff4fa5afeb781639cde3c7228f49c5f3bfe951e1ff53a7c2fffdc767b3ff072e6669c7ec73c76a5ca651f17b338c2b4d9663e5c141238bfe1af1cadad6d5ce1578a7da97c917dc7dbc7cb24f47c75371fae9ea6a3a7d5c9b836be39bea18ae7ef61a66d9187cb2b9ce17fe5ff87fdaf7d9e0ff470e1f550fcc75fb200609a86325fde09cb9313880b2b4e08374fd009c1f9297743629ce90a285a13635fd603d062d218840260fde192020f578b15df01c406554a1f101e2ac2fd2e011b733ca10f28bf52012a524e5034592d58b7517e8f9511ffd39d62d8d09a6c2b50c5cba4b0324429636809f3ffa0bf8c427fe3f7a7811ca1a2573ca4e2206b5e0f8985967786810fefdee7ba21b9a13dc242e9716afbfd4a0b74890956b56c0f9179e075d5d1d04b2c2b6083e4140d6e8c830b477745097ee9e6e2a11f7ed3bff1d3ef4dffe1c7eb5792b1046439654053d996854eab9410ada45eaf740213ce2cc4fb4ce54464f0003a5109c76e0c041f8da57bf4199b08e1ced27500f1e47f005965054f30f54b6a1506516427d109c45685b77c56961bda6427acdcd1f230cce19199e807ff8877f82bbeffa0fa855b14d323e358fed5a672f425690ac0fdade2b5f7935fce17bff10d64437c62b15041086502949caae74f2bab5f087fff95638efbc73080c85fcfb0706e14b5ffa0afcdb1ddf81b1310552417dd4b2ee2d56ac255d56c3a42c68641704cc09445da93a3c3f3c3c42ef8391fdb15da2318f4537b0313315faf940241fded0ae453cc7c7c669beb87608a8c261bbba16d0da22986a7c7c34b2bd76f26dcca05589c6c1f28363e308122c477d2ab4fe63631334cfa1e141927d6c748c4a1aaa794892b7adbda27c96b2f9d54f39109295af75cfcb6afec0d43eec301061fd239bc4228588fcc30c7594fd0ae89db2b0e9f29026888b6303be07411273d84f24400234953201304172ed149b2fb61322cee0171876cf80d59211b3e269e3faea38cc7ca5199b750c0ead2c587564c8ccf3aadbeff4b140cf291e877f7041529694178acb3032b08cf7073ec67e6fead2b4f6bebe81cc3fe84c1eaeefb37dffcf83f0a1de8d37de50776c3e5eff5f75d555d02a84198666ea6133d26cb8fec74c41e79c730ee441bffef516b0e57151b3affff161e0a38fe6fbb079c386d3c836e79aff371ae378fdfebffefaeb73057bde7fff8f6685ffe74937ddf476ca38d26afe9ff5dc74dad97dd6ad5bd75259217ffbb77fdb9b716f36f9bff93ed5b541105a1a507ea6083342eeddbbb7ee58abdcfffbc10f7e005bb63c0579d225975c0c575ffdd2ba63b3ddff6db99a7dfd8f20d85b6eb985b2d3e649070f1e8caeb91ec9ddfff11e4c777737e4419861cea656f17ff37bf1fc6ffaedd2fa14cfff5a63ff3f5e34dbafff0bff2ffc9f7998efbee385ffbb6539561e53f1ffc0ece462e4a3ac4e3115c74e3b963686ab9dbda02e396c03cc6a4c69c657f750730afab379376a6bcb6ecb6f8fdfc8c05da8409fbe1bc9c363bb749a16c8b30601df83a9a9ac61960096a58f6f7ebee096c51f7cf3e363e6daf8d6d315084cdb347555f87fe1ffb3ddfff1d8a14387d531218cd2800a88011aec83e5e1402665fb18040432015349fd80dc2cdb1703028c31394316e9142ce09309e2627bd1328746194324b334604dcb573233b2405226904154dc3ed0a51679de744ecf9d4b6c093d36af0b97f10a2d7b218084d61d81061008125635b8c10019459fcb950a7cfb8e3be1f6dbbf13c92c757bc854ee4c10de4bcd0dcb0622ff071e781876ecd8a9b35ca9ec5242e313c2d8a7808e2150e58c3336c2d2258b0880c5d99916f72e82a3470e5259c3eeee05d18de91e2a4187e0ab4f7ee2af60dbd33b003355c91a6325d847a40616e10038d7d021b58ceda4de3784920b5416b118d2427a2f636e2bca98f48dafff0b7ce1efbf48e5e76a98c50acb09860a84c7fa55a5065576291caf1cb5c1928f261885df6d00cd54c8b4059e97d0c71efdc52fe09bdff8168c8d8c83e4ac637acd49e040c6202ca58b300627e2b15234adebdef03b91eebb81927d095c2f6c5b8be683761ac229ebd7c1eb5ef75bcac74255720dd7fe8bfff0657876fb76add3ec7ff9220cdf8ced2ffa5a2a97489704bed24e8025fa303b15d2e0e01001a9aa1355e8ececa4b26f98a10ab372a1ff757674c0e1234760d1a25e6a7bb4ef080c0e0d13406e7c7c02fafbfa61746c02b66ddb0ac38303949a7f7060009edfb52be233026ded1dd40e415c478e1ca5b28607f61fa02c5a2a1397b275ca8025b124e318c5271c6b343a86fc6ae144bc6aa63a24e8b822a02ede61e9ca50a798aa331b6d33757a234649032913e0555d5d4023de524c0aa596876394f627486220684014e8d82934b04a1a590663ab62a0149f13c2c8cca7ec8ce3b4d031178c6b1bce10a8409df5732c9512ff89eddeb61d7d5c703c149301acbc1f08431771c62abd97108017cfe998ccf286ba4c2203c8c8c7a50268723ba975c3fb0902b0e2b59ec7fb7f1e840f396eb8e18649a52de6e3f53f969bca23bb848baebaea2599dacde5eb7f8c67679c71063d786a36e1fe65cbc394e7f53f82af8eb584dcb1103efcffe8473f429fe79afffbc8d5de4559e68bb471e346b8f6da6b212ffaf9cf1f9d524925a466f97f2b10daf82b5ff90afadc6afe6fb6cd1ac3d3daa55df760f9af73cf3d0f5a814e39e5140248cf05ff6f742c6d8c3c3375da3163a6f77f573bd7f5ff430f3d44bf1ff322bc867ecf7bde33091c38dbfdbfd1b166ecff68effffdbfff7758ba7429e445789fe4739ffb1c9872b968a6fd1f4b90e2bd9b3ce8fefbef872d5bb6d41d6b15ff9f2bbfff0bff9f2cdb5cbcfe6f742c6d0c57bbb9fafbdfd5aef0ffa46de1ff93db15fedfbafe1f3462d4488846c7d30cd4671c8d84f68d65f2b215d1688e2ede3ec7395e06d228104e25083692292b5f53be46ed7cfcf9785a2031e5c862d0f678bef68d28cb7cb20697b4f3b60d9ac7ed763e6ae4e0e6bb4f2fbeb9a4b5f11d73f16974bcf07f77bbc2ff8fafff873a530d1e91ea649c6589c6e0633c2fe661f0a1925942e8927b22ce5085fd9827b6e5b2530c7caa9b277e16220657e1196aabe74a250c19e824ebb3a898761103070062c081a9095ba32ebd084337316f3db68b479dde81d75ecd80b38391264380da8484febe41f8e6b76ea79b313095544c42f1270043887f4d7708eeb9e7fb303a320a824a132a5087294dcc3e3ad7d1de01175c701e011a8606074957950a662392046ce95dd80bc343c3502a57e0ae7fbf173efef1bf82e79fdfc3399bead8d64f1a4b34868e133e1d59b14e085da6915f48259219332dfdd357be019b9f7c8a1e6887b52acd87330be1b1249b10836b500f21cc14e1100442d380bc89892afcd33f7e039edfb5373ad7460032bc4494ce0c4a891d85babf9a4f29bae9daabd74f6a602282c9124e954a092ebaf802d53d543a0a23753cfed86678f08107617c6242ad65ad0a5325b4d1189088403e01541e90323c45322170106d657c629cca47603b8c1b280a81ae3a3ba83dea02f9ecdd77806e640f8f8c40d7826e02cf616943cc6a75b4bf1f8e1c3d02ab56ada6720b880ec4192ee8eea6b51f1ce88743870f51c9ce725965e3c3f283d81fc74090d6d87895ca11a2ddd7aa350283e11c4647c6a29bfbe3ca564205769a345703eea823a80234ea03a4820c3a33f93300547bbded3175c4e1212ec5aa635f2853fe02c4dc53f5772913b068bc6f73494221ea805a71e941bdce313055af3d709cd3729be7e32c860cb4b5f6a7782fe01f47d6dec42511192826594f3c67feac01573c4773ef50e7c33a101908031aa7bff747b6a5d6657eefff791096c4bae0820be2eff3f9fa1f1ff6e043e7bc09e5c0f22b26cdd7ebff934f3e3997874ecf3ffffca463795fff23f0eaeebbef8e4b41e541975df622022a9a3457fcdf47c7fbf73f5ec3a37f6386b73c08ed0833da30b5b2ffe741b82eae2c4379fbff5429cb5e9e3626ee85f8c73fad40175d7431ac5bb78e3ecfe7fb7f08f6c9232b1982aff6efdf0fb63c8de878f87fda3e72e0c001b8f3ce3b214fc252d198b114692ef97f569e33bdff5f7ae9a5f09bbff952c893b004df912347ea8ee5e1ff79d1e6cd9be12b5ff94afcbd55fcdf1e6b2a7c4df9b25c7fccb7fd3f2bcfe2fadfcd6726fd7fbefcfeb7dbb9be9b634d85af295fe1ffd9fbd854f8bf9b4fe1fff5730fa6cbc85ef82c0ab4fbfb1463f26dc4c325431a8f346777c93713d468ee765b931a056dbbadcf011a05b6a9aea78b874f065b1e3b10dbbcecf65937a5e9b6b1c737fb647532bbbd39bf469bdf74c8d68ba9d346c1763a3214fe3f7d2afc1f268d75acfe3f3a3a1edd0ce8abcb3cc2ef094044678892492615ce548512720613603fd56025e65533b2b7c4d94f74df586629e36c2c783e2e41a58457e345e74b22c96e45fc5917fadd2c6d48636a9958767c60618e6d525dd62d5e47639e0c24209ec64b1a75ce289b53a04b92812a0967eba41a56a3797079329900d61a10af89925fc293bfda02f7fff081687c04fc20902a7ac7cc3c52950344f110c42334aa64e9b225b078f1423861f5722ad3b664f162e239343c4c0f6a07070760d5aa95f0c4e6a7e1939ffc5474a3680725711298bd49b834866b62bc20bbcda7932a5a864b8099893053d217bff44fb07bf75e9a07ea0db33471694cce88c33a0a4ab1c260268800360456529781dbb7ef80dbffe5df00714f94340ab3746930d96492b19a94ef08e283c0a5e7773d1f8f80798f48ed2a2d1b9420a0790ef40f8232eb40b38ad66f6838ba717c371c3e7228f68b29cf097d2328c7326219417a38abfd97c131282781a1c615c809415ff842d013fd557324d7bee8e6fa896bd7d0f92ecc9075f808f1c236c323a3502e95a112bd26266a80f99ed0efca9532b529e9588299d8b0ac269618c4f9a1360706066024ea8f60359401db23400f65edeaeaa4f28798e50dcf293c6048ba34cd528091214abf504e7c18110805eacba62f8833cd49598b634d9cd5cc935f4d72f638a133b7e978c4a50003abdc6bbc1fe84103e34788306221035705c724e9fec119c72d503109355433405bf482c4264c701758d76175d7771ab41b1ab11e0c3e1cab59e6c028aba899d11b8328f11bfa37c56dcd97cef33ea433830948e222da687fbf2a7b395ff7ff3ce9ecb3cf860b2fbc30fe3e9fafffb1d4c9b265cb206f7aed6baf850d1beacbaeccd7eb7f04fbe401c0c2876d838383def3795cffe3354b9ee5071198f23ffec78709b4e9a2d9eeff69e3996d8fc7ef7f8c352f7ff9cb210fdabe7d3b3d50656a25ff9f2939a642b7defa6eaf8d33e5e1fff6b946945597aeb920e135765e19084d42c0d11bde707d0c3c9a0bfe3f5d19300b8e09986f16e1f5b95d82300b1debfedfa80d66bf7aeeb9e7202fc27b307ff2277f12972b9d4bfedf2ad7ff08047df7bb6f9d94a5b7d9f4a52f7d89def3f4fff5ebd7435ef4ad6ffd0bdc71c7bf65ce9e8934d3fe6fb69dadbfff0bff9f1acde7fddf96218d87afad4fbe99a0c2ff61d25885ff43c3736954f8ffecf4ff60aa0e6dbe73dbe91a8d6f0cd368b218ad8f5cf398ae52b338a12db38b8e5557e6188d0256dadad8c7a7a3177b6d5ce335e2eb0bda69743c1cc3d65d9640c27699658eaeb53179db3cb206069b77da313388d8f6d2a85fdab885ff17fe6ff26805ff1f1a1aa4cc32d8d70440516928fd9d4bd995348808bf13f825fa5cab56d5437ae46f3e24d7800d53df9c618741042523ab544d8fc9f32869708000e362145fac3bd025f6f842149207f12c83d4e3950c3d87e605821009d0c178d534d88b337a71f942cee015c7239dc186cff3f86135d4101ac5390147a8ff95cb15b8e8a2f3a1a3bd1d082c65c851bfc8785a6a005488702ee2a1641470fffd3f816777e00dc380fe911e02ad1c832365cd8a96ebd4d3d6c329eb4e2220ccc8f048f43e0a232323d01edd14c77284cb962d85fea151f8c217ff119e7de6b9686e8292dea88c4155c513412dfc8a879175e3a5932ac5a6d681d716ea5ed44672462f11ddb0adc277efbc3b9aef8f8171796aed81006ea893385b19013302cfc8a0ca384ee36502dba87c6049adeec44408f77cff7ee83b3a40e02bb4c46a0d3344e94c6e526b07fb0bb4e968dec49433cc09d225ceebab5ff93a8c46eb02a1a0f142dd47eaac5943834370e777ffdd144aaf75000f3ffc08fc7acb33d1e1929ea98bea26e4b898573ec8653ea9849654e50e6bbae423fa00029e80fc2180d1d131686fef24594723bbc2cc6a63e313941dabbf7f008e1e3d4a40aafdfbf6c28e1d3b60dfbe7d303c3c04432398c56a8cce1d387090ec71ffbefdb07fff017a605d8e78f7504630093d3d3d51fb710249e1f723870f476d0e51d9420431a11c089ec28c706d912c986d0bf9aa346191c6e3b590ea983055a4fc1a7953294db6c106e62c8d0f584a94ca3502678eaac5805170fcb8512f2ea3a78183a0e2ab19bf9888a7f9592620ae9a91958a3f839449f62ba9e20f7d36ceb30a28839612ac7e7e06903534e225cf81c700599f59aba463265360c64e431fa111eb59dd04b8c232b34686442e051bef233aae736c373368a1fe4646872943daa4f59a07fb7fdef49ad7bcc6f980753e5effa31ef00167de74d96597516c9b0acdd5eb7f04426006c5661302854303a4df0ad7ffdffdee9db966bffa833fb8c9997d65aef8bfabed4cfdfe47ff3eefbcf372893798e5f4c9279ff49ecfd3ff8f977ea74b2afbe06b261d6f05ff9f2ed9fee9f251571fc9f70c722604609c73ce39f1f7b9e0ffcc9bf5ccdfcd771fad5bb70e9a4d58aadecc80d5acfd9ff9f8aeffffe11ffe01f2a40f7ef083f1b55a71ff7fe6f67fb4f99b6eba29d712e18f3df61801118f174dc7ff57ae5c0579d1e1c387e0c31ffe3378e081075bc6ffcdb6b3f1f77fe1ffd9654f3b369ff67f17cdd5dfffcca7f0ffe953e1ffe9fdd2c62dfcfff8f97f90d5414ca5d8c2b89495a6b83401d968b23884d9e758a8d1e24e670ed339379571d3f8b8825a96b65369c7eb6e3ab96bbcacfc6d1ed3e5e3a26309e8bea093769edbf87464f3f0f1b4e548737c3bd8da7d5d32d8e3baa8f0ffc2ff5ded5acdff0707875596236d7b55e3c138138301e806aa540fbc6b9cb1853391181759f4dd0025819629ce7ec5c7f46b92ae79ad8c8756d8c2ccaec2a506ebc001c61a935c4636156994dd8a75c559bfd4012ab185c7e8a1bf713ee452615a0ee4c93a913162235e047a0b5543553e4e60e92c05630aca01742de8848b2e3c5f81a5a42eabe503cd1060a61ab54540db04817710b371f8c8107cfb3bf740ffd17e254558c51c49540a30002c0919ea6c61a8f810ba7abae0252ff90d7a0019e8a146a80422025cba61e9922550aeb4c337befe2df8b7dbbf4b99d1a4cebe24118cc71010ca12a45fa05f084cf2007fcce62a699324dd206087d6d428d7c62f5c277a11d809f99661dffe83f0b9bfff12ecda7d00aaa0c652199354b6292aa518a8e34af7aed27304c799d64b2dab9a2fd95458a363cf3d7f00eeb9e73ea811604a016ac8666418cf41cd15fb49928b0041a40f41bc6b52d9e3bdf7fe083efbd9bf87befe7e9840d053d477223a87ef236313705b74ee5fbe75bbce6806b1dde358fb0f1c84077ffa30885205425151a0ad585ec3960cb01cc91a673f82ba2c62d804c17908426c8f6c86c092d1f9febe48b6b131a88eabac575d5d3d3030341cad450dc6ab128686c7a0333ab6ffe021181c1ea58c55ca15242cea5d4860292c13877eb177ef1e18891ee00df41f8583070fc260c4074b1662b6902347fba8741382bf06fa07a29bf663301cf11e1919d67e1e405b5b3b95df443bc0f1f0e1cac8e81865c5aa128054aa979a955afdb85c26683f500f86dadb3a9425720ccb107e29b75cb49694052cba992af41882b2db09f26dc2ed71dc114a0c15c7948ef5a1baf84631498336c18895b14842035c315681712d41632420548a831c1745922d2b5e6350e0008a93d69ec4e0d349f152f70f0162e06d7c5e1a405823269aa031695ddfd474963593470c3e13f559bee8b3912d5004a24e47d806cb568e8c4cfeabdaf9b0ffe749f8b0f9452f7a51fc7dbe5fffaf5cb932b7b2604c8b162d826baef167c6998fd7ff982da8d9b46bd72e2ad5c672e47dfd8ffbef4f7ffa53c88bb0c41266a1412a7eff1f9fdfff5886f0924b2e813ce889279e701ecfdbfff3de1bafbcf2aa96fffd9f46c7fbfedfc68d6740777737e449af7ad5abe3d2c073c9ffa77bff2f8ffd10ff9807cbfdd900e066ecff3e3e5bb66c81a79e7a0af2a24b2fbd2cba3ff392f8fb5cf4ff56b9fec7dffb6f7ce31ba924755ef4c0030fc0af7ef52bfa9c97ffbfe42557419e8420cc5b6fbd053efad18f5246bc3cfddfee9735be4f5596f9bcffb78aff9b7de7fbfe7f2c341b7fff17fe9f9d0aff2ffc3f8df2f4ff001a502305b89ccf5c6c5399ae3e6994c5e9a6eb985309523ec3b59de25882844b365710b2f5ed6aeb92d7b70659d6c276265b0ef39c4daea0d02898b9f84ec7c9d2fa360a88f6f1b400e6d287796eaae4e39575cdb2ce6d2ab214fe3ff958e1ff93dbd8d42cff1f1b1d25f004603bebc227b647518faea6ac583a43161e31335111e84424d9a062590cde2819f7e3f28471f62d30b2a3e8ec5bd8bfa4797066ae3a3b120a10c519b4882f8186b44f817b1d65a2ac980f68d9a49e1b03cd04f14fca2272b61921757fc554f110098083f84a0d5e08221e3284c58b97c29ab56bf1ac02f884b568ce93b311287d1be50d0341a011041a3dbffb79ba6948c3f3d8924162c99aab6c6301ac5db31a4e3b7d039584abb4556051f470b8b3a393cab56189b785d1f76dcf6c877ffee6ed303632161750cbf83303a64242cf032d8133075913375efc51c0a38ffc1c1e7bec091a2fac712621b62fc740d30fdf2ea9e9ff811e53d614c86a4f74e3e8e9a7b76bc49da2c006e581cae6a53268a9f96139c19a946637a84e54e1effee6efe01fbef0e5e886d441181fc7528012fa8e0ec277befd5df8bfffe76fa8c49ad0803d096c1f926e66fff28927952f050d2f4fd58c30fb514967870ad5baa8cc7640251e116085baedc3b287d1fbb82e3fd819bd0e1f394a99cc30db50ad3a4199106a1358fa2da49872f8d0212a69897e52ad4a8a3108b0c2ac1798f9edb9e8a1747b647fbbf7ec856aa4cbae05dd11af51025f754436ba77ef7e7a21986afb8eed912e26285b1f82c2904f7b7b1b0c0d0fc6d9dca8c4e178353a361a67f1d2911aea8cc3514a134b222eeced21594be50a64311ce4c8e512c7c6aa24573dc97838a9ed98ca93a2ed8632ce1088a748567c997156c74630e21cc653ca5865c42a06ef81712c9651883836737c65d0aa99392ae4feda26787c8e993c4f06dbb246cdd82344922d91daeaef350d68ade9f9c6f361194ba538f395091ce3f81e1a2059de6fe27da2aae23d6798117a9c71abacc17cdcff9b4d58deadbd3dc9c0525cff4b584b7b7c7e74ce39e77acfcde7ebff6613029e707f6c95eb7f0484e103f0bce8aaab5e42be51fcfe9fdc76babfff972c59022f7ef1e59007f93260b58affe7452f78c179f45edcff539f172f5e946bd619a417bde832efb9d9ecff264de5fedf09279c0079d0a1e8f721ee89a62cc74ac772ff6fcb965fa796099e694240b259aa742efabf7d3ecffd1ff7cb6bafbd16f222bc26c4fb7700f9f93fdadb89279e0879525f5f1f7cfad39fa6ec6f5bb76e8363a163f17f573f93a7dda6d57fff17fe0f93c69fcbd7ff264d65ff9f2acd85dfff85ffa753e1ff90a97d165e85ff4f3e763cfc3ff509d7f108283e1e8d789b8b98c5318f55d6a9f4f70541e6e39337eb188d8cdd653c7690cccad3452e87e1575ab049a3b4f3530994667bfb7b9aa31fafcdd145beb9f974c5e75c1b5b1a1dcfcd3e8b3d16feef97cdfc5cf87ffefe8f1967107d2134a8aac499a38c17685ea1ce80c20fbb81c7d7c7e8bbd4b977ecb1b44c52b79706f08a7463b4e3ec2f0c24a9e971f9a1be99c92a30f45ac607f99a6768d9970b9432c9178cf9e219bc59cce3295098016ee13eba2c21c804f6a4b23c85f519b4e844f45e0be194f527c229a7ac8bc75136ee90cff11db53b36310e3ff8de7dd10dc3212587ceb403247f7d1f06e99c79e64602f87475b553d938cc7e856592dadbda2379d6631e20f8d4a7fe06b63df3ec24f809c9384590959f14f8086d05415841e0e0cbb615db5840fa41a0d8b7bef52f94e106014281ce7e35937b947b0ad1ba95d4b83b77eca49b48a1a19f30c91756df4d03b3f89f8b76ecdc0d1ff9fffd6ff84fef7817fcf987ff27fccf3fff28fcc1dbfe13bcffbd7f0c070f1f8eec0759a812906437980d0c4b1b46871efdd9a330318578051af482af2a010005019ed06611f0d4d9d941f296235d8f0c0f933f205810dbae5cb91cdadadbe0f0a183d1cdc43e387af408d914961c2410ccc4389526ec5ad01bd96609ca6d0ba0a777290c0c45f1a6d40eabd6ac8bfcb50d96ad5c038b962c83ceae6e0822fe4b97ae8052a90c1dd1d8279da4408a98c905e77bb4ef28dd24efec68a7713ada3b229b9e804a64c7e8f3584a13415cdbb63d0b5c6d311257d95ba86c3870d831021271ae2a0ea9c2a78db4a8bd9fe2c2c8f060a4b7d1c96da4b275cec617eaec68380edaaff27b9d6d8ae38826b2695dded48c4f71d63fa14aa49663005d025ee2bdc60459d1724bd5afc425096552e20f746ce5cf35a384ac66ace4d0af40674403631fa0d2959a9f347400ccdf88b7357d9c330db28eeacad44823de0a0d40d3a56f89933e57c2381d1d47be18e3102868af03c0dcdeff9b1e030dc235bbfaeaab61e1c29e29f59bebd7ff98152c4fbae28ac9808cf97efd7feeb9e742b309f7a418806a511ed7ff08c0c232bf7910fac46ffdd6abe973f1fb7fea3cd2785f7ef98b73c930b475eb567aa0dc88f2f2ff3c081f6c5f74d1452dfffbdf757ca6eeff61e6a9ac7f1c321374e28927c1ba75ebe058a895fd1f89edcdd7d6b6195c933c32750e47bf27edb2bccdd8ff5dc7c6c6c6e0273ff9f1712d093715c25871dd75d739cfcd25ffb729effdfffaebaf870b2fbc10f2a2071f7cd07bae59febf3ebaf7d70a74cf3df750362ccc0c3666fc015533fcdf3e9785a78b66e3fd7fd7f1f9e2ffd3a1b9b6ff1f0f7d64a556f8fdef3b56f8bfbb6fe1fffef1d3da1c2b15fe9f7ebccc27a7e2a053ed633b82a9dc2cfc6d25bbbebb9c2d6d0cfb9cc9b351b0b0e54a0b4cbef15cfcb2926b3c5b26176ffe9e45d6b440ebe33b5df9b3f2326dc7d56e3afa4f1b338b1da5f1c9ba29b9f460f39b8e9d34a263e15df87fe1ffbe71a62afff1f07f045c843ad30a3d90c7f6e6986a206442dff13c3dd847408106626159317cf0cd602ae21586f1cd567bbdb5503140409f8c3355214000b39a98d95e38930b7e16e63cf4e7d0f88cafc0d019c92326438862bd80ce74a5fb31002cd480023c2f8dec2eae0b8950cf4170961892558312701e980d06cbcdd5aa70e69967d08d4fd55697dd32806e898000269c014f63c6a8be237df0bdef7f1f08b485e5ed786e12b40ecd059450a90470ee7967477391d183b7a3d0bb70216530c28735d8b4b3a303be77ef8fe0bbdffd0f02b4d4e1cba5d2bf42781d3f42604a6c0a93ce6a78523ca650001a5182871ffa196c796a0b5c7ce1793a138e8841737212a7106682d432a975dbb9732765604afc8a56c0bfcf6abb507e60caabd6190f0f0d0ec10f7ff86378e8a1472847dae8e804c850d04b73d25d852a6b19a832877bf6ec81f1b1092875b4412965b9a4911b8ad3183180054d0a7d99e30196d7c3ec4ee893a3d18d319a6bf4797c6c1c864746a1adbd1d06227911a435d0df47d9ad108cb57efde93014dd641f18ec83bebe7eca54f59c2ec58499a08e1eed8381814158b8b01b7a7a7ae0c493d612982808da88d7c478558104cb65188b64a8964ba43a95310fb36c4d44be50263058b91a795649425ba51cc5b3717a1858ad4e5076396140a994caac1f63d1e9aeae6ee8e8e8a41614b7404cf645970d48c515e7890f148c51124dcb642c298501c2e316107bb8b99772f9548a1b512ce43843653b8c7d3a4c2613971164d0168353e34c843c168fafdf39ded681bd744c2d9960584b27c8b564c450b619a1e391d4fda58edb3c479e178f1fc63a95711630d0fb08ef5b81e60d7acf90c65cc1d01d969e9c9c8d2ce13f57f7ff3ca923da3f4e3f7d63fcbdb8fe579467062c04ba5c7699caf6515cff27bc56af5e0dcd265f06acbcaeffbffbddbb72cbf6b169d3a649a57f8adfffe9e398fdd2fa62369b55ab561120aad9b47bf76eca6eeaa33cfc5f88e3fb9b652a847106b34232b5eaef7f9b8f6b4c1f4df5fe1f669dc16b85bce8e4934f82e5cb8f1d14ddaafe6fb6b3e5f47dc71766c2c13f226a26210018ff800c6da299fbbf6baf41c00797836b36e11f355d7c71b6d2b1b3ddffb3f269d6fe8f31fa8a2bae80471e7904f2201c17ef0fd6fdd19141cdf0ff17bce00570df7df7412bd0e6cd9be1965b6ea5cc6408c6c2b8d40cff37df5dbce7f2fd7f9b8f6b4c1fcd76ff9f2ecdb5fddfec6b8f9b2693cd33abad34ebfadfc5abf0ffc2ff8f950aff9ffcbdd9fe5f4e5be4468ee0ea6b4fde3eef6ae313d8e7648dfabb0c39adbdd9a611f9c6cfba683e59d2644c0b36a6dc5375f82cf2a6f131bfbbfaa405b6a91c778d3b9540d7689c467694d529d3e6e41a33cdf71ac93895b1b2ceadf0ffc654f87f6bfaffd8e8184c4ca887fa5ce6af4e369d8585ca48499d1fc62853452098b05ea79cb18a7800c4a5a86c194c3000f6e56c5360c858a2ac3b09408c41140cfe2279a48cbf9b59564263ad6df0058f1f1a73a5318402614943569a2f0133b4cc46661ae2c1fd453d7043685d0996b7a44abe5df3f297ab520c94c568b25c756ba7411c344ea8f8edd8f13cfce2178fa37654663029eb4158526596224852f4deb5a00bced8783a54caf817af9d303a360a6bd69c0083030394896bfbcee7e12f3ff9d7d10dd161955189814fb26e26fa650aa733e068084962cfc69408c0a5d582b219a50d45dccf9ab400c617998788d7f3cfef81471ffd055c72d10bf82050793fc816878e8594ad8671f94c7c3f70e0208194d4d2932573eb49fd79add97712805dd21a0176229a4fad1ac2686d3cf2394c7915a84c47da5e68be3ad599528124431e191986b19111e8ec68b3e44e7c1138fb96b1ef228829d080ca8eb60ea886d14dc1c8a7115824b56f61e941cc7e5559d84337eb115085beb977ef3e7ab8bc74e9322a29883713abb204cf3cf30cecdb7f107e78ff8fa3cf3b22b946e1d0e1c3947d8dc05135b5eee54a896cb4bdbd1d16f674c38927ae8175a7ac83934e5c0b175f741e953bac44868b19b9707cb4592c3f883cd62e59138d710026a287c903d1f1050bbaa1d2de456028b62f054e03ea8b448021506ba000a23558b66c3165dc42e50a0100722ad74532d2c7512ab7a8f26b090daa5467296313c5a21a30da8a62053933758f62634d65c11249e63e2aa1aae3aee4582344dc4672f629d0d906a5915d90fd906d4aca49f1d52c2788ed4b3ab31467eda3b802c6de22eb73b6c55910f567b405e0ec8122b169024871495930bcc2b8a68963b319fb8df3a0e7426bca7b10f116b11e38264f54ab0a4008f36bffcf933036d80f9b91e6fbf57f9e002c8ca7f86292b2b8fe47c207be7951ab5cff3ffa683e0f1cd11ef1e11a0375e6baff6719c73c66f6b5cf67f15fb4edabaeba2a1700d6cf7ffe0b38e38c33bce7f3f0ff466b399374e9a597d5c55fa456f17fd7e72c94d5ce7d7e81b46e5df47b73fb76c883ce3efbecc847164f3a3e57fc9fcff9ae7b7dfd972e5d9a4bcc70c962d24cecff363fa41d3b76e406c259bc7809bce52d37d61d9bcbfeefe297d7fe8fbf795ffce217c3e73ffff9a6031091b00c34667ebae69a6b72f37f04c4e31f62e6597ed3a40307f6c36db77d16eeb8e376b8eebaebe1861bde4c19e266d2ffb3c6fc46eda6e287fc7dbeedff2e7e79f97fa3b1b2ce6db6efff661b5bdeb4f6669b46e41b3f8b7df0e7a9ca58f8ffe4710bff2ffc7f2ef87fd99ca8cb00ec0e26339700360f3ed64858d7a236fa9e45b159cedbf3735156e3b4fbd86db3042f5b17598392ddbf91034d55f634674d1b632a01cf6777693cb36e5e8d3609df3af86430e5f68debf30b9f4f6409e66963350ac22edb2afcbff07f571b9b7f2bfb3f66b54170026749894b5c197d090410aa927a810644099b17678d9232396f94ba8a815352262029fdf01cf9841ae004d2c8a22254892f066fc520019994a6229984a85f3b6164e232bf5b64972914162801fbe367a10104ac0fe6c7ed020d90a0b6c857a7a2a20c315a4e6c8fd95930dbd4792f385781078c3ea0c77591d04261e620cc51f6e8a38fc291c37d3a2b9496477350a50c193c27a9ccdf45175e107d9e80e5cb56c3c0603f015456ae5c05dd3dddb078492f7cedebff0a4ffc7233085956fdf57ac4f39d0ab849cb2aad437c401893a22a6652c4003e26ccd455b75e9439488165aab5906e9686f26d3140a49984f2f25aa0d858c2b3861998c818205957d762b20ef4822aa80e5b5ca8c131aa4d00018d25a04cc76b3504c6d414c847215140655442fdd554d9cb50c64023700eaf81381262200ef641deb8ee08feaaca2a819df0af82f1afd5474646099cd4df3f40731b8dbe0f0cf45306b7c3870e43576727f4f5f7ab8c78911eb6fcfa1978e4d1c7e057bf7a8a4082fd03033031cee5f1ccfd4ed9b52a1727a21b80a370e4481fecdcb90b1e7af851e8eaea84f3ce3d072ebdec12d870ea7ae859d80ddd5d0a248519b51058b5e5e9ad547670effefdb074d1a2e873259265000e1d3c4476422044e5a4d1e792d2000d2c133d46c756af5a493721d1bb649cb9cfb386a63ea5f2de43878e501c65881b70cc84242ef07115a313ffc01e25237e503f8c37daae09882518e8a4faa24f87daefd50ceaf7be38f6e93138db54a8f9d6c52bee6764a4a236fa5c4d835f398ef35ec0b192e53341a9e6f5118d1f83d034484ceb98c6d76d4b3a039b1977982f83d362be5a46d0dff13c67e21a8fd6814b18cca7fd3f4fc207786bd7ae89bf17d7ff861fe64408fe62505c71fd9f505e002c9e53ded7ff988d6bdbb66d90079d75d65904c0629aebfeef92cf9e936f9e7cacd1fa9be3e11e7ade79e7d1b51982e29b49bb763de73c9ea7ffe749ab57af8a3f3763ffcfeaff59f7814632a79dcfbaff379bce3fff82baef73cdff4d5e69df6dd9162d9a0c4a9b695243fbafb3559be3bfffbbf8e505be42bae0820be09c73ce89bfcf27ff6fb427fbe6e1d2c974f7ff4b2eb98432c5de75d75dd06cc29297bffce52f09809597ffa3fd5d7cf1c5f0831ffc005a89f647f7743efde94fc137bef175b8e1861b231dbd1c4e3bed34686b537f5c783cfddfeeefeb97650f9b8aff351aa3f0ffe2fa7fa6fd3f8d1a9db7e7e7a2ac366cf7b1dbba6ca7f0ffe478e1ff85ffcf27ff0f7c83bb16c63579b38f2dbcddd7d5c6359e7ddcf7dde6efe26d8feb7bb7e5701db73ffbe6e2eb67cbd4889f2b58b8a8513fbb8de958bef3697c6ce76cb46e8de471cdcbe7c8598c7a2ad4688ef67157dbace49a878b87cfb71ad9a66b5d5c639bef767ff358e1fffe715d7d5d54f87f73fc1f1f56e321968f1ea843fd3a73462beaa31fc833d58cf256348eea44199924cf51f78f75c0f2e1437c23bb4bc899b300628015a5e8e64c30528308a276783cd4d95a426dcfdc978f230f06371038455a2f0d142b71792db627504008ca40a31415971164fd9940b25083d3cc7642f76359a89fc4bf38db080b7aba23bd6589bd893c42ffc3cc43f7ddf7a368fe089a28e9360ccc08087492d8056616ea8053379c44598e30ed7fa55c896eb82e2210076aece8d101f8f69dff012323e354e2ae1188e79829c18de8294ae7ac516fa10c099ca5802701bd1055f2d8634fd0fc813219099d7908572aa8fb0c3398158b730495821264ddca948d2ab98d2299517fb57e6a0d75162660304a6277ca26b5fea419078238d31c028d8249714db723b096247012d9a31ea7a3d20658d60f6553602ca5ffa37d7d91fd542843de82055d549e12ed0b6db07f60904047b5488e55abd7c0d0f0387cef073f82cffdfd17e18b5ff8327cef7bf7c2e123fd04be129c3d4d06b45e28bb020aaaeff8a2629c354989a1aae335e83f3a083fbcef27f0d9bffd02dcf6b92fc2233ffb4534ee28dd8043d01766c5c25830363e06cb972e85f68e0e027bedd9bb070e1f3ea2b2d8d11ce3c92b1d833a8499e870aee8332b56ada42c3e4237205d34724f916481dab76f3f81f0ccb5537142e87515f142a0feeaec92175eca384e2232b1c4a02363489499788b7a27e218a8d8a9381ac77150319b62a448cafb49237e719f50835903631fc7ff3370ca049306f1b890c45f1debb89c2bf3a0586d5c1ff0718aebbafc029640247bd71903790f22601f8270abd5d890e3f2855201b7420d90e5396016acbaa59a47fb7f1ec40f36cd399a34dfaffff3202c41c80f2b582ef37371fddf5c32ed22cfebff071e7800f2a20b2eb830b6c9f9e4ffe63aa78d61f2486b6fb735f963460b3bf35233e8a9a79e22705f2bfa7f1e840fb64d6ad5dfffd3a546d738f6f156a0134ea82f3f3b17fddf3ceefbeee2df6c6a346c33f67f3ebe6bd7f39017dd74d31f4cca54eafacc3457fcbf55f67ffcfdf8d6b7be157a7b7ba1d9343a3a4aa52f118865cbd52cffc7b2c578cdd0aa8440ac4f7ef21370e38d37c2473ef211b8ebaebbe373c7cbff7dfdec36b6fdb9cea7f129f6fff4633e9a49ffb7dbb8fa9b6de7e2fe9fb63e8dde6d39440b5eff17fe5ff87fe1ff73cbff0397f03e43b11dc056581647486be35aacac06eee36b1b9cebdd1ec76538aecf5982abebbc2b009a32f9f8f8c64b0ba8ae8095f53bebc5e5bce6b86906d9c8f1d26c282d58d83c1badbf8f9f8f57167e3e1b708d61af932d9779dc27a7ed7bbe006df7b7e574f52dfcbff07ffbfb6cf2ff2a65ef91d880de4bfc00dce01f1aa02c7bbe81c31fe287f830d91702e3e2247e905e53e00510f5c0001e89c04efa41bd1e28d133b633015e7a4ca9fb51f33a6e960e4d198d7159460689717b9417f43b1dc76e469bb884a2c12fd65bf4c28c3e08a408a6000e4ab05a01ecd9b3179e78e2573c03029028d9e92b0d2d0de0d6ca552be0f40d1ba0365125d0ca4474b36764642cbaf114c0d2c54be0c9279f8227373f05b52aca8cf3293924c82e6b2352628a785ecee8a3458138439402e9e03b4292f6ed3d00070f1d309b27004130791e3fb999ab5a56cccca6e6b174e912a8b4553271e0bea69cf44d8392e85b80e015a9ec0a4f49fd5df730c171220617616349d93d2a6d6d6087bbd8fd988f54a049cc7885842507110083e0ac726423a57289b25eb545f3426051a55c26f013f9575082a3fd03c4066d08012fcf6edf0577de79177ceeb6cfc3433ffd1981b310ccc770381c0e79634e26f651c421251039a509021072094c59a257dfd141f8c523bf827ffdd76fc3fdf73f40d9dbb0d3403406d97b343e8248b1a467efa25ed8bd7b2f3cbf7b37f10ba5caec452348ebaf4908901946fa2ac3b265cba8bf725b3129239b773da37f183f714ce41d9a8049c120a22019d3b221650b32c928a5b340c5f2eaf7d0dc3782fac8c16301401d784ac5262b332024190a41c7be183ce5d8c7e238cdf18b6334e832b3d1312c8d19f0582c9331e6a46b31471b26b32c62a88450c723bd0456762d807a1fe2f84f80b05a38692e93d66e0eefffcda6152b96c73234ba9e44f25d03daedccb6b3f1fa1f33d1e445986d280bcdd7ebff66d3b02ee79bf7f5ff962d5b200fc26c9a575ffd52fa3c5ffcdf27e77462775a1b535ecc7a9747dc19d399275bc9ff1bf19c29c287f90880cd6bff4ff37f9b67163ff4f9958b57237e1807f2a273cf3d77cefbbf4baee9f29d49da1dfd46a3df729a9abdff333ffce390c71f7f0cf2208c1318afe793ffbb64f7b533dbcee4febf6ad52a58be7c39e441bb76ed827dfbf6356c3793fe7ffdf5d75306f056260462dd76db6df0810fbc1faebbee3af8f77ffff7e85ee81ef2df63f17fbbbded7776df2cdf59ffc5fe0fdef15bc9ff8bfddf7f3ceddd1ec75c03975ef873233b72d14cedff85ff17fe5ff8bfff78abfa7fe01ad417207c0ab71dd53c66b7e573365f93a6baa866db2c0be652a43d17f31c1f6b3477d762a5e921eb5cd2f495e6582e595ce43332d68bcf48cd77972cbe76aee33e19d2e69075bd7dbc6c395d01c06e637f6fb4f1f1e734877739b3cf464d3ebe80eefa6e93ab7de1ff85ffcf56ff9f9898000479045cde8fdbe83ef46e3ccc2f95cb93ed80e5d13295342009cf71f614ee41602a7daea63397e0d8dc4640623ff4e33a113a7ed01feab2546a5c355ec92aab453c30130c831bf477dbfe10fc55e3925fba7dc819638c35e4ac59f899410e0464d0424843b7bc6408bec0928d98d54546a2777577c2191b4f5773166a4801f531c45abdb82d959c8b783cb5652b1c3870500355c2b8cc18a95fa7f9919ca42cfabafa8455b06af52aca1454c6525dd16b516f0f74f7f4d07c7ef1f89354424ee94e67230b5479365e665f68a0b2807ad91920859f63d89400029d9853d3d61b4d8733a7d1a85476905e9a219e561c1144836d55462c118d8365ff76ec782e16ce1719b247c0064482184028d2b12aef8837ef3adacba00abe99f110f5a85e819ea352a4503c682e52eb48e9498449a63619db872e012740674f020237a971185ca4843cedf4d3a0ada35df1068e112c8f02a6d4a8dca8027211075d0aae3a51251016be6386abc18141b29591d1511ab7bf7f30ba5178008e1eed831356ad84a1e8e63966c67aeeb9ddf08f5ffe2afcd33f7e0d9edfbd07c627f0af394b34e31a252d2aebb5904a7510c6769f1c0f75363aad1fc959f7d01e4b34ceb6a777c157bef2cf70df0f1f8c6ec61d54e722d51c3e7254cd3ffadf81fd07e0f0a143303234a2664dac8d3829626b23b014c68cf6485febd69da4e207283b0e79cd9d76a06546fb8cf482999976eedc49b297282ea8d88119eec86a0ddf562544954cf4927abdb5df10600d632cc532635f92909475a5b8584de28588b51683a28491458ae37790180a8d81b120b4f62873bfc696a1beb9c9fc380b5589b3fcf1fe9058b592df081eb11a8dfd0100265d8f71c6436a6364362432e6055a3e2a3b887a4270b0994531d20d66fb8bd7dc5cbe39bcff67b97e9b29e26c1fc5f57ffdbc706fc88bd6ac5993b9ed7cbcfe6f363dfbecb3f49ef7f5fff6eddb210fdab87123ac5ead32d0cc17fff7c9e9f201b36dda3e65f3b565c16bb63c1e2663f6ab81810160b95ac1ffb3f639deb469d35994d5a699fbbffdd93e66cb309d58e81acbde035cefe6f9b56b4f843c087d82cb5ccf65ff777d6e440ca06f26e13d17fcbd99d7fecffc703f4470471e74f9e5971358763ef9bf6ffc3cf7ff534e3925b72c505bb76e8583070fc6dff3f0ff534f3d157ef3377f136603f5f5f55116d777bdeb1678e73bdf099ff8c427ea4a6a4fd5fff97856dff1f1b3799b7656ecffadedff26cdc7fd3fab3df874cbdfcd356c85ebffc2ff0bff2ffcbf31cd46ff0f5c8b6f36749d4b9b80d9dfa73c9fb1a42d4456722db48b6f16c36d6408b663984ee17214d7c2a6cdc1e6e592df350fd7f8763fd7782e3e3e99ecf32e9e690e698f9166dc263f570073f16b14107cebef9aab4fffbe733e7ee6777bbe530942b61db9e633954dbbf07f77ffc2ff678fff23008b1f60978cf953092b21e2527a7894b35a99e3e171ec870fd06b5297f5e3927efa1cb68b1ff66b3e1173ba214af2f24bf361395526ac644c92cdb679a1b2a5844629449693cb0172692a0612c4d9b908e421340844c6a5bfe83ccaaa65e6bfb2a2525eba1f01574205dc8a4b0ec6bcf11d6ff662861c046328c1962ce9854d679d09edd4b80a7135323d1549998f14900718382335882dfa3a363a018f3cf2180c0d8ee91286783c841ae94668d594a2799615af48bd175c781ec97fee3967c1e1a34781b3640d0f0d476d2a70efbd3f8ee6c1808d5a0cf841f6782c2d191001828c0c3f5cd24e1dc381844ebac44c64bc86640232d4ebadb3f770a631506094128d8147b44ec8de048c0c8fc3ce9dbb62b6320c9df289e304c1224019ad4b55cb0250a9b4d3fc379c7a322c5ab450b5094a2c10d982faac013668136862683b5259352d7df4b98cbe20d5aca36f080d522021a18058b518de85e09cb2b22fad5fd66bb91cc06f5cf122d263cd00b781ce9a45b62a956e2b918d97233eec1fb88c1d1d9d118f1265b51a1f1ba3de04508a1e240d0c0c1350a944d98e8080476d1d1df0fcf3bbe1e31fffbf70d777bf07fbf6e34dc4c8f6a0a2e62169f5a854241552a4f5d531162d4db226d1d715b0a68ab61cbdaa1a7d87b6286555bf423874b01fbefeb5dbe1cbfff4cfb06bf701181e0b23f9ba60a25a8381a121e81b1882e7766136aa80ec1675a5e278a8e71e28500fad4d48c7f106f8c68da761a0025c185cd35269721638f427a1018e4481cae8353c3a0e7b76ef01555e516a9d2ba092e03c77ecce02e2cc772ab409ad672c3758d27e8325f794bf93594b9d198c4150d1416c1f670fb3e26a4d9743153ac6c621064b1a7239569964b68af7242d9434f8716c63e0aa093c9db4af4b23b3168f2b0c8016f261998c8c59bcc7c42510f19c2e391bef57fc198c8c8b7a7c52919471794404b461d6b6f9b8ffe74198d182cb7714d7ffe9d7ffcd24bb0456239a2fd7ff9c9da6d964cfc3473379fd8fd9169e786233e441688f08482c7effd7f76f3447176ffbbb2df3c2850ba1d984d9afb0a41240ebf87f5e84e5baf91ab299fb7f23fff7f132f9d96d5cdfed71edf1d3e26f5e84e54fe78bff4f55cf79c40ba666efffb60f1e387080c0a3cd26bc0eb9e28a2b26652b9cebfedf8afb3fc6ea57bef2959007e1beb97973724d9687ff235d7bedb57525cb5b9d3023fb238f3c027ff5577f05575e7925dc7aebadf0939ffc84ae71a7e2ffbe986eae832fe6f86ccdb567b8c67651e1fffe6333e5ff66dbf9b8ffdb7d7c766aea3b6dfdd2d680f99afb7e9efb7fe1fff5fc0aff2ffc7f36f87fe05b5cd76797413612d225f47428cd797d0ee9322297e3f8e669f3b1c76e34df467af2199e8f87ebb8cd37eb1af80252dad82ee34ae3e9932d2d18f864b0fbf83695a93857161db828cbda67e161f373054d3b8864b14b3e97653e85ff17fecfc71a8dddcafe8fe01cceba430fe6f5c375bc2951d319a1748738d315bd330ffd19891f9a57cdb4d0b6ccfa7b584b4aa949e6afc766c0133e8cc71257f8994aa4e9fe0caac23e664943733c33f316e9d72cada8cfd383fe008fa96c3082e5d0e70403c48488c1580cf6a1b9237002200624f04c55d69de81b67bc910a9cb072e50a58bbf604356397b9714939d7c988ddf0c830fcfce78fc525b612bb517a41204b182ac0091e6fafb4c1ea552b095c333e360e8bf04179d4146fb4f4f4f45039c3addb9ed5234af7b869446382f1523218696ff47a2456226cdf088d0b2ece56144b94d8b082f188d816fafafa3127966a1b382e9c75fbe3476a3d9150c70acc22e0e493d6c2faf52793bcaa1ca454ed24fb9b914d2cfea0c03cb1c86ca83a5b58a22291ac8830c170c9ba2bdb16b068f16238f7052f209efad14f3c9ce40c533ca000ca848540abf1f1719888f4393a36428d4746460954c17d4646c728e3d268f43e417fb11cc0ee3dfba39bd6c3f0779ffd22fcf2895f516632051c0a755949350995314ec1b282480fc90bf14b327e09fa0ea0805b825ec892cac9c52f952d6aa07f107efac0cfe03bff76271cd87f10068646611c41a4d13fb4895f3db9450198084128d5b2d5ededaceb80865ab57225f42e5ac21af3aebeca5ea5638ece96857c0f1e3a4c0035a1016b019bbf5e1f15b3d49814db54edcab8c45ea04bf8297dab05e6cc4f01974fd47ccc35e4b8021cd3745c0bc0b03399c475c54f4c02d106464ce4d81ec7151df3e252ae46295621445dbc94500f8e32f79f5856eb15ef2da2fe8601ef03ae1f644c52f72b19fd80c692b1be5c3457f7ff2cf2cf042d8ee24e676767a6b6f3f1fa3f0fc272aa4cc5f57fb6e3cda0bcaffff141f3c18307a0d984206eccf681656ee69bffe7f1fb1faf8f9b4d685b870e1d6a79ff6f0621c0d38cc1cdd8ff6df2f5b17fffdb32dabc5df264b90672519ed729f67ad83497fcbf51bf46fb7fded48cfd9fdf3133651e19b030231b665d2afc7f6a3453fbff4b5ffad2ba18d14cc22c588d68a6fdff9c73ce85f3ce3b0f662bdd7efbed70cb2db7c08d37de081ffef087e1a1871e72022be51cfdfdef93a1f0fff977fddfa85fa3fd3fcd4ee51cf9fd5ff87fe1ff2ebe85ffcf0eff0f6c217c06ec9b8cebb38bcc89fbdaa60524d7645d32d9639a6373db468ee9e3e90a32f658697d5d4eef6b7f2c94e6543c7f5bd78d6468d4cea55757d0b079a4e9346d73f1f176f1f79db779a6d9986b7d7db69ce69caea0efb339d786e1d2b37d3eed9839aeeb7be1ff85ffbba8d5fdbf565325b31018a34a9fa92c2a208dcc2a3a7b0a02a36aba14564997b3ab12802bac97454a279487b24b19e50c43237311b60f1321133ec6da53562e7d5c80a51fa38f393f69b6b75ed456308868f23ad1316e6364c7420a0d20963dbeea17aa3283982947d40854b0e1d40dfa21540d0223715416c219ecdf7710b66ddb1eaf81ab95526f48d98a162fed85a5cb96c2f2654b60efbe7d0a6c17c985379af0b573c7ce88e73e10d3052a49d080af58ed5a9710eb54651f62309488f55d27b31060ac963e2c639e266884f42dd45fc099203e8726eaf91d3359315b97235c73c26a78e10b5f181d8b744b19b2d88ef07f41d215fb845a0f04d0d2e52b8501b87118057715a09a63e63365966c670aacb46edd3a3873d39940999df4795c6bba580dcac4848e11b80554f22ccdb824945f22808ab318542a1502670d0f0d917c6323a3d1b1726447fb6141770f7cf5abdf80871f7e340614293f10892d681d5129467b4ea6af2a45e9b922582da8d361d244f921da707fff00dc7fff8fe1de7bef231be8eaeaa1b289983d0ccb732a750a0594a34f02b88c1f66a6a3d2a042952ec5cc708b972c824c24ad781cc9bbed9967231d8dc473e725e672917ac2c67a2a9022ada74c004eb1ca8cf9c7e348054aa2f27eba74203b99e4586bc4bd38460a037065ec03d2e06b0c02a6239bf19ccb33862618571a2516f51cb90d835dc198893464e617c757fbda89cb30824b4eb65b2dab34654599221bae4ef8e3421acd95fdbf99b464c91258b8b077d2f1e2fa3f3fc2526f08a2f5c99276ad8e345faffff3a0665eff63f9963c1e36638695d34e3b6d5efa7fb37fffe3fafefad7bf86661366f2a8997f7863d07cf37f8cbf083a446af5dfff69fc7de76d9e697b8cbdbe8dec7da668ddba93e785fffbceb98ea5f56b06d9f69b87ff3ff6d8e390079d7cf2c9941572bef93fbfb7dafedfd5d54520f13c280d80d50cff473a21ba8f8559a4f0fee46c25cc66f7f4d34fc36db7dd06af7ffdebe1a69bde0e77dc71073cf5d45399fafbfc90cf9936d4685fb77914fbfffcfcfd3f1bf67fd798e6d8dcb6919e7d3c67cbf57fe1ff85fffb8e99e3babe17fedf5cff0fd294ed1a1c1c03fafaf8cea705882cbc5cfdec85b50d3b6d717d4ab5c90c5e530d9ab62cbef11a2d6edab14663dbfda612345d9425689aefbecde758f5e992c50e3e8d9c34eb7876dfb4b5e436f66773be53912b8b8fd93af6e9c9f7dd374ee1ff85ff6791212fff07a9c0578c080acc0b8aa493c9408d1b2ae01697c6e263928f59e331d57426ab4097c302d38fa40223047acedc93e7cf00150688e1c37e066599995cf8bcb0f5a5df43a34c17cb5d97498b65423e0167a1d159784003d2501e94cd9211fb6246227c110046834090cd19679e061ded93d37c3b0150f62129e0e9addb086042eb146089c350f7d69999487f35024060d939fc2bcb13569f00d55a15162d5a44603b04d554daca50ae94e1d9ed3b6164788478b96490c6bb6d66385713404773d640976402a2ae6c1b67a7119cd947ea17ad9b48c6480c4f1d131ab412e86321685b63bb71c446389e540f8a1171a6b230ba71d7092fbbfa4ae8e9e98efc28b26d2c2da93086ca26689e21ad8b9a9206378afa79d2ff1dbe6c4420fa2605670d5232e13895b6007ee3375e0c27ae3d0174f53ea38fea8792a0ceb03c1bf96ee43715fd179f92807b252897ca04a84430d3c8e8280c0d0d6bff923034324c40a3aece2ed8bc790bdc7dd73d116b2c855726e424da6020ca7a2c04ed90d50016c8ac812e130a6ca3a1b28440bf208c4bc9419c890d4138c847bd0746f625cacc5793f0e0033f85cd4f3e4540ab52a5037efdf456e83bdaa7cd4d6a105462c58a077972bc979c75f659d0d6de018d2834141beabe38eeaf363f05a391be306a91e421025245e23312eac0a2e82794a10e54694195324cc6e556555c82d856e2b88231d5cafc1718714a320fa14a96a2afd77449c09211c3983967edabe9181a6b49268030c96d79df8dfd3889d734a6113743636f30ad5033af7b99e5664ddb475ba869f9e2fd8ac784643cc97a353f1b03cea7fd7f2af33a9ee42ad769d27cbffecf83962f5f915a4ea4b8fe6f3ef96289ebfc4c5dffefddbb17f2a0b3ce3a0bd6af5f1f7f2f7eff436a1fdf79df9e83847bf873cf3d071ffef0ffc80564974679f97f5ed4684fcccbff7dbcedf3f61e90750e69e31947200f3ae18413bce7e682ff67e1e5ea371d1b3a5e9465dc99defff7eedd037910fed19649f3c7ff5b77ffbff0c20b210fc2b27935cf1f1336c3fff9fb25975c022f7ff9cb61aed0030ffc04def7bef7c1dbdffe76b8f9e69b0998f5e4934f46f7b586bcfa366936fdfecf4a85ffbb659ccffb7ff1fb3f7d6cbb5fe1ff85ff17fe9fafff97b3323227d048009701f067533159794d5746577f172f9f41b802973d07f3dc54e6901638ec7e59644e1b8b8ffbf8bbbefbf4d748063328f9e6ec93db379e6f3dd3f49366036932f9c677b5b575e5e269cb603a7b9adc3e1d363a36555f2afcbff07fd7f7d9e6ffa6dcfc403f30f83247026945c710b81170693e50a9fdb12ffea0258083d4a5c7106065000042736cfd701dcf210fb32d1f97e0bf38108c7089a8148d2fb8ad6a449f39130bcd4783bd707e3198401f8bc793ee0b04a1dbc56383015ed0fa115a675cc6502898913aab3325757576c0fa53d793be44c03ad7d310a00032e6c0f158356a87e33db1f929e83b3a80b38e8f215ca11454d49cf11896e5932a03cfc29e6e1aafabab030e1d3a00131313911c9db0a0ab0b26c627e0de7bef072ca3570d6b3154a78ea402c0b8cd4fcf0ff41c686cc91f8c73bc969cf149e876f1121a76397914d6b5d4b68240a150567536b2200674851ad492f803b8e56e74bde8ea13a3a56a71132165bc76e75ff002b8f2cacbe1dbdffe6e741c65403b52a507a5807865a53616d445c047d9b668b292003aaa71688893e89ae6acf5c87be29a1356c1ab5ffd0a68efec82a454233e081204c451ed00aab246c03b02f144ff0d8c0c427bf4a01efdb91ad906be7774b4533683654b9752d6343c8625fe284356e4f68383c3f0d9cf7e1e0e1fee8b442a33a44ecb1ac6c020b51e5a745031a15c2e41f7822e1aa35456361c86552a7b3836360163a35c3a27d460299d292fb6c3302e4988c0c603070ec10fbe771f5c75d515d0ded109fffaad7fa532890a4024a15a536bc4e0267ca96c7faaecdfa245bd70d98b2f8332022a93fc7b89a9c43150268e1e2f24c0f85815b6fc7a2bd4aaa1ca32a6753f2996f37a699ea1725e2a3f28438e39ca6e8cc18d98a0c1adbce691bcb1381cd7c2a42c29c1cc4aca0142ee67d81a69385400b5380e3b625c1ccb0d5be3f3f13c4c7d4102842d89040ccbfc4a3a9362c880575e6d9994928d63a8de1304977b642fc0f9f0787afc2af2e3fdcb97392be518eb6d36efff79d1d2284ef4f62ef45e7f20f9e6331faefff320ccbe3215bb48bb46b4dbb8fa31cd86ebffbcc8f7dbce3c3fd3d7ffcf3db70bf220cc7e6592ed73c3c3c3d1f5c461fabc7bf71e7dcd6bdba1795d17ef00c6f77873d67dd4e7a45d621381bea6e5bef5ba13068ffa63f6bb7b1f80b84d9a6c22be3e357f0bd8fe60aea71a13b32bf16f2e6c8f20f92d5bb6c0b66d5ba3ebcf6fd3e73cc9e7e379f87f1e84b117d768b6fcfef7c9e1e3e99329fbfe9fcff5ca82050b261d6bc6fe9f45e7e638763ffe9c75fdd264ce326e33a915eeff6ddfbe1df2203bdbd2fcf1ffd6bdfe3ffffcf3a3df32bd9429b49984e3eddebd1b4e3ae9a4f85833fd9fdbe1dcffe88ffe2b65d07ce28927602e10decb421fc7d777bef31d3a76e9a597c179e79d0b575f7d356cda7496f7f72bd26cf9fd5ff8ffb1fbbf8fe6fafe9f6623761b573f734cfb789ed7ff59644e1b8b8ffbf8bbbe17fe5f7fccd5b6f07fff38763ffe5cf87ffd98532e16ed13c037497b32bec0e36a9fd589ecf353ed9375911a39a28f7cba6ab490599ccf3ed72818a5f1b7d7c6e467ea69ba4136ab4caef570cd2f8b5e1b3947d6f1d3644ed387dd2f6dfdecf3ae71b3aca34fa6a9fa948b0aff2ffcbf55fd1fc10835a3a41e3d20d7d949f8e13e3ee4a65254c80f92b2690c90a1b90bf520dfe4cf59590495bf4b4050d822e0f1a47ee88fd97690bf7ef01e982029435e9009700aac79c599ad6c3f911e14b610c963153d37d0bc63ddc5602e635c915cf8c6bae6a1783c91ac1766a33ae9c413097cc58979ea24b4dd9afaa9b920c0637078109edcfc6b9818c76c3558524e12c801a1530c72107a2e78aebdbd0dce3ef72c40d010de84c0cc402b57ae04a9813d08acd9fdfc6e5a05a9659193c4a1dc48b8327aba492bd3af6d3d9b142a7180b25e09771bf0d9be9431b00de565fb40c05967477b8cb598045e4b21d1e00180747262509430bfe9f94b686b2bc13b6fbe09366ffe153cf3ec4e05269446d62c6a47b9d3b4aca1fe164f53d94bec15891c02d8c645f2e0306a8380245cf7dede1e78ddb5af8233a31b4908de0a28e39a924b4ac166aa4191890eb16d6767070180302b1a1e1b1d198d4b140e0e0e46b6360ea3a363d0d9d54525ad068746e05bfff21dd8f2d416850f633f81523c0f0200ea79a18c389d258b17c14927af8595ab56d2e78e8ece68ec4ee8e95900dddd3d50ae5460687080ca6be25f28fefad75b61647894e64c7626158053f96e120370a4a7b73e03dfbdf36e58bc7809ecddbb5f679c63eb0d413b2180010e441eb802679fbd09ced8783a4c76c6fa6557a745ec204203dc86227d3cfef82f81cb06a21e101ca80064a5a49f560ee987c04701390596cb1331c051d6bb8154a22bc091ca161597aad43192639408ea4b1ad6c55f03c0146a39541958e5bf2561daa8510e130f7126071daf6340941ea3d256212027672773edf51c9740bf33f08a65e2182ef4be101871379e1f58cb22129d629f12cbc4c7c2f9b9ffe74969d783c5f57f73c92c41585cffb7ce7ae57dfd8f0ff9f2a04d9b36d57d47b0153e107bf8e187e1d1477f0e8f3cf2b33823015e8b14e42704f8983e55ad56e9fa1e4b4767c9ea3093e4da475bc1fff3a0d9f4fbdfa557e76fbb149acefedf2c426001964ab6a999fbbf4b3f69fb6733efffe545ae78619f679aa9fdffd9679f8566d329a79c026bd7aead3b361ffdbfd5aefff11e1d964b6e36000bf7ef1d3b76c289d1fd41a666fabf39c68927ae85f7bce73df0477ff4474dd743b3e8c1071fa0d757bffa555af3abae7a095c76d9a5541214f709ce6099e66baeef79fffe2ffcdf3f6e9675f4c934559f72512befffae31b2f44993df3ce65a235f7bdff999daff5dfed1a85fe1ff9079fc34990bff2ffc3f8dd2ecb46c1f747db78fa5199bdddec5378dbf4f118d0ccfb5e8e6e7accedec899ec7669d42828d8e7a6b29036cfb4be8d8282abbdcb41d282b18f7c6b9d457edf795f70b1e5f4f16d44697a493bee73fc2c32a4cdcfe69be63f761f5fb068742150f87fe1ff3e3e2de9ffa0405426a884c04f42c4a0073aae4e120804810374dcd03fb5d2e5008548324d513b1e0b4103946d8604a33e25e3817ec099ad4c7de971e331643df82a8604f1dc747940b33c9e9a2767818198bf304a45706615c12515592f5c4e91c6630696be0d7ef83964704432142c5db61456af5a194f4fd4a5d4b119621b0d88c3f26a2501870f1d8127366f8efbd0ffb13c9b28450f6138cb9124be5806ad5229c19a1356533939844774777753b623ec88e0ac23d143b091e1219dbd484c9a571003ce142047d985d0197b941ed47ad529d9ca23a4b2f8988a5019a1b25fdc4bc2f024e52a31b3525bb9045dd17c48c75a7c219b712359cd1f049b031913fd7bd18b2f8537bee90df0894ffcdfe861584820a93a03c0de819237c01275b20e820500d2b10ac00bad5a69b357209580d6fbfcf3cf81eb7eef3ae8c66c67b446aa34253e8cab54020594125c76b346c7318342b53aa14af74536db167d1f8d6e02226f2ca9f8dcaedd94292d2470d6048c4f0cc0f8d8286c7df63978e08107e998d459d85ce09818d0197d3e63e3a970e1c517c0f265cbc8eedaa207886bd79c00a7ae5f07aba39bd0dddd5dd016d92a820c87237b4410d677bffbeff0f5affd331c3c7804223123bf0ae2d2760ac717f1278452189dafc277be73b79a534d2a0096b655510b62ddc680255d1ab4d2568237bff90dd04ef30c75b9c22999021c3a7418766c7f2e62ab33d251860b5d661101ab22d059abf48f72217506b3909362699d29315568d2e559393396c01859a2b566be3529eb3248d542dbeb14af408358f1a1b68cd18af5d9ac26ed317a0dcd12b314f785327a8e6dd80bc157bcdea8801ad4efc9b82698758fda60d62e29ebf648064f31782cb0ec08009cd742713c562dc8e68101c4f4eebe763379cce5fdbf99d4d3d343a0803459e6f3f57f1e849906998aeb7f98b24dcd0499b12fafebff5dbb9e833c881f2ce2b5c73df7dc035ffad297a2eb8807e8c163417387cceb8ab436cdf6ff66125ef3d8609f96fefdef898dd3d169b6fd3f9fb5e23f286bf6fe3f9beeff359b5cd7ca7c7ca6f67fb3df8103077201adae5ab53afa6d5e496d3377fd7ff2719f3f6591216d7e53bdfedfb061030172b0946f3309af83f6ecd95d274f1efecff68599a1def6b6b7c1a73ef5290278cf55428019beb66edd1a5d937e11ce3bef3c0261fdfeefbf9580688d7ccb265e23d771f3fc54f601fb987daef0ffe3e7fff37dff9f0fbfff6d9e697d0bff9f1a15fe5ff87fb3fcbf6c377609e01acc35799743b88eb926e89b8c4b0e577068c4dfc7d7a7fcac9466bc8d8cc63c9665415de39a8be933361fef34dda6adbd4f66b3af6cb021f80c7dba7a68d4b7915dfb1cc4ec6f1f9b0a655903269f0f35b2ab46c1232d8816fe5ff8ff6cf67f7ce7cc2380dff161ba2e5b15b78324eb0a6548d1e7ea4a5fa9c6c443cafa4c26f45d8332a4d11fc709857aa05e977945979f8a33a2240a88c15681211f8133f0461af395d2618f52415c1800c480045e2f4bc7a40b3eafe76eeac352663c26b7a13ebafc20828356ae5c050b7aba75539d51cbb75c12e273c832881ea83ebf7b0fecdbbb1f38db0e8d456013a9b2edc4d9b91054a31e022c5fb624bac1588dc66aa77e08ae9998a84247f4c0fcf11d4fc1f0c89802829150ce29b17a14d84826e7ec632caca8835118a734facd77de1a1dc0c80085e37109339c4f5b7b1b2c5bbe4ceb02ea407a3347894c8a8c458aceb577b4c3effcceebe091471e85fb7ef0208c8c562d509888e72375291ad631e835f001d3026e4766c5009e00569fb00cde76d38d70c61967e8070caaedc444a8fd5900970544ddd4c21a3d9c1f9f18874ab9423a1d1a1e86ae8ece18dc8325fcdaa39bc3834343303ea6b2511c3c7498328e3dfef893d143dce7635980416884aa93757aa954ca70e6a6d3e1a557ff268134ca6505c0c1ac5bebd69d04a79d762a2c5dd6ab7d548dddbba81356ac5c06a79f7e2a952dfac2e7bf0c8ffde2c9e85c247f0d813f9ce928d4aba10a391ed87f9880596abe869dd2ff8406568a780df0edd4f5ebe1c5575cae75a6805a622a0f87a2b17efdf436181e1a896c52906da26c54668f01762ab01956a2e342a8e3820862078bdb70cc04f633b66be58c716cd23189e36a60c47302d002c400dac0c864a5de34988dc1b0e6b4b4ce947126b13d8ef3464956f6889ace3ac8402a60bd1bf15018f2705c16bcdfd49292530c942b6b7d0a3d669d8c32d10582dccc2c8c8d56702eefff7910665a62005671fd3ff95c2b5071fd5f3f6e9e94e7f5ffae5dcd2f41b87cf90aca2ab16ddb36f8c4273e01f7dd77df9ccdac309f69f1e2c5ce2c433ecacbff679af0fa0fcbddcd96dfff8dfa36baafe5daef8ee7fe7f3c298ffddf778d627e76d9beeb984bc646eb66cbe8a2bcd626affd9ffb3dfef8e390077550f6eea04e26a4c2ffd3e56c74fc58affff1b7cc54f6b0e3490c46cfd3ffb94d5b5b1bbceb5def829d3b77c29d77de49d935e73aa1fe1f7ae8217afdf33f7f8b8058d75e7b6df47e3eac58b18274c2d4aabfff0bff6fbddfff3e195d72363ad62cff4f5b2b5ba759f8a7f1717df7ad93ebd8b1ecff260ffeec6aebe3e1e397d6b7f0ffa951e1ff85ffbb8e057cb09113a4093555c737db725fdfbbbd48a61c2e4737cfdb9fcd5723e598f2f964b1659ace02fa64777d378f9be3bb3e4f95a76f9c2cfdd274ea6bef731297b3b9d6ce7420d71c1b39a93d96d9d7f5d9eeeb9b5716c737df5dc75c6db2f4f76d68c2b1a9daf32afcbff07fd73859fab582ff63e6177e708e1cf861367f37c7a18c2ad10d662e6b05f639a11edcd7743629eccb0feef93ce943eacc27fa26143fc0073d7ead5aa5be581e4be871e27980ca9a4587419756c3572417defce6cc5ddc964102f15cf0c5b22a8525a02e0d480819cca5cb7e51f9af50f5e50c59d6c2286042ad56978d8b2aa04995c16ac38653a31b3c6d041951199bc0c8a014eaa274a02516040aa1b99623fd546bb079f3af092c136725d3e00c942b207e785ccb2d4258be6239ac5ebd02aae32350ad8ed3f123478e120f8435ec3f7088b22009c163d64d270132183e2cadd25ea0f589c743630d6d22c011899c94c44b232abd863ad4b209d287e2848033bc518637459408a6af019dc77624aa68ec572497f9f24b05548a31609f16c09029f5aac2a91bd6c1fbdefb6e38e7dc33a276680b352513940978861c307b91526340eb80ab1e227048cf17a132aa721ffbadb23dec826013cad414fdb7a0bb026f7ffbefc34b5f7635b44537744ba8b31a19158157ca65a56b2143b2670444a10c0ae828283311365ed0d505d5e83c6692c22c54386e476707f918668a1a1b1f8f1ea0f642dfc0083cf4d39fc1c8f088b25fa13c2b884b4b727c417f1e87d34e5b0f575ffd92e801ec52e8ea427e28170295243cfbccd3b0f989c761df9e7d944d2ba051b1c46908ed158095ab96c19bde743d7ce84fff184e5eb70610cca4b22c0965727a35623b45578b4c3fac2a801ff2c26c5f04ba921cdb125d9622dd5c79d5e5b074e9523a46e7a4b21db5ca6a51a5b61f924f18f038a1c0a18ffde209181fab463ad0f114ada01aaacea2448037e5d77a9f213fa9e998ab327181d6a3d099d4f89fd063876195f8a8cfa18a9d1a9cc4ce24b4f19b190bf1ff54b653fb25c136c5e4eb3bb394217dd73eadb267a96c821cd3e26c8068c3fa2fc725674f344ad8821e0bff7a36d0719ecbcc9a7b2ec70fa17907c60bfbd66531d47b497c9d2092f28be61e5576c5675eb679b0ff379b1afd1630dbf8682e5fffe745c5f57fe376cd249f4d36ebfa7ffffefd5456b8d9b422ba0ec5875abffffbbf0f77dc714701be9aa384999fcae54a4bfb7fb3c874cd56fffdef92c73d27778c37fbba3edb7df3a666efff8dd6dfe49fc6c71cd3eceb7b9feafe9f273573ff37e34f5ed97d4e3ffd74ba8751f87ffdb8ad70fdbf6edd3a683621f807afcf4cb99aedffb65e1044fca77ffaa793ca47cf073a70603fdc7df75d70cb2def829b6eba09defbdef7c1b66dcf64ee9fe60b76bbc2ff5bcbff6d99f83cc0dcdeffd3d6d2b6219f4cb67c3e596c994cdb9c0eb9ecd8d7ce1cdff579aa3c7de364e957f87ffdb885fffbf998630214fe1f704793816b0269c6ea1bd435091f5ffbdd245b09b6e2ed2064f376f5f305265b073e43b1e7ef7a37298d8feb7c1af98cced4535a10621e6680b3c99e679a41fa02a1d9c627836f4c9fb3d941264ba0b5bffbe6668f63cbe05b3bd3a97df66bcbedd29dfd6ef7b5fbbb026b56fbf6cdabf0ffc2ff4d9efcb9f5fd5f3fb837da9774c614a1db9534e08a1fe623408a4b60c5fe05aac4088da3fbf27905600a55993f9964c40294412659ae18c0055a360584502015a1dbc60031e4af796ac8039de77972f1319e1303ae645d3c30326c99366e000db844165728137a7e25fda0df942bee87f08aa0a4ffca51405ba5021b376ea012800458608c462019ca6d480bb1f60873120a181b97b0f9c92d303282f5d824c4801728291586353d2f95090b592e5dba04caa580b20f74b67740757c8cc057cb972fc3d9440fc2066162621cc29a24908e9a4bacfa682d959e4b0816895330e9ac3ef4d52c5b290954824032390948a2fd40287d862976cd447c841e0b20ceba86e0150435f5f62e8413d7ae8d815912ec8b502553160ab5d4612cbd4b1eb4eb1a18682b3d08da7089def1635b05e0452fba183efe898fc215575c021d9d1a00430a2b930a093086739142cf0b683df023db714036a6a044ec8354764feb6ff59aa5f09ef7bc0b6e7ed77f829eeeeec89ec2a80f0298702d23df84aaca2aa4813ba323c3303136ae458ffc17c157d197f1e87d6868888078384bfc7ee0d021b293d1a83d96aec38c69f87ee8f011caf8540bd1284b0478226d9131875a1d21d974efa245f0aa575f034b972f275cce04f10fa05c69a36c51fd43e3f0f433cfc10fee7b307a38fb0bd8bbef30d986902abb5729d27747bb80ab5f7a057ce4637f0ea79d7152640f6391aaa5bd30a44f4c8e4520369a5f0204a4230262b0137a4610f9c4ca154be075af7b0d54dadb080657a28c5ac844db290018c9b2227ea1b6c32a2e0ec8e8d5d7df0f8ffff2090574c2e5ad6159c6aa8e071833cb1a44a940632a2608ca8aa6e297d48044a918082d2ff995b60bf4ab9202dd118049c7cc9a8e7bd8240629714c052e81a8b25c853a4b160446e63cde2338961afcca3af6f33ec099b5885764530ca2c507aeb1b3c904982a8488c156420714cad405108360f9c14710f188b3229ad770320159c5102c8337f3e78c6de6be1af373f9f11cdeffb35cffcc04b9ae2f8bebff7cd682091f9e94742966d7fa30cdf7ebff66539ed7ff03030393ca723783b09ccfc73ef631d8be7d3b143477094b37a9df168a5ad1ff9b452ccaecf8fd9fcedbb7afb9e2fd54f7ff3ca8d9fbbf8baf397edadadbd7752ebe69be9375ffcf7b5df2daff312b63b309afcd30bb4ec95192dda4f9e0ffad78fd7fdd75d7411e84d949f13e8b2d5733fcdfeecf9f714fffdce73e372f41584c9b376f863beeb81daebcf237e0b5afbd163ef2918fd03104cdb97cc8e723c5fe3f3f7fffcf86fddfeeef5b575b06f3bbcb1eecf3769f2c7348e3e33a9f463ebf31f564ebdcc5c3b42b9b0aff2ffcbff0ff99f7ffc0b530ae7717f904355f2e815c6d4da1ec3669ce6392afbf2bd8353234bbaf6df83ebdd873f1e9c265b42e3dd9fd7d329b01c0fc9ce604ae76b68c8dd6ce269fa199f39032db66e39b834f26d71ad9639bed4c5df86c9ccfdb6d5deb223c1b992fb0fadad8f33275e1e2ef0ac4368fb4c0ef93b3f0ffc2ff678bfff3c370e618704611dd161fda5475462a7cd5f403ef49f3d0fa631015e99365d1408078f4e83c82b5388b4acc42b7471efc0091b3be707fe2a9c102815146901ecc43a2c79a7ee88f6301f74599796e527123f934f086336791fc1a6c86c76b3a231676a9f15cb40cb1ed680082e2a700334c9d9d9db07ad56a60055056189071d9373a366949d578840b89c67fe699ed2a0b17817994f8c20117c226a254868da76f208095ca28a3800c2847756222ba79300a838303947d48b08e2c5ea26e4db4cd7133e3bb94a1d11014a8c4f8c7eb27800132fefd62f2e82649023961569e0da79d0aed1d1d3a6f509348a49fa21279f839b2dbb3ce3a03fee7fffceff09ad75c034b97f5801468e70ab448801f7e07c2f35006a5407fae8582404eaa04a856b8c06c4612dadacb70c6991be0fdef7b0ffcfedbde02dddddda47fccb046b64dad03956989ec32a4b56ed7250f6a947d29d0253e55f62ab4a9fe8141e8eb1fa09b4a98096b22ba11d8d656a12c5908242c95dbe0273f7e10c646477486b97abd7329483cd7d1de012f7ef1a5b078e91205d80904ad19f998c4d2841558be6c192c5ab498beef7a7e0f3cf6f82f61e7cedd305145a05b4903ea1478ef8a2b2e83df7fcb9b23db6d8bce711c12ea051ca684cace868027a181072ae7930202451f2be57224f704e90a6f809fb6f13495204d039e10f416d03ba80c6ca03d42b2fd4a0de4c206013cb7eb79d8b6f5590d8e52003405e6e4fd4a676b32be938e4a0670934293d20b03135546a724bed0047589c1d804912f97e8d3a02cec4dc0591d83152052f96948603c2da306d3ead0a2d74fc4d9d96a469c96fa3b7089410da4b56d1f8f98c0a7b834ada000a1632268b982c476a401d48204e05b3782b19771cc170046dc34aeb7f4775af779b6fffbae35679ad2c69defd7ff79119651c1385b5cff8b5cd7c14533e5ff362f3b86eddebd3b2e73d34ceaefefcf25f35641cd25fc430f2edfd42afe9fdf9e084ddfff6d5e597fffdbefa67cbe35606aa463d75e9dd3924ca266ecff3eddb8ae554c5dfbf4ed6acbdf5d6db2eeffad40cddaffcdefb827369bf00f57962c593ae9f87cf17fd7bab8e6e3f2a7349e2e9f3275e1e2eff2fff5ebd7435e9497fffbfae30b41581ffff8c7e1252f7909cc777af4d147e0d39ffe34dc7cf3cdf09ef7bc07bef6b5afc3be7dfbe2f3f61e6152b1ff17cfff7cf299f2b49affdbe3fb78dac77dfbb64f2f6976efe263cb657f77d9994b6696c7feecd38339075b1f85ffc3a4f376dbc2ff0bff3f1efe1fb8047129dfa54c9fe25c41c6760a9b978ba74b0e7bd17d81254da93699c6682bcb2563231d986dec77d702b9fab90cc71c336d4e69012acda91a05029fb36771045f303775dec8d17ceb61b6f319bb2b38991b90cdc73e6fbe5cf6eee3e3d2a779dc94d3a51b7b3cc8a00f9f8e444aa073cdbbf0ffc2ff7d32b59aff639b1267a8d26d383b15f1c7fefc005e24d9b20840653f8cd7b2f043719009a089f92af047a8810cf57205969e686ce6ab06502000986c17a0c7c5369c8d0a412c81313ec80468852fceec85a8256c21ea74acf54619b6a41abb14c4bc645d43fdce32423d2d5abc184e3c792d2851acfe3e22592521b3101cb375eb33c918dcdb61e60afc508333ced840c01a6c5b2a9708a032363e06eded6d94896874744403ba78ae067b5a073d52a8002aca162825977a81cac4c359be20066285752f2e3309a032f4305867aa14e87c50e5e8c1f245175d0001d666648165436dce38b1bde0878e8e76b8f8928be0fff96f1f8477bce36db076ed72a8b4a18e6adacec80a29295120c2f8452aa78c5a04cd8a337b21aea667e10278c5352f810f7de88fe186b7bc1996ad58416b44adc83c03e289599ab017659d127ab45081b1108484c708580598fa5e01addadada083855ae94e91cda0502b182a04ce0ab43870ec18feeff318c8f4dd419ae88e72e416ad8ccca952be09c73cea2b299ca36949fe1bae158eb4e3e112e8ed6ef9cb3cf8205dddd7443fae0c1c3f08bc737c3aee776c3c4788dec4cc538093d3d5df0e61bde00679fb38978091d14f42e00b6b7c9ba4fc9390212453aeae959002f7bf94b617174139c32bbb1bf5a0180742f3902a9f552b14b35defaebadb07fff01485c464c726a15fb74acd760443b66c6fd79248a7ba22eae9118bcefe918260cdba7331a140a665c9432064d71fc34c7a4b641e28f82c733005760be9b3c782c8eed569c8d8fe93985e6b5823e0662f28f4eb3546d5ce65026202e9e5bbcf7699910d8c6f1ddb3fdcf8bfd3f4f2aaeff1bdf7c6906b9ecb5b8fe07eff166525ed7ffc3c3c350504133451b366c88b362b6aaff37835c31b799fbff547fff9bb2d8329afd6cbebe7ddb3eefdbfff3a03cf67fd7b58a8f9fa9f7bafb206266efffc99cae555c3e30d3fbbf39d73cf644fc2399050bbae6bdffdbe76c3eb6fddac74d395dba71c59b2cfedfd5d505bdbdbdd06cc24ce46606ac66fabfd9cf35ceb9e79e0b1ffce007e775262c939e7df659f8ce77be03ef7ffffbe0c61b6f84bbeeba3bba3fbb7592bd16fb7ff1fb7f36ecff66bf467334c95c57db7e1ae9d7a503b34d33f67f1f0f972c2e7bb4f916fe5ff87fe1ffcdf1ffb2cdd0a634c5bb94922694abadcb317c6d5de45a64db687d0af5198e2b20f071df9c7ded7cc75c8e65b7753980abaf4f872e27b7f59366e4f6f86981c2ece79b9b8b5f23d95d9b469aacae006bf3f5c9e4e36d9ff3d999ebb8cf315db29bed5df34ea346b664b79b8a2ddb6d0aff2ffc3f6d8c5cfcdfe88b40896af4601d6fa4d379e42d445c5a4fea36c4d778700f465b13ac25f4c3f46a4d014f28c394062060a9ab50f7611e783e10932f3ce2f90af5f0de2c89682889401642cf291e5767d26299639d832ab347196c784ed2cc44238c31459dae381797d07272d63009f5a003eeb070610fac58b64cc92110885155e5c802d07a512a9cb4ee525006a203078ec0debdfb803a805e13a13a99f3d16a809eee1e181e1983654b17473775aad0d656a516e363e3344e99b3e7203807d9c5501f5e4f300021ea50a0f00d1af0a0327605bc4ea42b05f4e00c34ac2fca0ec4b0a3c661a99e249b87d0e5ee00baa39b97e79c7336ad9bd0b227d2a69380992382a7092c7127f4124938fdb4f5f087efbe19369e7e0afce0de1fc23df7dc0f870f1c267054a8334299583a29a49e29ea2a844a5b890073679eb9115ef5aa6be0fadffb5d587dc2095029ab529365b4bb9ad4d9d374c625622909543516ddd043600a0e8165062943d2841a701ccb886a1fc63283381e96a3ec896cb5bf7f0046c6b064e528742de886871e7a14763dff7ce45f122498003aa94bfb51713f08ca016c3c6303742fec06cc1285632390ab56c3cc57659269d3599b60cdda3534e6d1238761ffc143645863a313f0cb27b65006b07527af21b023d966e42b4b97f4c25b6efc3d78e2975b60707054013c43110395107416040a64168642192b999c485e087e8bd6e7cc4d1be1b2cb2ed3401f9d790ecf99fb1bad85bdc252f94ee48fd5286e3cfcb3476160605083c5c2b8a3d499b2c82f754c50a140c67184cb5292df68701ccac859ae38aa917de8189a48012a4ee13b672bc4cf3afb55dd9e1dbd63b635f61061c450d21bcf55aa4c7c54164375567c110025655c2e83fd9b62af8e7b411c186412cbf55c38a3551c1fa31767138cf5a3c1b801c766636f2b19b13bd68794092857263130ac8509c057c2bcdbff7dd744cda2e2fa3f1bdf5621dbfee6cbf57f169b9809b2c73edefe6ff3b5fda066659b2da8a0e349e79c732ebdb79affe74141907e2f0da9d9fe6ff3f15dd7f8646eb42e59f7ff56a399deff5d3cd2f464be9be45b8f2c6d5d9476add52c6a6453ae63c7bafff377ccea9ec79e88bf6956ac58316ffd7f365cff63d6a7bebe3e68261d387020b6c766fbbf6f5dccf3679d75167ce6339fa192845ffffad773c9a6da8a84e5083ff081f74736b382ee2b5d7ffd75a42bbcd78554ecffb3cfff6dde66bbb9b8ff67f17fd73cec73b3e1f7bffddd6e63b7f3f9896b2c9f5d16fe5ff8bfeb58e1ffe06de73bc63296f9a479c2feec730e7be2f644d27837720edb01ed3e3e65b88c364d59599d36eb82f882948bb74fae2c81d036369fc39a6b65eab251604a9b037f778dd7c819ecf964e19f264b23fdfb02884bc7aef38ddafa9cd6178cd3ecc3d667167bb76dc03ceefaec1abff0ffc2ffd37464cbc2df5bc9ffeb7883061079ecd4ab7721e2ac25c278588fad43064509117f06cd8b1ebcb3ec788c3357e9fea6ff33382b0623e03c453dd0411873a10c57fabd6ece467f60b804820818cc25659c018cb3ca080d3260c017de44e38c2e0c6249e4465d49051ed16d96af58060b7a7a08a023441897070bd34c07db62ff30805f3eb119468647a36365125b6ba56e8d14f0462961cd896b61d5aa55b078f162ca74d51edd0c58d0d505dd3ddd303038088ba36342a3bf280b902c51d62cb54eea8525ded4bb69a30071062b6c2c55462c851fd1a00b02e388b84d000c1a93aa4c5cdc390be9c986ea1dc75919cdebdcf3ce058db24b7849bda4ae144d09b73aa08d971a892727fb7da9a2fefa5f95e843160864c4b5ef8537bff977e1b75ef30af8c9038fc20f7e702f3cf4d39fc1ce1d3be1c8d13e024681b64b2c9f572e55e8af30d7ac5905676c3a1d2e7de12570f96fbc184e3bed34023149b26b050612587e90c0922adb18bd07cacf6a1355188f6e92752fe856c044a1c05013d14d3e0458a2dd0e0d0d43a5ada28048bae4e0e14347494f3ddd0b60fffe4351df21b8e79eef43ad8ac047041f2905982a2aa14d944258b674199cb6e154054a8a8eab72a5ea811496c30cc30e68a3d2584ac598150b8133582eab560aa0af7f089e7a6a1b2c5eb41016452facd188202ef4b557bfea65f0d5af7c0b7ef8c39f52f63329132914144b123e11fd457d9071b63ecac456228385d7fff6ebe094534f517263e9461942022f959014e00b94bd6b301c82aed027309bdca1c347e1c73ff9198c8d8e47674b7559973856483d76a8b3c5297f930aac1694923d85e29e4cb24b090dc2427fc4763afe603628069072bcd4a6586fb63a8e83796d84e0ace81d4b0c8ad84615f8894079a06278b23f0475f1bea6c7a632965226205c1d2349660314cb31298e8df6f59c9697e3691c3b6402806540567c539ac086b544d13c677bdf3640b9f369ffcf935cd7e9c5f53f38afc19a41a62df1f746faf0f9808f6cfb9c2dd7ff79ad8929431ed7ffdbb76f8791911128a8a099a00b2fbca0e5fc3f0f52fe57ff1da9557ffffb7466ef21be7ebe3da051db3c28effd9fc7b3d79a3ffbe4f0ad816f6cfb9ab5d1bae4e52fbe6b11a699d8ff593f98d5f9f0e1c3d06ceae8e8a07b33f3ddffcd63ad76fdaf4a446e8566d29e3d7ba27b75a354ca97e5609a69ffb7fbb9f8226196cb0f7ff8c30436fad4a7febf3863d77c2704ebe16bebd6a7e1eb5fff1a5c7bedb570c30d37c005175c50ecff30fbfcdff5d935fe5cd9ffed7e2ebe36d936e693cb753ceb35884b665f5b17afb4fddfd6936b0cd3367d72a6e9c89685bf17fe5ff8bff99d3f17fe3f790c176ffc5c4e1bc0c580bffb06b295e422d3205dfc6cc5db7d7ced5d63fa263f15a37319419abeecb9fa0294eb7c234a3336d7586986e19a4bd635f63987ed8caeb1b318749639a6e9d5c5c3fc6e8feb6a679fe7368d6ccd27bbcdd3172c5c9b581a0fdfd86932f8360b1715fe5ff8bf3d66cbf9bf7eb08ee7439d8d446aa05488000a9d55c4359f5067cb02fd19507ee42d54d613046020e167065061bf4097c58a4b1fe278a552fcc0be4e17a0325ee1f71a950384a4c421ca8f6d840602e84c5da0e740002d637cc932b23eb0a955128c7541fc510e2e53a87505bc76380f036c26806d4003b39007c90570fa6949798e182505c65717d1fc10f020e081077eaa8f0903649670a9e3118d77daa9eba1a3bd9d0035ed1d9d04621827304c40602c3c8e7fa1552a05303e16d6250c62fd8391692b3617e11596c030043003dbcf92d2600d98d4f1a30c613250e0145dca0ebb9e77ded9b072e5aa44c6581f8e7820270f9725628806324a239b91d12926c2b42588303ad7dbdb0d57bffc37e18adf78311cd87f80d298efddb7170eee3f14ddbce927e01e962e5cbe620595f15bb7ee64ba51dbddd5a5c15d914d1198275490b640fb9a54d9cd708d71b4f1e8265e5b7b1bd92602b9c6a3b5c61b7bb8b6b5b046a501d116155050c091234761243adf5e6983e19161e81f1880de458ba307b63b089c73efbd3f846ddb9ea56984214f540189a8ea1b68bf8b789db2fe642ab749764319b980c6910426129431eae73f7f0c56af5e158d7b049e7f7e37b5edeaec24bfae443639143d24def6ec7638ef9c4d5029b5291f8c5e2b562c876bae7919fce8473f55eb8c651a0d15cb40c68b8b402989c0282a818907422aa7b83e92ef77afff1d027c21f84a659e92749e32ca11781067c459ef94ed9327523c0b237f0ce0a9a7b6c2962dbf8efd3dce90e788ed0440943a348646b63ca93305421263e2bee87a1aa415e892ad6064efa3b540309291999062b0bd2f80ce3aa8db081d0f6b262856c76bb2091dd7941c108fcd7192636fa84b8f72ffc088a51c2305ef0108ded2e7794c9a071ee37d86c1ba32c98088d90bed58ccfb8ae43d0adb6a19b95d89f633ffcd07fe3cd7f7ff669229bb49c5f57f3e6be2ba6e2eaefffdfedc4ccaebfabfa082668a366e3c237a68bd843eb7aaffe74dcddeffd3ae71d2f4eae2617eb7c775b5b3cf731bd53fff356bf6fe9ff5dad0b7179b63a7f1f3edf169e7f2f4a1b4b1676affb73fe745f3d7ff5bfffa7fc992c59007d9fedde8fcf1f4ffacd7ff08207cf7bb6fa57b559ffdec672973574109616630cc1076f7dd77c355575d05175c706174dfeae5b076edda62ff9f25fe9f26c35cddffb3fabfcf1e5c7c5c769e75df9ee9fddfe71bf6f7ac3698758d0bff2ffcdfd5aef0ffc9734db3d34919b0cc01b8a1cbb85d86e39a8cadb8349e763fdb007dce612bc4e760599d3ecbb12cceef0a1cae79351a3b6dc1d38cc767206946dca84f9a6ed2e6e492d7d7c615801acdd1651f2e9b6b248b4bf73e4a734ad71aa4f98bcb7e7d3ab77dc2d5de35868f0aff2ffc7fb6fb3f8399184055324b4a19a5a3029d29251e2b3a4719a6c23079b88e7c8d360cb412c667247c281fe8f252f8c2ac2a81962734e44279429d01456557520febb9afa9436c0bfa1d39601b061a108840ca384b95d0bca595829e8f839e479c618b415a0494d0a00804bc6890178e4b6df5180a708159a526087872ea691ba054c24b06254768d69df311f1135446ee9967b66b1da0fc7c111feaf28156b75200edd1cd91f6ce0ed4346541aa4e4c50bba3478f4267673be9a6bdbd3d5a6b946902141824e1c1ea6c44d44f4946730e751622a16ab1293911d8c2720a75bc415825c528609ccadc4465daa27f1d5d6d70edeb7e8bb236e54a6cec26597352d34e3c026130e55264bb9d6538f9e4d570cafa13606222249022ae0fb628951118d446c027553a4e8367a4ca9ac6a5f278788587423b0e29635289d6b5833ee30bed14c1576d91be10785593cadeb1ddd1a34760c1822eb2472c8fb967ef3efadcddbd00060706a033bad1b67bef21f8f7bbbf47999eb05c27d98b54b3213b966a7e282b02fe369e7e3a01fcd0676bb52ab455da68fd15184d65337a62f366f8d5af9ea2328c6807d8660566885bb04095e7ac4dc0de3dbb61ed9a55b06af5aaa4a45ff4ef92175e08bdbdbd70f8f000d8181b2c49a88c4b7f47fd07080a54beb9a8b7076ebae9adea4121b9791867fbd3a8cd58b721db2b19aa0269e1f8a8b7b1891a3cf8d39fc140ff00e0cf002139131dc486cd718d6c97fc5441bae2782844023605e31a20b61d55f60fcb6480066771b94f331e734ca3f8a4e375bd4e54bce13286f17e639aab7674ce720522b9d63341b5341fe62b750cd4dfcd39854609da508f8fed4a06b8d6dc33e22c65461fda2f785cdc77387e73cc3781591a04ccb19ece837f1f04630ef6e7d9bcff8b2c017b06c975edd5a8ed5cbffecf8b841093e6ea6a331faffff3f293bcafff0b2a68a6e8d5af7e15bdb79affe741767c69d6fe6f7e768de7db5f5dfb83bd2708717cefffe545cddeff5df6ed5b17fb986beff5f1b4fbd936d568ffcf8bcceb14fbf84cedff7993cb16e69bfffbd6968fd9fdd3fcc596d5c7d73cdec8fff3a666fbbf2fdeb96440c27b87ef7ce73be1a28b2e82bff88b8fc3030ffc040aaa27cc8875fbedb7c35d77dd055ffbdad7e015afb806def18e77404f4f4fdca6f0ffb9fdfb7fb6ecff53f5ff633966ebc237ee4cefff597cc326d7d83e79ecf369ba29fcbff0ffc2ffdd7d7cfe1fd81d7d4a3519f982816de0360fbb8d6dac2e05dbf2340a4e2e9edccfc5cb361253412e674d735a571fdfe24cd7295c81ca45be35b5bfa7ad25bfdb4e9965cc2cc76d5d35724c9ba7cf165c81d8ece3b38534f95cdfcdfe69c7d2f4e60a74260f5ff06db4d9d87d7c7e99e573e1ff85ffb7baffd7f5d32f0250187a73492f20996360c8830fe1039337eac0908bb39a0848d6b9a40102711f96519f374b5361abf8c13ecfdd90878f85a6deb58ca62d9af29b2f533f66a9ade41cd481cde8b3012e002a2786400605daeaea6c831356afaa2b97a7c68674924a6f870f1f8143070f415c3290f4491a05027758eb8dc099356b4f88de3b3500678ccabf61ba7004afe0710483ad5cb93cba91d2a6415df1d2ab356a289c1691c4d4e013a18e60c6a35003a6083c2315900cac31d2c860c79f088074ce399be0424cebed953103f3e340c2f14f2f49f28a6711d08b0038610dca589a50a86c566d8184cef6122cecee885e9d91ad54a0ad8c25fdaa5157045b6159c8a84f20e973c0cc0510208a805b045433633997c55480275a0ba9b25f61d633044ae15fe97576a9cc686363e3f4bdbf7f804a0c0e0c0c41ad3a4159b16ebfe3dbb06ddbd6c8a415704bb1c7b14a2a2b9d063c219f934e5e0b6bd7ae490c3b6a4719eb100826027a4750562d92a74ac04741a5f8aa519bc3478ec2d0f030c98feb3c3a3a0e3b77ec82f148b67849a3f7b5279e08a7ac5f57e73b32465ce9328c8ed5c2562f7ff9d5f09ad7fe96519254c918f04a49c58b40840c7c63ee3c44b416c32323f0e0030f51e9446e11ca241ec5c0c350c5511ec7b49b44781de30c6be1e6b85e8a4f9864201449fc08382e734c030719f15b9a31d4bc7682c4979208a5dbebec8460c4c7bafe2c9b8ed9e6b95a58bf1675d790f13cebf798786c6eafc700bd8ff0989ca991e76802c0d49e03b12cf365ff6f252aaefff35d0fdf35bbddc69e3bd27cb8fecf83d2e46ec6f5ff33cf3c53948c29e8b8d3ba75eba2878baf6c59ffcf8b66cbef7f3ee68a9126cfb4bd204dbec9dff359933cf67f939f690f2ed97ce3b87cc4b557db34d5fd3f4f6af6fe8f9f3103f3a143cd2f4188f1d2a6f9e5fff5e3a61dcbebfadfb546cda666fbbf9ce2f53ffed67ee10b5f081ffde847e0b5af7d2dac58b1020a9a4c786fedc92737c3a73ef52978cd6b5e035ffef297a37b6adbeada14feef3e9697ff9be7e6cbfe3f55ff377539db7effa7d9956f4dedef696bc9ef3e9b4b1b33cbf1c2ff0bffe763e6bbabbf2d87796c36fb7f601ff07d4f3b6e1b7816c33585764dce1ec717785c06cafdb3040e97e19a8bd628e898f2fa16c76574a6acf667b35d9a634d976ce334c7b3e5b6e5b075e2d3bffdd9eceb72609fe1fa644f9b8f39a7acebc7fd5dbac94a597dc86e9316905cf6e35ab72c63650d74aeef69c70bff2ffcdfe66d7f9e69ffc7ec21f8003ed02500913bbef0183e44e707e9c4dbe42544fc10bc6494c7e252554894ad4acb5cd3d9a6b87c153f44afe9ec523437803853128f19689e35cd8b33b9e02be092835a7edc9643099a8fd072298091990586b36b0118401e43fe58e75a2e6c538ab36ea97953e946ca42a3b26d611620250bf6d165150301cb972f87152b576aad4a50430b95192a85107e806095eddb77c2c18387d4b821c75afc2a89470889f85846ad522943efc285baec9ca0726b08c25a17dd54eaec6823ce987509cbb161c937291540c8944606ac7f875c925f52cd55cf0c0cbdd33aa30ec9766402a0a3ef2148eb9f4d940148043aeb5948575b5d0b3ae1d5af7e05f42e5a44022000894137c2463f21f88d5e93e59f8476b15e5ab575af4c64f210c21a5ac4ff2f11282db265b2670dcf52aaa42c70b8868869c115094a82e626597742015b3033528db25c853118917d626c7c8c7483e03c0454b5b795e3b271c3c32364b707a31bcf98c1697070883256f5f5f7539b2347fb60746404f61d3808cf6edf050f3ff4a8cad255230722dd52d9be507da73103413677c6991ba335eaa6711068558ae20afb0fadbff6f340a838837147f996ba498537c3f11d7446a9fdfbf7c3e1434794ffd2c8212c5ebc084e39e544107a5162dba18542c5074a772574ca1a50d2b9e81c66d2bae9ed6f8513d69c8068b1680d22dd23a04db0cd68c89550f1426b1d180419af5f508ee65f83d3376e804d676f824a4785c6c17fd5683d281b5ea0f8102f8a3f403a52768f322923430017fb5812b3b07d8974a77c4fd03bfa83f231157784ceeec77a55be2808e4261934c5e6a86329b545409ef6458ef9744e839c14604f65d3c2d8c9312f30f6e0d0dc53c10082691b131c2775fc8c63b46ec3be61ee47718cd77b04c776de83680cdedb70efd0d90739669b2e6cdad67cdbfff322f33acd2563239a0fd7ff795171fd7ffcfcff58c89c0b7f6fe6f57f6895da2ea8a0e341679d75166cd8706a4bfb7f1e345b7effdb3236e2e3e37d2cfb7f3328effddf75dcb46d97ced3c673cdc5354ed6fd3f2fca6bffc7df1ad5ea04349bf0b70940e1ff2665f521bbcd4c5dff97ac6cffcda0c3870fc7bf7f999ae9ffd3bdfedfb061037ce6339f813ff9933f81134f3c110a7213fef1c3d6ad5be1831ffc20dc72cbad54a290ee77692afcdfffddd767a6fcdfa6f9b0ff4fd7ff6df97d73f1916b6f36e598a9fd7f3ae4b271d7fa9be78bebffc654f87fe1ffaee369fe1fd8c2b8be9b0cecf32e05b8bebb84b01ddce6eb9a886b82be76ae85b59dccf59e660836a5f5b50dd33ce69bc7541c294d8eb47ebe315c866d07b5b4fee6399ff1bbd6dec5d3d6876bdd7c7ded735975ea0ade3ee7b165328f99ef59e660db862bb8fa025f9a5e7ce364597f7b2e85ffbb69bef8bf4b5617a5f9bfbdde3e3dbbc66ee4ffa572297eb84f7df0a5fbe343f8724965bb6120166796424e04ec90faa13caf038ec3f2f0437c0d56e2d251a19155a5649493e207fd812e6f48d955f4790272a94928708f6e1f83bd6a4a8480803b259202cbf5b168613501258031dfd0b1260c1aa8855c32507dc63e0124c02c0558a811a009b92000834b8b29696bb071e369b062e50a057112ba9c9908b5a6fca4f8016c7be65938dad767c84ed02cfd4ef026f559d468bcdede6e58be620565c2421016cad7d9d9497ff1b9ffc02118181c267cc809ab57c2e2dec5041861fd6ab510d887e4970c8e53c7a5165bd909022774fe203d1513762434a045cafaef748cd64d43b1f0b380ba176646121ae88665f7105073ceb967c02b5ffd4a2aad5842d04f0016c84983db027fdc6539d2fed1f4b56cfcca42757b8a506b972043d81771ee916d0af4b9c44643dd90ec15ed0c4dada6814e42016b6a91f227a29b387179c15240be8b6505310313be46a31b3a582610b34821c067746c94324b61c629cc7a86bccb9532b4b7b51388abadbd4dd950a4e791d111d8b7ef00742c5804326887bffedb2fc09ebd0795b54b0582421938e39102c628a0cbc6d337c0e9a79f4663e2da913d4a48fc5efb45a5d24600290200054a7e5652b55a83febe7e98a0728a12c6c62760cf9ebdd19882f861bfce8e004e3d757d74131b818fa424e54b6464e8b365857f8a7c01ef734b598dec7c19fce1bb6f86f32fbc20b299488672342ea8528e814cb258498a2f047b235e94c98e9c3040b856c44bd964efc24ef8f30fff57f8e6d73f0f7ff3d7ff075ef3da57c34927ad8eecb242a506915f186a5eb8b6e104f925eab84659c074562cb25705f82a11e84b03cd28639988c158a84f2cf52835782ade13359095e33c01aa62a058a04b58420c922aeb58cc256639d60b765029e34c5b1c13e3f1b48dd39a19a5134b1a482b0d1fe07288f1752140f2cea512f567c1319df919656991187c45a03e2963402fcf5168b919881682ca94e7bb813ed7f7ffbc28ebf57fda75f15cbcfe6f052aaeff278f9917e579fddf4a3659d0dc20cc7af1dbbffd3b549208a9d5fdbf99145fff64d4499affbbcefb78dafa68f4fb3f4d9eac3135ebfeaf3943ded4acfddf1777f9b82b76dbdf7dd7512ebf708d9775ffcf8bf2ddff9bbf272e5cd8eb3c3e7ffc7f765cff379b868686606222010436dbff8ff5faffbaebae83071e7800defffe0f1028ab203f6ddefc04dc74d34df0bef7bd8f748654f8ffdcfbfdef9aa779bc95f6ff63f57fbb7d23db49ebdbbcfd1f32ed013e3b76f135d7cde79b2e4af37f7bbd7d7a768ddd4afe9fe617ade2ff69e314fedf3afe5f76390a37f42dba6b23f24dd677dc256423a37439a5ddd66ce7e26b936feee6b9347e8de667cae51adbd5c6d69b2f20a5ad836fae765b176f575b5b16bb5d5a7fd778d391d1e667cb9425d0da7c99479abcbe715de7d3eccdb539a5ad6ba3f54efbeed389cf670aff07af4c85ff0bef1833e9ffbe31f918b7c7cf6d950a3dcca607e2fa58a01a26329a250241fdf5206791e2327d5231ad1b03bfd7f48376fa5c5365b5084ca58ff1bbee98c866f0b329d02082aafe8b31caa8a5c1420858507333e6ac531b851edd061a54a2b2bf403c1fb3cc1767bd625dc5197d18b4c05964aad5586788583ae5949361e992c57ac4ec590818b0b475dbb3509da8aa0ba0187011bd305312a28d1020a2f58567962d5b0edddd9db07bf773b0241a77687008daa207244343c3d15a97095087eb8c609833366d8cf8efd0a0937492908035b81ce24cdd4bd756a0e6571204147ac94bae8275912e039da1a814cca000334128eea4e50f40d5be53f6466b4c99ce54293e2a4327816c1b7d07335a8992b27d954149eaf69c55ae467a191e1e8dd6ba0dc6ab135476107d81feda2efa3088250675fbe777ef26c01682b1304b5a7b7b856e02def5ddff806d5b9f010520e20b650971aa335040179469f1928570f10b2f82055d5daa54a1469e05bae42695e2c3d28be5b2513a3434feda33693f12c9501e1884de85dd946deaf0913e181f1d858e8e36953d2cb25df4a7de450be1e0fe3e00025a322a508f2742ad0f808ece7678c96fbe045eff3bafa72c5d0a3408da8e2540cafe662e1cad01287019c2b75067a79c7212ac3a61355cf6a217c2430ffd0cbeffbdfbe0de7b7f08cfefda0361a4c352b98d7a8761598d89614ea54703bdd871e6a838ce9262459c858fc09a90e8dc24028c11b04ec53b5e5308d53e5e95ca5f25efed908c2921715f61c65fdea3a501d0d2f10eb8bf795d01d6b504ef5300714ce218cf9a96507f2dc059ae185016f3d2e74a3adb17672fc4730cd4aaea7d48187b93a00c58a579b7ffb70aa55d1733cd97eb7f9129be1c7f12b69fcae2fadf355e9ed4ecebffd9103b0a9a5df4d297be14aebaeacaf87b2bfa7f9ee4d207d34cfa7f5a1f974c5963836b4dd3e4758d0b90ef9a20356bff6f74dc179fd38ebbe471f9d554f7ffbca8d9fb7fb2be900b71a9b6f9eaffa63ddaf2b7caf5ff09279c0079521efe6faf4bda7af8cee3f7b7bffd26b8e69a97c3473ffa51021799599e0aaaa7db6fbf9d7474e9a597c2dbdef63638fffcf3e30c794c85ffcfdedfff8d8eb7d2fe7fbcfcdf3c97c6afd1fc4cb95c63bbda4c65ffcf324eda783e1f4b5b2f57bbb4feaef1b2c8e81b938f71fb3cfcdf1cdb96bf15fcdf77acf0ff7acadbff0393816b516ce34e3308d700a6102ec1ed49980a743996fdd927834b661f2f9fa26d7ebe799964caece26bb735e7692fb279ae910cb6e1d9e3d8b2bae66eb7b5f5ef9aabbdaebe80e43354b3bf4b4eb34f9a23d972f9dafbd6d99eb73976a320e89a83efb34b176976678e63f34c1bc79e4b5a90f1cdb3f07f803479795e2615fe9f8fff63193a013206102131c089b39be051593f017a21dfb8a41567bb0188b365611bce7ac27c625e5a3f6669140665040de215f6ab69c05845673b115a26dd428fc4f27a74209444aa945b2de611eb5af7090dfb20f086b9d646161ab0ca2906d18fe995ab564347473b4c9524bd0278ea575ba086a9f2119821c3fa39016820922ad387e36ed8b09e4a16ae5abd0606b0b41c662fab85b06ad50ae8ececa04e631313508964bbecb28ba15c9ab4ba7e990428c01c64ee322d925ac71a36021b4e3d055ef55baf8af4d8a11a64dbce5a9e689e52eacc616a41117c8525fcc82eb1d41a818b80005541b90413e31371ee33fccc3102415a084e9ba88604ba1a9f188f7ca31cf97745956b04a0ac5908b42a9502ea3f3a32466518d0aebabb1740cfc2a570d75dff01dffeb7bb41d64a91ef0686acca379007e89c5d98eceaec7337c1dab56b223f2891fc815e1b95ddc98ccba0ec377651a15f40fc14380bcb240e4532e1e72a4c4c8cc1d1a3479921c9bd6ae50ae8eaec049f1110284dfbe5a597be10de75cbcdb066cd1a02828963b41bcc4aa69299a978d7d15186134f5806af7fdd2be1cf3ffc41f8e33f7e3f9c7bded950ae08923fa4b47cd81e01439899ac0210673b534034ccb146f202c4e500e3d81ec7efc9004953b75c36103f9734408f884bc3ea78246592f10f8f07d6f54a1cd7cc715463c3ce647c9c79725ce78c59b4560cdad57b00c7c8d0b15fc62561752c65c095d463d0de6294b9755d833121483130b22adae7e7eafeefbb9e6b16b9ae538bebfffcd6c477cdee3ae7ebe79aa37ddc9cb33d8e6b5ca6bcaeffc5b16e02c78166c2ff7d3c5cc70b2ae858094babbff39defa26bf256f5ffbc485dba15f7ffdcfb7f3e7b625efbbfbd7ef5ba70936bad5de76c197c63bbfaf9cee545cddaffebe7dafc79f747f764e6b7ffd7cfcf35a6ebb34b1769fb8e398ecd336d1ca4ddbb77439ed46cffb7c73e96ebff850b175259e27ffcc77f84bffccbbf84abae7a09dd8f2ac84dfbf7ef873beeb8036ebef966f8ca57be42a50a910aff9f2c53b3fc7fbeedfff6d8c7e2ffae75f6cdcba43cf67f73cc34fb70e9dff64fdb0f6cbbf0c96f8f658e53f87fe1ff85ffbbd72d70394123c5fa047239a36b60bbaf2983cfa1b338bfebb81de05c8e669e6fb4e0f63c7c0be55a485bb7e642d9bab0cf993cedb9daf3b4dbbb0cc1b756b6b1a439bc6b1d5dbc5d41ce9ebfbd06be316dbe5964751dcf72de358fb4f17cc1cafeec9b8b8b97ed17a6be6c5e2eddd932bb6caaf0ffc2ff6d3df9c6b4f99a73c9d3ffdbda2a4065b010c824440c02a03ebacc1367aee13ef890dfb5f6f4a05de807ea280bbe787c500fd289bfd4802ccd9ffb97acf35e8ace977579426a5f332f6e423da4020ba8925e900033f48bc107082811249fd00fee131d71d92fce9085c742635ef41db3b0688000cbcc2514cb4105d69db28eb2f94c9550c66a34af3dbbf72819b83c1d658652f22260ad5a0b0974a0403a65ba0172e69967c2c8c8287475751100a7adad3dbaf13740ebdc1edd0ce93bda47809d8d679c06cb562c0521544943352eaf96218bd68f2a72462b06d248e7a4740ad9287561d528a106e420cfb6b632bcf14d6f8033cedc4819844a2591215f9797fdf16993858d4f217c5cdb2a13aee5f8d8b8b24d2a3b17aa4c3f98e14860b63704248dd3bbd099a5f0c61602b626aa13d417cb0c8e8e8e44edaa94350d3304d5aa21ddf4457d22e06a7068109edbb98beca1bbbb8bb2a48d8f4fc04464023ffaf103f0cfdfbc03060686408186503273c11834a4ca5d9e74d2dae866db26faab3d028d49352f9c0382686a46863af55ea20c6c940d4b974d045d8a0f686dcbf4797864381a2780f1094919dc64a8ec02b33bf5f4f6c082055da032877166262d1d05030540429f78f5ab5f09679f7d168d5129a1ce54c942398d1befaa7ca484b8ea2601de6a502a4763453c972d5b04d75df7fae8a6e3c7e0a52f8b6e3ab647eb1644f3a79293aab4614859ba708eaa74a364a1394e92ee82fad88e4d4c50164cde373186051af014c72bec2fad1f50aeeb132bae498ecd5a9e389315400cb4223935582a2eff178609b68ef70b1ddf4df0548963a596856d04e3799c8191c7150a84887261963606094b2d4360f812cb8bbaa8542af36eff671f6b0572cd234d37f6f5e35cb9fe775d8f35835cf364b2e5f61db7edda65a3ae6b405306d767fe9ee7f57f5e3493feefb23f57df820a3a56c29283ef7def7b61c386535bdaff5b21fe9ad42afe9ff55a252d66a49d4fdffff38943f63d8c66ecff2e72f985ef9ab2515ffb1ac7b5a659f6ff56d81b9ab9ffe377cc925b2e57a0d974e0c0fe79eeffad7ffd9f873f20a099ffc0b0d9feef3a6eeac5759c3fa7edffaf7ded6be1339ff934fcf55fff355c76d96571a9e282261302b13ef6b18fc1273ef1c9e8f3013a56f8ffecfefdefa256dcff7df33956ff777de6efae63a6bc33bdff8b0cf1dff62fbbaf4b4fbe316dbee65c7cbc0bff2ffc3fadef7cf6ffc05684cdc0a5049fe1980265e1651a6aa3c9d90be5ea63f7b795d0c839d20cdbe508bec5b7c9a50b97a1da0e661b894b5e7b5e263fdbe86cfdf8d6ce1ec73c6f0780349dfae4b7e534bfdb9b9079dc654ff6f8b65d99ef2e9b73d9904f9f2e728de7ea973657d7dc6ddeae79a4f941a3b5653e669bc2ff0bffb7dbd8e75bd5ff119083e5bf106c2439f30803b0c061c3d83794f10d4dcebe62663689e7a8db87a68e64f2905dd8edb18d94ba6497c0ffc57c42638cbacc2ad8360043bf0a21c1800002580988816571b6180693e0679681d747cb26b5bcb19e0ddd8169c7a6cd6329382d6f5777279c71c6c6fa35d4634f5a530043278a577f5f3f1cedeb0304a3501f5c27029609124bc425bb684002b0742de8848181fee847fc3e557e0e811fb52ad4aa35181b1da58c4208cec25289a7450f504e3b6d7dd47542f36540107e4ee605960daa75650dc9c44e482fe1e4573c5f1214c0cce425279765448e329a6ba95286cbaf7831bcf14dbf47651483d8b7a11e169416a74938f512196efe0ba8ef732ccf0becd807b1ec4ad6508394b0d4a00cb5ff805237029a703daa13e3a4566cd7566ea32c66582690b3d38d8d8f12e00469686810162f5a4c6d3bbbbaa273e3303a364676807e80b6848096254b1713cf050bbaa95ce150f4fac9030fc36d9ffd0738b0ff60140b300bd70481bb482342ad355a1f02e01044857676e5552f86952b56287f0e94dfa1dc0a0c14501fca02c5f3893e23380cc15908c2c28c79812e27a74a282a7f1c1a1a8189c85eb1cc62ffe008816f687da3760b1660a6ae05345f09892f9ab6a84e4ae8ec6857e024f22bb4bbaa02f9803baeda7650b77efa98726e20bb55d9bea23813a892915d5d15b8f0a2f3e0fffddfff0bde75cb3b60d99245d1f1c42e63908c401052598b29e3ac72b1ed712c672015ea4767faa34c5e00753e171819b254d63475ae663c74e2f932b896c6354afb09433eeccf59a8180c256502b6aae9f5c03ea5784e220665c5af7855f45e142f8de3bacbd8c3020ddc025eab40c988e312e0552f5da041c371248a64ab440f5614a878feedff7990eb9ac23ce7f333fb3ad63e675f47dafdccf32e9db9ae53cdb6be3da3d1da321fb38d7dcdc5941728ce259f7d9dec23db46f9dd67ab761f7bddec97d9d694d767e7b65e6d5bb0e755b7df42ba7d36939ae1ffa62ecce379cdb9a0b947f810f5ddef7e37bcf18d6f9c15fe9f0f4d8e39ade0ffbe5868ef61f6bb7d2e6defccbaff379b6cfb6bc6feeff28946e364e1e5da975dbc4cf9d2f6ffbcf78766eefffcbe78f16258ba7409e445f3ddff5de3b9faa5cdd535779bb76b1e59fdbf996466926cb6ffdbfdcd3dcae6e9d2abbd1efc8eafdede5e78c52b5e01dffce637e1939ffc245c73cd2ba02037f545f7e73efde94fc1b5d7be16eebdf75e3a56f8ffecfdfdeff28946e3cc35ffb75f665b736ea2c9fbbf3d2f939f6d4fb67e1ad988ebbc299fddcfb75eb6fcb69ce6f7c2ff0bff771d9babfe1fd8c2d8429b82b90cda16d23548560375f1f6c9e40a602e277529354d769b7c0b659f73f5f3cddf5ec02c4e64f7b7e7e73268db816d1e69b2fbc8a757d71ad97d6cfe3e27b33721938fcb065cfab0cfbb64b3f5e60a782e9bb2e761f3703966da1afb02aa4f9776c070adb37d3e4d8f85ff17fe9f36ae49adeafff8c39feace9bfdf583f43a1ea0cb63e96c267580282e3705d448f55582c57d5db6c4631008406799225979ae522665aef439ce8e5597e945f767f0986a1f68a098facc25b1044b2421ce2454674ff86e9412a473f86279f43b00830c1490015f2523d30b0ed1bd6001ac5cb99cdaaa01d44bc43c6ce2f9e24bc0eedd7be1e8d1fe886f5983615056a9311ab85ebc162181b3962d5f0abd0b7ba33e7db062f972aa88d8d5d5011d9d5dc4bd5ca968d04b40a5de7a1776c34bafbe2a3aae413022015350962d8fad0b86a3b06e583d4aa9900e5ed27df4495977869443800b7c3ff9e435f0810ffc6758ba6c99cabf25c4947151c2f8979584f56f5a644c4c6561536024cc60457649c0331183b1100c582a97006155581610c126a827041f21b807415608a6c26c6faa0c20501b0443615941a4a054a614e6989d6ca23a4e402ccca285802d6c83dffb0706606464847c79607018c6ab121efed963f04f5ffa3aecdf7b50f90b805e7fd07e22d532070a608599b3aeb8e24570f2ba7591cc0824e28c460a0448502d5923bfc3ac570182004388db0001246b94914af9aa8a2d0a84a5f43416cd154b784e44f32199d0b74a0195f3ecea5a1083009358a1568ed70ff5343a3aaa2059e4bf61a4ab80fc0792a6fef5b5ed984cd3684b99aa04cd51c52e49202f1156e194f5abe103efbb19defbde77463a5a1db5c3f5a9c53ecfe03960bdc636c219bd44126724e88580381316e8399b1905b10df22d69a09662a3e5c5b6421899a892585f33b21d32b19f299b4d325b711cb763b8d4e73816d7c90b89afe2b95a580fb88cf9e939904c98f54ac77ade1348f65846d51ef952394dad2b9415c18676c983f9b0ffe7452e7d15d7ff8ac27032b8b819e4ba96679aefd7ff7951ded7ff1813e93abfa082a64908be7afffb3f00b7de7a2b7d6e75ffcfd3df6d6ad5dfff69f1de279bbd774e6dffcff75a85df9bb1ffdbd746b63eec317c7af7e9dfd5d6b55737daff5b89667affcf7bbef6dacd3fff9f1dd7ff79505efeef5a2b973dd8328a29eeffd75e7b2dfcf55f7f06bef18d6f1028ebc4134f2cca133ae8b9e79ea36bac37bff906d8b66d5b7cbcf07f70ce7926fc7f3eedffcdf2ffb4f531db8a26ecff2e1b30fd278d479aec3ef2e9d5e763661f9b7fe1ff85ffcf77ff0fcc86ae09371a24eba0f6c0b67137225b269713db8ab48ddbe463f3f219998f97cbd86c591a39ae4dbe40608e9fc6d767483ee774f1f6cdd794cb378ecbc97d41c46eefebef92df0e40661f97ae7d8e6bafbbcf8e6c7bf5c993665bae39b9746eb7b3f5e31adbe5e02e7ff1e9a6f0ffc2ff67bbff63261b7e684fe31a5953e2d2526c4300715615041b71f6130662810651f14b2642a976baa41f0395900ff2e3f257c290d32e43a82b7f51bb50039e0467cd21d0881e83901e7a4d7549410436d434c0030f50b934539f7a0c7e60ca1958780ca94b5f01af1ff10d34a021012dd534b082c00e118755ab56425767276421497a84bab5dab3671f0c0e0c2b09495601f6fd6b214a31506851ef2258bc6411747674427bf4c20c48fb0f1e82a3478fc0b265cb95beab355d020ccbfb95e08a17bf10962f5faa0031a40319cbc3e51bfd64da2adb5460bddcfd7c7e40b611755bbca817fee00f7e1fce79c1b9044022cead5365ab3191014952648d405761ec4f3559a3ac64085242d054dc2132332c1d58aeb429fb22e06089d601814cca8e6bd4062483174b50c16c52da6687868755362da944181d1921c0d6d0e030e9767ca246624d44ef070ff5c1dffeede7e10b9fff32ecdab557e78fd2d98704020a418199a42ae9872790c785179d0fe75f705e5cb293e420df02b2d332ca0a109fa3727281b25f1503b58f87aab41cdb34979d4450d7c0c02065c0c20c60b59a02ab11ef4a19da30e317c7256d7b60b887205e3518191dd19ae5b29da8332e517aec0b1c69289a1b96ff34c04bd1c77249c292253df0f6b7bf19def39fdf053d0bbb22bf1b8fdad6621b50a146f908c9acd33a29f0a788e319677f221589242355a22fe583a82bb689383320401dbc8ce37f291071f63106bf0ae03549e27f3de8c99cba88330ad22c74ec8bc155c6fec17b1e82aa38fba009f08de3804c321f929cc65c63502067d832e683f10c743f6c83654510a467ce793eedff795071fd0f75f3f09d6f16d96b60ca545cffe743cdf27fdf5ae0c3ae562a535ad0ec23cc7c75e38d37d043d3d9e2ff79105fdfe6b1ff375a0b7eb7fbf8f638d718f6ba4f65ff6f81e5216ac6fe9f36b6fdddec6bb731edc7dec71b51d6fd3f2fca63fff7f9613328cdaff9f35cf67f973c69d796ae39b9ae57ed76b67e5c63fbfcffc081839007f9d6ae19fe2f9a74fd8fc06d2c47f899cf7c063ef7b9cfc1eb5ef77a38f5d40d50503d6136acfbeebb17feeccffe0c76eedc49c70aff6f8effdb72cc87fdbf59fe6ff3cb6bffb775e4ba1e708d671ef3d9a24b77aeb6e6f8e667dffa16fe5ff87fe1ff8a029f4036f32c8a3179d813b707b60ddc9ea4bd983e995cf2bbc856b6dd2e6d8ef6a2b88c9e79a4f131dbdb8b693b8d6b91eda0e993cb17405c3ab765b30dcae6ef734e97e1d9b26671589b9f391fd767df9abae66c8fe993d7a5176e6baf8bf4045793bf4be6467ae1bee631df1a9aef3e7dba82bf6f8cc2ff27f775cdd13cc63c0affcfc7ff3b3b3be3cc28741c65e197481ecee3e79291e98afa837af85ee207e3359de585f59908a5e4d219abf8213f40524e5078e6c5d9586a526a208792d5cc7ea5e6a28161e5523c81a0a43e109028e43586b8ec19cd35d00006e68d63eb17587132964d03c0d43c34384bcf13416655029a8570e6a63354d6a9c0b699745b474260cabe7dfb606c54011b484dbaab613d910cd568ce018db16cd91258b572a52a533786801b098b172e8c8eaf80b1b15168abb4453740da284b1296221c1f9f800deb4f8657bef265d0d1de018800225ba035e411fca48066860d67ca1625751938a980201ac413675c8acea09c6f7ad31be086b7bc09ba1774abb102b52a2585ae03c83456be84a013b40fb431b407b2dbe85fa55ca1f28a982daa3a31a1caf6950401a5d88eb1cfd0d01054a2f5aa5146a851caf623b42d625b04451d8ed611f90e0e0e5226b3f1b17102291d39d24732a07fb745e3e1bab745af35ab57c1f3cfef86ad5bb7c1df7df60b70c7eddf85c307fb015d177486272994aef91f840a84d85e6e830b2f7a015c7ef9e5540a105bc7e04b0640d642021aa16c5c1a54d9aed0be96e80681562a6356b2a42a735d18d97f0d864746600241583aae08fea76315eb947c9c4446bb0d75062e09a3c3635a32359f467bccd409adbf0c0c0f154a014048ba2084de9e6e78d3efbd0edef5ae9b60e1a20504ba23e0aa8e3f249b08e23280a0b58e58379de429562d67a2c279958db27faa93caa8867c393320819700eacbff997b3e035f851a88b24e196d39e605d63e4bfb00caaf79733b0401b6e90c2b66db38d60b05a822b0af9691a5a1f80b1003b898bf292b683098108962a42903a8b85ea612846db11ccc1f8cb1e6f2fedf4a64ceadb8fe6f1ed97a72c9345faffff3a2bcafff0b2a68bab432fa4d71ebadef86f7bdef7db030fa3d61522bfb7f5e76cfc3e6b1fffbfcdfb597baf8bbae79ec317df24ed6c3e4fd3faf3dd1a456d8ffedf1cceb145bd7f6fab878986beab2b5b4fd3faf4c9d4ccddcffcdb5cac3169f7efae979edff765fdbbf4cfe2e991be985fb9ac75cb2dbefa63e11fcd26c42605260fc616a33fddfa54bfb9c4fe7d3ddfff137faa64d9be02ffff293f0c31fde071ffef0ffa0ef05d5d30f7ef003f8c0073e00dbb76f8f8f15fe3ffb7fffb7d2fe9f87ffbbd63a8d6c7b39d6fddf25972d876b2d5c72da76e1e2ef5a13d7bc6cbe3e3b2ffcbff0fff9ecff814b292ea36d34807dce3636173f53109fb3b816d1e66b7f77197016c5f80cc424978eec4060cac99f6d1e697af1059646c1c2b506367f3b18d86d5deb9016085c01c596c717f0b2ae8fab7da3e3b683f86cd777ded495cd378b9dd8fcd3e471f1c8e23b763b5f80b575eeea63ca51f87fe1ff365fb35d2bfa3f660ba1b18c76f8601e014f0c7e4200043ea08fd785e7a05f9a993ac7fd8dec27cc93b3ade0f871f616e6a11fb0d3e66a64642159adf1a496951fd8534619a9c050d814811f0884c2ec2858728cc02d81cec25253e3625695a424613def784c948f6f4eea2c589c3526592706c5802e4506046009a3f7934e3e89c036321a339009c02144908930b2e680885f0c14c3927108c0c2326d2854002ad315e290f88525d182a04c001d04e4f42cec86c1a12158d4db03878f1ca52c4798054855ac0b61616f37cd014120ed1d1d3030d04fa09c57bff2e5b06cf9629e3d0158a8a42301a4f07b8d5e8601d14bc605f440cf5f90eecd17627ae236ac6bf403321405e02ae96c4f8ca8b8fcf24be1cd37bc11162f5942a0235e21d23ab555f3d1d6a65f7a15a5f53a0e24a4fb051c6742697c0ee91c83f4c8e741d989d440ba71ccea8459aca27e0886c3129313d1f76a6427ed6ded04caa2ec57d1dcd18e711608e4e30c6b78f38a4bec2dece98ef84d10800eed1bed606c741c7a7b7bc98fb0cd8ee7765119c283070fc1c0e0101c393a085fffe61d70dff7ef87d191312da32e298a768ebe4120a690c04088aba9b497e1ccb336c2e5575c0e95b68ad2b576182e9d87d9b8707e41a065afe2fc396e487d5e1944a5d2a67516c6d9918466c8a09de1e1115424956494bae4678d0159e43f32b1039dd94a4a368330d2ed18f507d31464e0be9e8916adee45cb2b62eb326372fca2a1d8aa75293c9d5d4e846a2e0bbadbe12d6f79235c7ad9c591af09f2232a55a8f58df30f75f6288ab1a477bd8f718942ad6ace4655d3fe5732626e20441d1809ec3d8ba534faf0b99a118b0323b321c769948b3315d63883974cc059dc0efb7386447b0f65a02d8f87149ad7523ae6723fe6137bbf9e433c9e94312098e66d80b7da8d0c1d00f377ff6f1615d7ffe9d7ff7990cb4e8bebfffcfca595aeff0b2a282bad58b1026ebbedb6e821e0fb9de75bd9fff3b6fbd9f0fbdf3e9eb6dffa784c65ffcf734f34a999fbbf4f87ae73ae7ddae667caefb273d7f1b4fd3f2f3fc963ff77f56b26e16fa9c3870fcf5bffcf22b7c93f4d9e99bafedfbf7f3f349b962f5f4e25a2f3f07f9b7f56fb32c76ad4bed1fefff6b7df047ffff77f4f60a357bce2955050423ff9c94fe08ffee88fe089279ea0ef85ffcfacff9be3cc87fddf25afd9268d8e97ffe7b1ff9b7c6d5dd9fccdf577b575ad83cb074c3d14fb3f3494bdf0ffc9fc4cf9e7abff07b6102ea73407b00777050a5f7ffeec3300bb8d3901dbe1cd73bea0e4e26dcbef0a122e59ec79a491cbe87d6dcc39d8f3b59dc8eceb3ade48767b1d1be9cd36205fd075c9e4324697a3d80e65dba0ada3343bb3fb9b6bebda386c1e2e594c39ecbe2e5dbad6ce379ea9cb46e74df2cdc92587d9c6b79e2eddb96428fcbff07f80d6f4ff050b3aa15c2ac50fe4f1c50fe1f1189702942cb394f59b1fcbcff333e6161a6326301af5909cfb01cba479c773e6bf06d37de287fd90800df038678752d9941468448915b510b1e4749c1ed8ebb285788c4a18e2b8c8c0f00fca5263c5403ccf600404a009d6991e41e8cc4c04c80a30d5760556ad5ca1e4022c7918ea3640fd430ddf08644901a9f43f3a169d1a1818802d5b7e4d63231887cbb0994b88600d0556011a73d9b265b074c9123874e800017050562cd7d6dfdf47198b30e355575797e2a119556b5578e1a517c2bbdefd0ee85ad006414992ee840686800972910c7651fa62080aca474012849748a87b11d02cb611dd8795068a25db4ca5ad0d5e74f945f0fffec54760d3d96729c0992e3f98f4d79f048b61c72161bc6690a402840460d80881533003da044d10b3404d8c8f936eb02d02a5781604da2a05943508fb5259c1488308bcc3c6b8566d6d1d049cc2f28308c4c27378938ecaf691fd2a3fa03140521fb45d2c3d39343c06a363e3d416415a0b17f64067b4f69fff87afc1273ef929f8d1fd0fc2d8d8445c128fc061b482644cb46e94b92b1c87f6ce126cdcb801366c580f478e1ca194e87bf6eca61bc943830330363a1a8d3d1acf132780404715534a94110be74bef52790b8dab419de833aacc5ce45f04b6025af791e1412a9988e5fb342293c0694aa7daeb425e7506040a0db692a43b102a2395f06c3f345f3c57b35e92fd53811d63930a301b1302c0d827d44b1068ada4619498090a4b46aaf28427ad5d0d1ffcafef858b5f7861d46f2cd20382deaa51dc2dd3dc9185ca24588a3344d19ca42a098a603bc9fad2e7eb6217b6d5a0a9c08857144f8d6c831c5fcdbf782f19190e5513ff7501c7e1b894a01075a562f178bc6fe8f2ad608cc7e394ecfd9763af31061567d4bc883724fb09b7e5fda9c42538a37e5d5d9d919d7768b6f367ffcf935cd7224cb62ef9bcddd675ed66be9b63f171fb9ac775de245bc7cdb8fecf938aeb7ff74dccbcd625cfeb7fcc5cc499010b2aa811ad58b112fef44fff14eebbef3e38fffcf3c976669bffe745c94fdbd6fffd6f7eb66571ed95aef59ecafe9fe7daf8e6843453fbbfa977bb4da375b17dc1a763bb8ded0f262f9161ffcf8b9ab1fff33b66f5437069b3097f8f0f0f0fcf5bff37fb986d5d6be71b8f8fdb73769d3729abffefdaf51ce44979fbbf8b37f3303fbbe2b92d8b3d8f34c232d9ef7ffffbe1b6db3e0b0f3ef820bce31def804d9bce2aae5b237ae08107e09a6bae816f7ce31b14430aff2f9effb9dacf66ff6fe6feefe2ed93dd5ec7467af3d98b6bfd1a8d63ebc49e77e1ff85ffcf47ff2fdb13341bd8c2d882998b9c265c9ac398bc5ce75cedd2c67239791af9e6ee0a082e5ef662ba0cd565883e63b6dbd9f3f1e9c0b5162e034e239feeb21cf7ad9f6bbe697af43982af7dd631ed60e40b363e72e9da3edec887d26cd36733763f9f4db96cc6674ff69885ff17feef1a6b36f97f77770fb4b5b7c719a9e8bc39b6accf48153588dbd6c9a9be281efa3bf7a1631a2060f6d502b9d7095449a9d02c8dc5c735888ac7609925bfeb6c55d447cb4572e3470603e86c5bb1feb4cc0472e0125f5a2e2a77c8bca50242513f4326a165127a4ef8c06be5ca152a1394542fa89fb6e6cfa5c5102821b48e00068746a21b417bb46e14e008ac326a12545547442b20d06bc9d2c591fce3384998181f8bbe2f8f3e56a1569b80bdfbf6437757176505c3325d030383b0ac7d090c47e32ceced85ebaffb6df8c5cf1f873b6eff36652e0a4341f2080491805e7f046669a80b81437476215c095512121cf6c6f621f4f403bd1e100365102483325d74f1b9f0fffcb73f860da79d06311a436a5db1d2e2b273e9f1a9194412840ab414834ca2059fa88e53862702fd61fa28a9d617b3a1912f68d16b08102290530d16747552c9bdb206934cc82a8191504913d5093ad68d20ba8971181919858ec867b13d967e43bd60062d95494a425767178c12b8aa46a5fc2a950aecdf7f103a3b3ac99eb66ddb1ea95601ff48762a3328639fe1c951d23619443c24ecd8b10b9edfb597d6a48cc0b1b63265e542de98456f5164432b227bef5dd40bbd91edf72e5a0c95a88d0cf572a31a42a97395256b87609b1a5f5f04aa9c1f8388b03463ad3aa6803894498bc165c371b62cce6c85ed03952a0e185345c709e826262f5ca3b02eeb2525b5c4804e50b1cef89af413609a2b55cf8be2d8b9e79e0d7ff0b61be1e9a7b7c281fd87687c048f22d832d0655015e08dc14a1c5f34788cb35269601301a150b1c443b78d65484051818e73743e0ce3ef764c576342e26c3aded18f9dc8c618fc24a01e0ccbe3d35cb01b96014470aa6e1bea71cd7d2029dfaab26891fc1a945b278fb99f6b5939d6e239bc112974c954a1f725ccced619d9ff7cdbff5b955cbab68fcf87ebff6692efbabcb8fecfe6cf3345795effaf5ebd5a817b0b2a2885962c5902679f7d36dc72cb2d546ada47b3c1fff3a6d9f0fbbfd198e6dab86268967d226bdb66505efbbf6fdcb43dd9b73ee6583efb71b54b1babd9d448ef4833b5fff371dc139b4df81bfdc08103b076ed5afa3edffc7f365cffe3fa349b100cd8d1d15177ac99fe9fa66317c919deff118cf5c10f7e90d662f3e6cdf0a52f7d097efef39f5346f7b1b13198aff4918f7c04868686e0ad6f7d2b7d2ffcfff8fbbf6f3fb1c7f4f94023395b71ff6f35ff37fbcdd4fe6fcae9b26fd71c5d94e53abfd17cb2f8a22cf67f274fb34fe1ff93692efa7fd914c425449a7066bfb405b6276cb7b385f5051d170f5fbb3487f77d77c9600606f3b86f817c01d5fc9ec5605de71b05635b5ed71c6d7e2eb96dd9edf3bee0d3c8d8edfee65869b2f88286dd26abbe5c3cec73b66cbef746e3fbc649ebe792292da8bafaf88288abad2da3cdc7d7c7ec57f87fe1ff79fa3ffe75516f6f0fecd8a91e6e5376129d79841f94239f403fbcc13658022cd0997b685c9e8750e0037c584f25aa8c52562c8f308004d8bea433b9301f1009088cf8f3dcf4d8788eb3f6605be0355403c563d8b620eae66e1dd7e3c6e974b0af062870b92c06390068dde876f1fcb4dc4a7712162d5e04ab4e58ad06e04c39bc167508905065ed510c1528261070f4f061387ce80828f496d613679531f8107c263a8ed974108cb33cba91d377e42081748e46ef3d3d0b616177378c46372d30c3140275c6c6c7a83dca70f0d0e1c8062ab06cc92278f7ad6f874307f7c30feffb49244fa0e71a2472caaa0640685568601803deb41a0cf970daa2eebbea16681d8454e210cbe7bdf4a5bf01effbc07be1bc17bc80802826a14d923d65f493a61099a806921000ada44b0c0a025ab1616149402c85474013cc6415b599a85509b88420b952a9426d4003dbf02129de60c2f382805d3502c094ca25e8efeb87050bbaa21b759d549a120159038383745317412fb88e78836a646438f25141e02bcaa025daa0bdbd8d002b575ef962f8fbcf7d91405548522d929a837230759c815391ccd5f12a0c8c0fd25a524ba9ec4e50863265ff94c1a952a60c448b23db3ff9e413e1d453d7c349d17b5b5bbbe229429aabd4203ab4f390fc945d5944f3d03e4d361590cc251d6b90060786e9a512a995a8a45f8c8962a022961d85202ea319131ba0802992842485969c02235c4f156bba1674c2ab5e750ddcf9ddff806f7df35b548e11b3da498caba10632ea328c820159384729e21812ef2960c473bd88711c038863625dfc374050810670d5a894a388fd8bcb0a82b92f4a6593e64cedfd9878ea77e4c9edb96cad30325971ac8ef7dba82d0211f1589cfdcb8ae72c3f7f463d051ae415b03c5a670804ac54ca75ab305ff6ff3cc9756d585cffe74bc5f57fba1e9a4d795effaf5bb72e974c02cb97afa0eb9fbebe3e28a875e9c20b2f8cae0daf8457bce215d175dba9f43078aef87fb349fd0e2aeeffa5edfb79509a7dccf4feefdb836c5b77f1f4b533bfbbe63695fdbfd964cbd5ccfd9fbf6336f06613feb637013ef3cdff5d31d135be6f9cb47e2e99a67afd9f07f80a094b10d2ef668f7c33edffae78e45ad334998ef7fe8fd72008c442b022667f7af4d19fc3934f6e86cf7ef636d8b66d2bcc4742fbfce4273f09975c72096cdab4a9f07f071d8bffcfd7fdbf15fd7fa6f77fdb4ecc316d596c7e2eb96dd95db66ef34c9b8f6f5c73acc2ff2753e1ffe963ce05ff2ffb149db678ae45f605813405351adbfcee32665b169fa21a0507d7c2b9e4b46531dbf91c386d2cf3983d96cfc15de364314e171f7b4c57bbb479fb0253a3f1f8b8f9dd371f9f8c5902973d7ea3fe69c1c377ce3596ddceb5662efbf4c96dcb608f911674d36cc0378e3946e1ff85ff9bedcc715ac9ff4ba5007a162e24800f525d86aae8bda41f74e367a9b34e05260808c730004f047a908e07e8f8d22000a9334a0996d9f629d0c02723db0b536894d4e2a39cf98ace1bfc80d781bf0b053ee079083d2f5b67a0cf493d0666e801ceaa05aa44606865d2e2ec329a0b95fceb59d84ba5c9eac11b932fe8a42e4d884d299b9428c3ee3d07e0e8d17ea092864617b3ec18b3437910e081631e3d7c9000564b962c86c3478ec2c4c4388c8e57a8218273901f027a7a7a7a60a25aa5cc4bc8034b1e9e7edac970f3cd37c1fe0307e1a9cd5bb52e4167e61149869ce87848e34ad247a8f35b21b0064146a0e11621b0ea69a5b5d8bc7692f82eec5900575ffd12f8c07f791f6c3a6b130425a18176f19229dd83a9baf45871fc49264b279550a41a2c3b47c00f95d50a6d1675568e748099c6707ee3a8f3922a33878028f42705ae42505d99fc6f7c7c9c8eb557549917e4598bd606b3f928bb88f88e87b4be58be008174588eb0a3b38364c0be0197f123705b8d1eb60e0cf4c1a2de1e3872a48fd61ce5411e989dedd0a12309b84ec42b14f9aed065f9f49a52b94050eb82b2d4784d4b2afb14da82ce9e158e57a16f7c10fafb8760efee03f0ab279f86f5a7ae8333376d8c6ea4ad498058d83e10041a6399cb65955d4e01f694ade00b01580abc5626fde283dcd191b1783d948cc99a90f5d07b40803430a049ca74dd3faadcab2e21063fb11dd4bd67e12162df405bbff6dadf827b7f701f1c3d32a06387f22ff4e15a4d8dc7002c19320a4ef95c60805595198a385b983400a99c91cae48380598e69689bb2ac741118605b9e99e097d6538db36c812ef96764d5123a8e07460640068a72b62dda3378dfe1f8cafb0da6d3d7e7e378ad4b179a2ec79f9127fa86d0b2c7f1289a137e47fb9e8ffb7f569b9e09725dd7353a371faefff32297ed35b2aff972fd9f37cd94ffdb9fedb1f29afb860da7d2c3a2db6ebb8dfe72bfa0d620bc1ec4bd121ffce2c3cdb7bce52db06ad52a3a3e97fc3f2f6af6fe6f7fb6c7b2f9bac8b7bf35ea3f9dfd3f6f6ac6fe6f9f4bbb5e71edc5ae716db95d63db73749d6b857d5108d1f4fddfe481f1aed984bf670f1d3ae494cda4b9eeffaeb1ec76795cff3ffcf0c39007f5f6f6d2de8bd46cffb7bfdbfaf5912d97fdee92db96752afe7fc105e7c385175e402591312b169647bef7de7be75d562cbc07f6a10f7d08fee22ffe82c0f24c85ffb7d6efffd9b2ffbb646b45fff7d9964b6ed73ced71d26cce37be6b4c57bbb479db3215fb7fe1ff85ff4fcdff9d19b0b091cb41d20282eb387fe7febe76b653da02372297827dc1ca357623b95c63a439bfebbc8fa74dbe315c812a8b21f802441683f4e9dec7c3c5d376343bf0bb82b86f4ea67cbef9880641344b7b979df3679f7dd8942647a3b999ebe79a832fc86425974f17feef3ee71ba3f0ffd6f27f046e2c5bba98be13082b7a55397389d1961fae130f878dd6c90eea817a5c664a679302631e987505814de618cc258c0137227ef05fd3c0300600c419b098073fd09709188133b0900cfc8e7d753f9695e72a39830ecf09fb40020e5032410cc7a0765a2ed611f51112562c5f0a9d1d6d04e220e0030989eb54237008d4813824f145104f492850cace9dcfc1d1a347b54d62df70925d5136221cbf1cd05f732e5bbe8cf4d4d5d1a9b32895a37755b26df1a245d0dfdf0fcb962d8b1ebc745209c2de853d04d63a78f030cd7bfd29ebe0aa2b5f14f5fb6ff0bffed7c7e1574f6e81d1918948b4360205f1b210e84b23c6687e42e91ec157aa34a15a1a0495485a335db64c63b3b09c5c5b7b5b34dec9f07b6ff85db8ee8dd7c3da356b81927281029df09a94d85e12eb00b534d9e3f69448c6ff8bc904f511e88a8043b5184884ffc6abe3d10db40a1dc79281687b58da13ed924bff5427aad10d58554e106f1a61562acce284f68c99aaf02ffe70ada4cec416d44ab128359d690b33614d446bda56a9c0f0d0302ce85e4037755116cc90b5b0b707068f0c47bc02688bda8e8f4f50c6b2d1b1d1680c0570facd975e05dffcc6bf42e2b602b46193edf2717c234014ae699894c703b67fc90534855e73ca3b45d9b34646ab303276180e1c38025b9efa356cdab411ce3def1c5819ddf446bb54e026041786a0424a403a231bd27e578bc6c44c5ecabc24016ff02f00874747f43285c0683de5836a99a877c4b713ffc25988baf554b66aadafcee6159b00c7225d3e54257133b275813264b67bb35ffd77002ed9499f2266575d7939bce84597c1b7efb83b8e6fd8ae26b98ca754d3e2186bfc2f94e60f230e7f329e7fa863619cb50f30c68671795466a5b290e9f8a6e74af15303beb87d5cbe907587719c01b038a6ee2bd982744c2f19719b636f9d5e2089d3f13ec36d19c4ca7332b2fe09ad5496cb2cc5c8fa5ab46861dd5acc97fd3f4dae9924df756871fd3f35dec79b8aebfffacf8dfa3783f2bcfe5fba74692e2508f161d1f5d75f0fe79e7b2e9591c1076605e54708b8dab061035c71c5155462f0820b2ea83b3fd7fc3f2f4a93310fff3765725dcfa4edbb59db67d9fff3a466efff765b970caee3fc9dfb37f233d71ca6bbff37935cb6d9ccfdffc4134f826613de97d9b2650bfd66c73f9cb265639aabfedfead7ffdbb76f8766135e97e11fc5b582ffdbfa71b5cf7bffc712c9f87ac31bde40402c7cdd7efbedd1fb93d1fda1fd30d709ef933df4d043f0677ff667f0d18f7e94b28415fedf7abfffedb6a69cadbaffcf06ff3f5efbbf79dc9e93ad5f9f5dd97348b3291f0f9f9d17fb7fe1ff85ffbb79965d9d6c87481b2ccda1ed85f02db82943da644cc3f01d7719a6cbc17d41c6e5e82e834a9b8fadb72cc1cc65b03ed9d31cdf35b64969c1cc358fac32f9daa5c9e6e2e91bd794dde7e0693c7d3a7005577bbc46ce688eeb3bef0b342e1d4d9587af8df93dcddf0aff87493cedf10bff6f6dffc7a7da8b16f7aa87dca69f18e31048001210140246382b8a9d0d4bd66a7136149b900703a1f8617e2c8f7e496d939c794bf2b9e85888bcb1bd1e57b21e7466182e83159739e4796af99118c4009a7729484a0a32044c98a513a5a10c8384c1135897f1fa48581bfd28eeece8044453082cab86e7043333de6972aa1f82e130094e2805ecd8f91c0c0d0e8299710940d6952054feab00250b1080b56c318c0c0fc068a0b266f5f474437b5b07953ac3bfae5b8819b9b428e57289b224619625ccc0d4d1de0e7bf6ee83952b96c36f5c71297cf4637f06fffaafdf81ef7cfb2ed8bfef08017992929222d62faf9dca46a4f44abac53681029dc4b6169944b92260edda93e0e5d7bc145efbdad7c085175d081d9d9d3af3d5ff9fbd370190e4a8ce845f6655f57dcf7d49331a492374c2488024c096cc0d36b297cbf61aeffe80975dc0f6820fbcfe177c6164f87f83d73fc6bb2b64af916c6c6464246e6321014202840408dd071a69eeaba7a7efeeaacaf8e37b119115151d9155339ae9aaeece372a5555661c2f5ebc179195f9f5f774ec6966ad425433aff2cb85fe7faac5b03fe901291bebaf89b4631514505a29c6ac08513380ae1717d47c210638d560a5cc754ba50e66248374948a9c06727a769699a0901ab2b3a3cafec869ffe47780b960f3b2b47d5183e9701ccc4e007201c4865493006f417300be26a7a6a9bbbb4b1e9fa3be817e3a7ae42875757572ff5147cccc68dbcf3e8be7bf3c972c183d59f198c684b17fe4b718cf836057a72452a90c15139732c9d8d8047dfffb3f943ebd872ebdf4b974c9732f66dd0d00c7b0ee61dcca6e55edd715691be2e3388fb13fb37b0fcdcecc2835b53bd4ab058050c2761c1c1a7274cdf09734848506d5c53ac61332590c119f06f4c5fab96b83709b8cf4a267d61121eddf4f3ff7bad7d0e76ffd321b87674058cc80428d215ddbd3c6226b7d519da5cb13eb1c2b40940558527316d5eaea35cb306945fa7345a722b4d7167b48ac1bdad0002d5ea38d2e66bfd4e5d2f4832908cca81fa5ebae0150c5661fd1ef608353e127eafa37e332a9286d362da16dc36392e3000031d6c0b895b6ffb74aeceb0bfb7b7efd1ff6b7c590fcfabff9f83fdd9265bfc5b8fe1f1e1ea6bebe3e5a6c3972e408b37622b51dc03eb7defa79faa77ffa47dabb776f9e96f0348961b71a1919e13fcc400a9f2baeb8925ef082e7d3dab56bf9181ef8ba6bc2728cffd6885830c6c5d8ffb3e2df9e575f99509bb6b873638e4527b0ffb74a1af9c462eeff3e3bda92156f59fa84f46a66ff6f8564f9b4fddd27a762ffc76fe15608fe88c8b05e435652fc87fc3d74de3ee6f3e167d386afcc37bff94d5a6cc17e0d60b49156c4ff52ddff2fb8e0024ec587eb4b801b3ffbd99be9eebbefa2fbefbf9f0e1d5ade602cfc31c37bdef35efa8bbff81883b0f2f87ff6f1ef7e172b64ff5faaf11f3d8bfddf6d2f6b5e7cfee1f61bd22d4ba750b92cdd7c6d86fab575cfe33f8fffd0f1a516ffc55000f814c9129f22590b489603b803c972a290aecd04ba7bbc19092d4a6edf3e1d5cbddd31345a34dcfe426d66397aa84df3ddb7b0d9f3e0abefd3afd9c5c367abac45c4374e91b1689972beef21bf6c14b022b00886fcd1d6dda777484fd74f7d8b8b6f1cee31df3cdbede6f19fc7bff9be54e31f8c336bd6acc2c11a2315a9bfb23137880c40a2c6d092a4290bb91e1ea2e3afeb6d3d88f85864e98a07fdc26acfb01be19801429105ca4a75d73af1319c134283530a691d5929652ae23a0c64a9a54624dd8e9d2ab0ea80c422a57c0a38133a8d178ea54c578cc459082ed3035769bf64990debd7315006a0029154e4f15a1d05bbd06c04913a9e54c1ec05009bd26befdefd9ac1cbd092ebba86f94b1f533a25f2814a3f7575f5d0dccc14832000a8eaece864d0cfdcfc24b36a158a25027004609c75ebd6cb63159a9a9ca21ef9b0a6b3a383814260660290e785cf7f2e9d7bf65974f185e7d13ffed3bfd05d777d8fb127a003e2547111d503b0d4e4f0b798c7aaecc6e91b498142d6ad5f432f7bd94fd1353f7f0d5d76d97369483e14046b54141bfe21a4c08b19f413c77ace18fd645ea63f4ed448a74fa27450cc8004f62580ffe45c16a32283b038550bca24cadf4a4595220f3a2b201d6c102bc0965080c26ab5421d25c5263653ad720ac892a69defe9e996f354663015460a862b00b47053094039b05bf5f5f7a54c4d45d90efaed943e562c82516b9efd6decd038b70580d6c44499e6c1ba25fbe2547e7191ed5d94e3e8eceaa2f2eca43564355f510a0c340e576d602bcd92a402517e96638e541639955e30e271542a091dd877886e3ffe0d7e207bf9152fe471a9148e2a75653dc00f69f4a49e9da51a8398f4dba7773d4533333344544b75e94e1ddae9e828d0ea55abea98adea0ad9fe24d47c9bf50636075b58a15092b6ece236124e37493a8e0a6a0d1344d4788bd7664ab8cb382ad0e52fbc8c530e4c8c4f53e2800a614ae3737c8cdb170bae4b845e87236d9b74dd8fe21484c56b30e9758c7588eb80b5666d36ebba01657151d39f29a363d9eedf96aaeea340f5d74189b5ce1b1f4bd2b594b47f14685eb39f01446583ad0a1a2c46b4704f357b090bec208f8d8c0cd57c9756cefeef1bdb62497efd5f3fe73e3d5a213ebb868efbae272162995fff2f9684fc6231affff190e689279ea0c51480acd47eadd897def6b6b7d255575d25f5789cbef5ad6ff183b2fbeebb8f723939019b1504602bb08c75ca6bf773ce3987996e376cd8c0c03b80ae6c5929f1dfac6ea75adc312ec6feef7eb6bfbb6dfbf6639fad42fbac6fdf3b91fdbf95d26efbbfdb87ab83cfce5975ed7227b2ffb7425cbb9eeaf8b7dbf49db7412f8b2960c0c26f4c3b5dfa4a8a7fdf7ae88b1d5b779fde213ddd38b5eb65c5ff9e3d7be8d8b163b4d802e673db175b11ffa1d8134b64ffefefef6750f93bdef19fe83fffe777d0dd77dfcd60badb6ebb8d19b296ab7ce73b7733fbd7bbdef5ae3cfeadbaf6b966e3df3d16ada0fd7fa9c7bfab9f5dc7ed2fd4a66beb90f8fcc21d83ad9bcfdf43fa85e62a542f2b8ef3f8cfe37fb9c67fd13ee8160a75e81bac5bd6b750644d42a89e3b098d161a5f101151c345cbd77fd64296e5f0ee82663b49c826aeb38474f2d50bd9cd6dc31dbbfb39d4c689f4e3fbecce61d6795f7d57423adaba8616675fbb3efd7cc1ebd321b498b8fa85da71756b76c1f28dbb910e6e7ba1b973cbfabefbfacee33f8f7fdfe7c58a7fdca8e73e0c7b8a504c262249d27451a889177fc767c37062a558d307525094b09858708c19aa74fba4db4a9952d0bfdd07ea6b7d0c6b1503ba745d805c0c480020858206fc903e66a728146aa02a755c55014a0c7cc73067b14e515cd73fa5c72dfb924859ba8cfd120dbc21ebd8c68d1b99b588f8afa19385380d510fe252ac49829b98af087af2f19f70aa3341b5ee23333e6db32ac020116c21e88c333653b532cfac5748f176e0c021068bac5dd74fabbbd7d2d4d40c89f29cac5562904fff403f3db56b176ddbba955999ba3a3b744a3cc53484be8686fae817dffc0bf4533f7505fdcb2d5fa5af7ffd9bf4f0838fc9876ae3549eaf30f0cbb8719ac431520c5e989f62a94083fd3db475eb167ae94bafa257bff65574d9f32f635b4500a51531c2249d8d588f3561749793ee2efd96e89ec48273a742380e1402865f15aaaab48a89020981cd286660952a003626809a005604d86e6e760e35185c541555797e56dab68b623927b3d3d30c461a1f9fa0ceae4eaa54aa0c94431a48a484eceb513764634db9041017407118e6c8aa113987533c47b8790bf01c407347468f514f771703e96239f7d073ecd8186ddcb4593ef89c927339cf0c68bd8303b25e858e8f4fd1b66d5b69957c30373936b9d0002a58cc87260c266ae12114e3998ac18a66a94bb4ad14407252eaf9ad6f7d9b1fcabee255afd03e1feb9855af8e0e75792ddd904a00e869b6a7c9c949dabbf7008f0740a6000c92db028870e3a68d4ac5fab30b8ea09d99e9197aec8927e9bb77df4377de750fcf07e670c7b95be9d2cb9e4f2f7cc165b44e3ec854e94fcd5adea48dea0dc66918775efa5cbafdb63b53762f15e76abd890a719a928fd76951eb8b358fa29465303d93ee338aa1cb4e1f68004c057b2f91afaa01939a7dc402b2a6ed5b6b9d01af46b4702f29ea3da06ad66a94b1005f2444c01a94a68a458a492e15c729d0946d9028063f03f2627ded7d4428e645ec0dab57ad5970dd63cb72dfff175b7cd796ee395bb2aecddd76b3da3175daf9fa5f8893591f4e8de4d7ffe1f86fc5bcb871da8aeb7f8073165b70ad820760975e7a297f877e679fbd9db66f3f8b590b20a3a3a3b46fdf3e6606397cf888ae69ff1af0897bee64f7e3467523aabf66102750d72e430dca65b783b9dbbe7d7bfafdbcf3cee33dd1886f7f09ad5b2b25fe5bb92f1a1d167bffcf1a73e81ac5d6d53717a1fdd7a75f33fb76ab64b1f77fdf775fdf8d6c1d8a2d775c59bed328fe175b5cbf5bccfddf9cc31fc100c0bad8a0e4471e7984f73c80638dae46967bfc87fc38147ba1765cdd7ceb6c68af21f2c73fc057478f1ea5c516dc67801fb632fe7d76346dbb122d81fdff8a2baea0cb2fbf9cdef7bef771bc7dfad39fa6bbeeba6b5932af5e7ffdf59c92117fd890c77ffbfcfef77df7f5dd0efbff728bff46b6f4f94ba80d77eceee7501b27d28fefb32fbe42e77df55dc9e33f8f7f5f7beed89642fc177d939265fc9093da4af88ce11ba0af8c7ddcaee37e0ee9e53a45c8986ec0fac6e5d3cb6ed3d5d7170ca1f1ba36cbd2272bb87cbab86d672d3cbe3642fdba8b897dcef7d9946bc6a1ddf3be636e3b3e9d7c7360f7175ab043fafb24a4a36fac3e9fcf0adc28ca5e7c7db1e42beff329b74f5b973cfef3f8cf3adeeef10fb0076e44e1260068d1711c8c39f86ed845f0701dc700b44ac1464e5f26252169d0163f7c8f6a17870ca842ff42a44c3778d05fd04001ae637445fbb69d747b64c6a1bff318a2284d79651ef81b4018da32ac58468fd84ab518d9ed993957034a5f42295ffb6c4b1431c391c29cd4ec8a07237d037d0a4863d8c3004511a44123513ab244c49ca10c0c39ac6b0100a832ed0380cae1ef01f003ff0cfb589187a2006548330286a5b9d9191a191ea24d1b37c8875fb3f286d211796c9eb66d3f9b26c747a913ac66529fb29ceb2d9b36323352a9d8417bf6eea7b3b69da19892a49200168171096c590077fda7b7bf857ee1177e9eeebcf3db74efbd3fa4871e7a84763fb39bd3e2991478600c82aff4f676d3e62d9be9ec73cea2e7ed7c2ebde0f997d1a6cd9b744a38d84bbe4a716ad7585b844d4ea453bec57a661c5f23c590c573172d8c85bae9310d3629291b91997f0083aa8add0d202bd8975302caf182b54b81853ae4f912cdcecdf02800ac125541e5aa02b1e1c1194031d5b24a1938579ea7ce6ec5aa542a45343935c58c4abd7d3d6ab4b26e776737a716049318fe3a12002dc46655335951047064998e8da91b491313530c003b7ce4080df40fd0d8f1719a9ec1dc1f93f311f143d03959bfbbbb8f864646a8fce453ac771d8b98d05e19abf4720a33a552f109b03f1514f0880bea736c63f8bc016e466aee54cc45a99b23f60b51acc193459a9ba9d20feefb31ad5bb78e2ebef822663b220dde2bc0ff350b124078e8d7803a0f1d960f6af71eb0a2026bb45e630cf80b47654c22ede2ba35ab6150793cb17c0590b084e716be0de8e6be7dfbe9965bbf449ffad43fd2534f3d499393d3ccd885bebf2aedb471c317e9e5afb89a7ef997df4c2f7cfe4eb65aa2814eca068e8f9a94a29a494c4063a1d6b548067b57679163e38edbef54a90e498148790d29d4d6a1288aacc7b2c6e7a3fab591d716b5fe3130568eaba0d3f171290398d2a0a68a5e7fcd1a5cb0d2c5469ac5d0003c23aaad5406906a2252586b34c72ca9b535a69a6fe0dd80c4c8d489eac1632444ba0fd4a0987aaf226bdf805f26a9aba63a72dd8202aa22d69082509948aca8fddf777c31c477edd6a8ac919572fddf0a7175cdafffa34c3d4eb7b83672f5588ceb7fa400fcca57be428b2dfbf7efafd3c388d1110fa2c1e0641f733ffbea9deef86fd44ea85e56195b7ceddae77c3abbe5dd7abe185ca9f1df2ac9da435a19ffeede9a55d63e67bf67ed8d8da49932a7539a592f4ec7fe1f8a13b75dfb7c33f1eff3f59389ff564a2bf67ffbfb9a356b171d8005d9bd7b77ca5e68eb636425c47f561cfa7c3e34ff59edd87d85ea99f34f3df5544b18b0f0bb150c58ad8affe5baff9b7300abffd11ffd11a724bcf7de7be9f39fff02bfefd9b39b9683605cd75d771d03b08ce4f1ffecafff7d635c8efbff728f7f5fdf59fe6acf4748675f1ba17edd58b3cff93e9b72a139f0e993553f8fff3cfe975bfc171b75662af93a760deeb693e5683ee5438eea0bda46e5424ee45b947c9391d59f2f60b2246bbc76bf3e09396db3fab9e71ae9175ad05c7deccfcdfa903d96ac85c3edaf91cef6f7d0fcf9f40f8d3bb428dae57c8bb0ddbf3bd6503ff6715f7f8d62ce37465f1b211df2f8cfe37fb9c4ffa07c688db465007930db555c0366180013c055e9837ad35f14d5d8a9e467f310bd6ab34c0951c7a2c5f58ddefa9d1fa6db6c59ba4eac415cf86c000466bc69ea29cb1eb1061e1846155133861a87496f68fa1022eda30e5866e23e56200de8097082ad33ebaa4122007390ee0fa77a7a3a69fdba75ca66f2683102735785cb502dc9235b2816008b19161ae236c6c68ed2f1b171123a8d9d19088310446a409dba3152290f37ac63fb630e67e7e638cd5b7f5f1fa7bc03731225f3ccb814478afdaaca764a38c51bd2d50d0f0df1f90a1f2b524f275895a6a958546036a474db7ac61adafacb3f4f6f7ec3cfd2e8f1493a3e3ec1cc4100ac009c03e6ad6ee9477d7dbd34d83fc0ac4d1da5821a07bfe61478ccf826ec030625034b8b6af31551963fb301881aa0ab9266d0573ca7917ed7fe6752be25156626c3b92a035a04b339e15f312e92194aa532cfa91ec11c06c016c039cc165428f2f4cdcf95a55d2bd4ddd5cd8c54dddd3d0c9e1a94f300b62700bc6047a405c47c8d1d3fce2925c7a40f0c0d0d5232334347a44fac5bbf563ecc3cc840aef9f9b2b46541f6db25f528f31c77943ab94fcce5f109a4a204dbd6714ed7d9dbd3431313c7e5fcf43148aea7af9bea4d1a598017e56b80ee14c18a26e7687eaec2e028b51e28bba906f05e5b57ab1c1742a5e2645b225d299f65963a55ad4873b355fafabf7d9359c32e79eec53516bd28aa319c09c1ec61c5428901618f3ef6143df3cc3ea963a4960ba17c41240ac2c7f1112b26a481fe7e5a3532a419d612d522425aa7d034eeb377ff01fad30f7d943efbcf37b31f339093e3b9c8fdcc4a3d9fdab59baebffe06bae3f66fd1873ff2417ae9d52fa652316680257baa0106a56946954d12d26b9e40fc9bb50ce92a89ce7fce76eaeaeea4e9a90ac575d934a305f362d62bb3d69159b7343b20ef0b667dd4402c03bce235c869a3aa99ac38ada05eabebd6761317667f274a59b1ecb55b271a4d9908ed353ed17da4ebb5d0404b67af4ad779e8a3e3cc80aa8cc49cbe33c9be6e92ef7dfdbdbc97d9c757cafedf48d7d329ee387cd778219bae84ebff56497efd9f7d1367b1a5d5d7ff60ba6d85dc79e79df4bbbffbbbfc7925c67f235ddc76dc180dcdb13b76bb8efdbe52e3bf55e2ebfb74efffcdc4bf29effb1e9a3f9ffebe38b0cb4419fb7fab258fffc8fb79b1c5d53954c657e7d9ecff6ebb3b773e8feebefb2e5a6cb9f9e69b53c0c44a8aff76deff6fbdf5f3fcc76a8b2d975d7659beff7be454c73f406e605d35ccabf86384ef7ef7bbf4ed6fdfc529b171ef74a9ca1d77dc417ff5577f456f7bdbdb18d097c77f7efd9fc7ffcab9ffe7d6cfe33f8fffe51cff4557a1acc67c0b41a3000a39b03d609f92767d5bb2ea8426312b787c460eb5e16bcb375eb7bdd0621b5a28428ee19e77c517fceedc866ce39b47f7589613379a8b2c9db3ce87746ad60e6e1f598b45a33975ebfb163ad72e6e7db76f7bbedcf2a1b187fc21cbc7dc76dd7a79fce7f16fb7b114e3bfa7bb5b3eb81ea0b1b1e3eaa1bc10e90379939e8fdb013841a7f1e3de352b8a79005fd1ac2691d587497fc54000b4a3f54a1fd02b85945e3aad545a5703ae62032e405dddbe6b33d44bf524c79f4cdb963e281b1b3b98baba7d61c611a9747066ceb88c61ec4af58cc995aeae6edab07ead6a83bb1396d22972c4f4a63f46cc9c839ef7ef3fc4801d77b0aa3fa1406191022e8095a9a7b78b864746a89a443c14dc441a66b629e2f48210b072219d1b189a6667e768a0bf8f8e8d1da1bea4878aa512b3111d3c7888568d0c5387fc5e418a41a1189ea6a767a8d4a10047ccf8d4dd411b7bd6d086756b8859a2d80d6295292d52e029352798bf8a65e744b128452a4521de0b0ba0568b2cc6f7a8e637ccd8043f878f68ac0e4055481fc802404dac7c0e8c54008db07d4b45c58896ae0f82aa95aab45d893ae4bfd9d9590659e114d206a21c98ae700c4c5748cf373834a84173d2e6b24d4e2d2875e91fe865bd0606fae49c56e55c089a989ca0dede5e9e1fdc3c02980e7d607e27a666a9bfaf87db850f630e8e1e3b46ddddbdd425fb0620ab4ee0de3c2fa4e6514a5c8ce9aced9b68e3c60d34313e49d333330cec1b3b36ce9fc1a22687cb26e4396730a07ae726752a47c510577366154a1133777df7eeefd319676ea19191557acd11ec4b7184b47331eb0aa530e67bbe772fcdcf253a06abaa2c78a6220dde44671a15b4fdecb3390d6152cff3a5c145007c16e47a55a5bffbd48df4d9cffe0b8d8f4fe9021129178d29a21a281438a5279ffc09fdd99ffdbfb466f5085d7ae97353e0a0d7ad84f11bed6291f232b3466ddeb445ce419fb4e72819a308639c348ed498ead67a5d8ed74f9d26d6b053b9eb3afb32d5fc1cc0338cc830192664ad93a2067a35a90a0d382bd20029d3a6d271e15e4be4bf06b3cfa7e96ca3a8360679ac0ce63d5d273663d7eb6cc1bd4ed3fb84d0eb313e634feaed55fe9eefffad91d0359def3add776de7bb4e36e78dd8f6f1b5ebeba3913f645d63baedbaf5dcb1b48b84fc2aebdc4ab9fe6fc55cd9fedaaaeb7fa4b05bb56ad5a2a7d90103d69e3d7b98a5d5d50bb2dce3df372edfbee1abe38b97ac58b6ebbb3aafb4f86ff59a6cfb2951fbfefe0fe9ebb3bdfb3dcbaf42f55b2579fc67d769a52ce6fe6fcbd6ad5ba91502d62da4dc0528c4969510ffedbaff8301ab15b269d3a67cff6f41fcbffad5afa69ff9999f6106a93d7bf6729ac27beef99ebc0f7ab02540bc672bd0fffcf3cf4f819d79fce7d7ffcdd459a9f16ff76dfb93eb8f59b6f1cda37bccd5c72de38ec1f5d990ce59e7433a356b07b78f3cfec95b2f8fffd6c47f31cbe05983329df91ca799a074150a0d3eabbdd0b990a1b3faf0e9e9b6e7f6e9ebd777ae91a3861cd7eeab911337331edf62e85b647dfd64d9a0517fee78dcbeb202dfb561237f0bcdb9abef89f8405639bb6c56f0fb6c1e6ab7197bb86d85facb5afc1bc5be5d368fff857dfafacde3bf75f1dfd3ab588bc08c22e483ecd8a4f043595590bf039815e3bc0644e1b830edc9cf7850ce4c414284edabf58f448d810ac74c4a2ab46703afb8bc3e1e59bad435492a0517d791bab31e49526757336ea159b0cc8b19bcec76036b118f45d4c66aea93eed716000146e4832e300651a258b00ae49d25352e3d38ee35c1cdb92719d8422ed44cebca80a648e90260d0d0d090629091c70002eaece8a0eeee6e9a989c6496a4bede3e06d7618ec17e343d33477bf6eea3ad5bb7d2d4d4243337e106041ece954a0565cb087ed1c37dc03f7436450522eaece2346f18bf623d130aa8a6d9868465132e572c110038cc0994c024b166d389b4055a2866fef50430964783ae0012633635a92b0056b8298354845c5e1a687e768ed30782a569be5c66db82010b944f00aa015482f9a9cc96a9abbb5bf99bb4cdf4d414db14a90c711cc700e41a1e1996f6452a43a15269ca3e01a0ab96134e29a852006a3621a9e7aa5523343d3dcb730e26344e01a8fda0bfaf9f6d8ff9edebeba1b2f427306fc5a512cd49bd9305eb8650290299118e148b5594d0962d9be8ca175dc9e3878e33d3d30c149b94be8587aa070f1c6130d6f1e3e30c164313957255cd75c63e19a92e69cfee7df4fdefdd4757bff4a71930a558e52aa45489a9bba757ea51a089f171faee77efa1946d4bb7c2ba0aab4d75942ebb6c2703d822aa2eec3b56f37a8fecf7af3ff1499a1a9f62862a454555d52da19502cf59354da317d37df7fd88fed7fffe5bfa8bbff87fa8bfbb946ae21f648dc94dcd9c50712b87b876dd5a4ef179f8c868ba26a5f612fa7f1a94c9e16e40669102d3199d229dce4fe8cfc280a674ff763a410008d147ca50a8dbe235dc0235913e9e32100a91b26045f69aaffb802416f896db366906dd7d00fa14945d63ad438599bb64dfb1b5aed2c2fdda66634c8f1b9f95ef60fcebeeee5ab067afc4fdbf5512ba76cf2abb5cafff5b2dbe38ca3ab712aeff5b192f27bade983aeeb993bdfe2f702ae5222db68c8e8ed277bef31d7ac31bde50777cb9c7bf6f5ca6edacb8f3e9eaceb7dbbecfb7567afcb7720db6c7604b2be3dfd7a65dc63e1f8ab513f1017fb9d6aebf46f2f80f9f5b0c71f7e5d311ffbe7edcba1b376ea4c1c141f9fbf5382da620d5dd934f3e5907c05a19f1bfb06c3becff00c43dfae823d40ab9e4924b523d20f9febf78f10fb628fc61c0962d5be88a2b2ee775e0d65b6fa57beeb987bef9cd6fd1e1c38768a908008437de782303b0f2f8a7057abb65b3fa5b89fbff4a8cff66c6e3f33b57af503f593668d49f3b9e7cffcfe33f8f7f75aee836186a2c74cca7bcddb97b3c2b20dcf2ee00b2162a77708d02c87500fb98bbc09963a100cf0a7cbb8f5050647df78da991ce3efdb3ce6539bd6f4c59c1eb9bdf9093666d2821891a6c56a1206da6cd906ffbf40bcd7da88e7d2c3497a1f1648d2964cf468b78a3c5288f7f5a309e3cfedb3bfe911aadbfbf4f3d3c37ac54b62eb21c83afd086ae67bedbf124a8f6e0dd7c67ff1335e0900d8c12b64d220be810451aa0a1d95e886a2c2f544b7d650d4e0390d443fdc44a5518397d99cf11d5d26441127b0c512db52274303a91c346c3472c1bf1994801970026e1baa1b037a8ab5427e60e62e00ad2ccd5faac4992a8f66c361ae8ba6ab5027b0137373830c0008bc3478e300005809bdede6e0685019885d47725796c7878986f3680ed6a64cd2a59a7cc3723c0a4d4d5153398084c4a5d9d1dfc19e01b65ab020387f0302f8ec0865649014949759e53cac136288f774e3f2734b311202d005fa4e355c7169886165122e551894e1d1889289d77e5974a210624914acb27d857ab2a4d1fa78183bf2554a90a66139b0395b988d816785518d9a56205b603231652161e9b1c93735262d69ef1f171065cb11ed22fc1a2866348e93837374fbdf21de921315f00021574fabbf18971663b338c7598bf394e4339cd802bf45fada8349963636334313943679c79a6029611d5c77aba464464189b90c6102f1c453acabefe7eea1f18e4b9db72c6161e0f4079478f8ed201e9b7fbf6eea523874719f007509a5a538a6c3f1379915e47149026a2071f7898ce3b7f87bc0976069f8bb97c85fbede9e9e51079fcf12765db0728052299f55c699bae35780720ed9c73b7ab9484266e7539d26b0d40879ffec7cfd2d12387e5d74ecd7ca74ac2af01fa52a9052d1f158263f0ae6f7f87f6ee3b40e76e3f83e28c074a9166824b44a419b1140811fd0358d6d5d5512bac9b619dcd31a6235327edf5149f8bec2bd574ed33e9078519a3f66db3ff1bff48eda08f99f3bcb65bf684702a407b3fd28c55649d8fc8da7775bb64af917a5d2f9acfe843afe5108014392d69214a7db06efd8e6ae90bd37eb5de863d8b016cb28d7ee99b608f5b89fb7f2b25746debbb3e095dfb85ead8c74273e9d6b5fbf2e9ea5e93fada0f8d276beeb38eb742f2ebffc8db572be7a795d7ff6bd7aea5b3ce3a8bffc27f3105d7823ffce10fe935af798ddccf7b565cfc37b326b8bab8c743beeb2bef8e67a5c67f563c9c6ef1f9c9e9dcff9b89ff46f1d24c1ddff913d9ff5b2d79fcfbeb2db6d8bab9c78c9c8efddf8d1d802f56af5ebde800ac7dfbf6d1f7bef73dbafcf2cb539ded775b47f37939c47f68ed0fd5b18f85e632341e7102fbff830f3e48ad10dc731b1919c9f77f6a4dfcbb761890f74fdff296b7d06b5ffb5adab56b17fdc33ffc03fff100e27529b0627df39bdf64bdb76edd5a773c8f7fbf1ea1f643e3598efb7f1eff14d43feb9c6f5eb3e63ee493763dd71ea1767c3a3513abae6e5975f3f85fa85b1effed11ffc56614f229e73388dd51c8c15cc33432966f22dcf3be31d8e233a84f1a396556d0d87d850c1e9aa8d0c2eb6bab91fd1a2dde6e1b3ebded63a6bc6f6edd7edcf65cdb85c4e7e421dd43bee44ac8e1dd36ecef6e9fa131658d35e46bae3ea1f9cafa1c9a479f0db262ce3d1fd2238f7ff2ea92c77f7bc67f4f6f2f33b230cb8d7ca85f3069f6f479039a52ac45eac1bb0dc6623d442d751b8ea7ec2aba1d738ccba10ef4a01ab0ca3c60376da21c0001912e8f63052bfd5f9a96509731026098617d41bb64e9c98734d34b7adeb53dfad300a748d727ad2f9fd7750c2880c15ab506b8ccc60d1b18ac2238979bd0a01d353893148c5b89ac3921053c000067d7534f4351fbb45fa05ea148838343d427e7106018a818cb8a6039ea5fdd2be7a3c46c57975c74114d4c4c9824705caeb3b383babb3a68ecf818b35fcdcdce507f7faf667b9a4f5970c07884b20c9490ff95e72bd421cb20fd6115fd81fd2aa932bca4a0e12a71ac068c99c598002e22a10123350b68109b332e115153221a19a8b936188ca4813db50c91007e80edad40150d3863d08b405973acc8e0231c07d318e61ee02780a4782e9258c60e5214263cd072b9c28028804e90beafbbab938aa522cd48bb17b9cebc2c2be4f12eb62dfb4e142bbbcbcf9363e3f266f12a06de4c4d4f717b037d7d3429e7156014a40704080529093bba7ae483cf328d0c8f70aac2d80c2f56ac4c550ddeb16da84052a478c9d8d7134e5909705962989522c51205501e6e1e820dadbf7f403edc5d473b76ec6076af83870ed2e38f3d490ffcf821e95bc76bec490cbe8a53062ab48fde4647c7e807f7fe88ce38e38c5a4ccaf7be5ea4461da6a422e88e3bbe45070e1c50eaeaf961153df3b97acd6a3a6bdb561e03005366ec185e4197393a7a8cbefef56fc88f25f98ad58bc151451eaf5aaf553ffc1e099e47001df7ca787afcd1c7e8dcb3ce503168d6615795141a2688c82e1351078077dd1decebe91a6f1a403a4f468399b52eaa3159613d8ba205602bb33ec57a8d24fb9c8e63c3946562d03066c5600ad3be50b7e7520d98ca6b1d830d45ba8ed731514535b095391aeb798e749f89d58619aebdc60a5103f546fa1cfb09cea5c0cd85fb1eda03d874ddfab5fc807fa5eeffad12d75ef9f57febe6c2886ffcee7923cdcccf72bafe6fd5fcb4fafa1f0f97f0b06fb185d926efb98753119e7df6d9e9f19510ff3e7ddd38f0ed11a1326ef950ece6f1dfe46f88d324bef96c75fcdbf51acd835da791ef8b93dcff1753ecbef3f86fed5c1871ed74aae3dfae17b2dffaf5eb99016bb1057be29d77de49fff13ffec7bafe5752fcfbe2c42de7eae38ba7469f7db162b7353e3e4137dd7413b542703d0410602be3bf99f9598afb7f33f11ff2275c27e38f5477eedcc9e02bc4ea2db7dc22ef43dd41ed2cb80778f7dd77d3d6ad5bf3f86f32fe433a452b64ffcfe39f82e7423ee5d3db3e66cafbe6d6edc76dcfb55d48dcb8c8d23d8fff3cfeddf3be31f8c6e63be76b6b31e33fce0a2c3be8ddcfee605d433533612127b4033124f6407dfdd9bab91232aedbbe3b165bc7acb6ddbabe600e4956f08582c694753f678dd375b6d0c2e3734ab75ca385db9da7d082e1b619b28bdb8f7ddeedd31d87ad57484f9f6ef6e7908ffae225344e57175f1b76793798437dfa7408cd8d79cfe33f8fffe510ffc5624c6bd6a8bfc2621090103a9559a4584f5046339f9807f0dca76aacd69eb1050e93bac924741fb13596c81a23e976eacee9b66c1d238b992b7df0ef9449410a3a7561dd78ed973b977a1ca4fbf1ad01c2b46bfab3748e23033a52cc44676cd99ca6068b5c8888ea8c84a8b184a5f9d8e4fb94fc01bd77df7e524820950e4ee9431a48a6758ab40e8950e0a9b932cd95557abb99d95979536f40b63543c7c78f33400820abd9f979aacaf2f3f3652a15157b1503eee4dc26498566651b7141a59a03800c6c3c607a429a3df45d91e5010802c00898a2f27c953a00cc623056c2cc5ad0adc2692a23062fc90609a01bf60741758c4b3c673cfcfa7fcd8868e29525c627d15bb5a28056caa723950e0d6901cb1a04558899590a85016e0218078029305f21d52003d666e774ca36e2e3009dc15e64c62ebf03c486a953ef8266645b09cf47857538766c9c015a131393343535cd2c4989b43fca4d4c4ef167b092c1ac45305b0100275f485b383a7a8c6d7ef8f0610663019c059d478f1d6390d8eccc2cf57477536757371d3d7a9401608e41942fc7511a1f98eb2e0dc052b119a5d6ad70aa51c5fcc5692785629303fbdbb66d67d18b5f7205bdf2552fa5a1c121b6470ad8d4b610962fe0cb534feee25445183f6c522c46d2af5753577727dbe20b9fff12952be5fa7a64fb53edfbb9e79e431b376d49e7d9f51c9401980b0c63c41c5631fba88281aa1528421a443d2e8e3d1d73d00d203b00cbd4d8d32af5bd44a62ffb84ec8199e212952ab4b3ab7ecf66d3c7545bde2c763eb2d64c215246abc8f2677b3db5e3cbd82511f57663e6289d0e10f369335af1cba475b5f73621527d14c02aa9ed05ba5fd34da55a4dd7eeaa5c5348afa546777ce7b49d162b22807e09cad2c26b15d37e3a06734cbe0068c31e0676b995bcffb7427cd71890fcfabf357392d56f7efddf3a69f5f53f00d378a0d40a79e491473865f14a8b7ff773a81dd3871b8f6e3dfb9cebef3edb86ecb852e2bf15a27e4ab666ffcf8a7fb71ffbbcdba77ddefeecf3f590bd7dfb7fab24b1ae312179fc8b96ce896ffd5eacfddffeded7d747cf7b5e6bf6c4ef7ef7bb74e4c891f4fb4a887fd186fbff912387e989279ea4560858490d002fdfff173ffeb37cc8c8860d1be84d6f7a13fdcddffc0ddd78e3dfd395575ed912d066b3f2a52f7d89539c42f2f8cfafff7d76cce3bf56cffd9c354e9f7ff97475cbb9755c3d6dbddcb872e7ca1d8bdb66c82e6e3ff6f93cfef3f8776de34a3bc57f6c77ee0b7c57215739f798ed08a18e434ed3c8e8a181362a67f7651fb78de91ad9e764590b872f30b202c76dd76dc7b7a0f8eafbf4f4957317509f0ef698ec7af671778e7cc1e38a2fa8438b78966fb93a87bebbedf8ece2f3ef468b9ddd5f68510ad9d32e6bbffbc6eb13b70d7b8cae1fb975dc973b8f79fce7f1bf1ce21f0fd257af5aa5598b6a2440e6a665c13042914a43c6ac4f89626831402df390beaad3faf1313cdc17a23ea5541c2f48a167f4c083fd24603795c92e49411c86cd85c72844cae8829459050d2a7006cbe915f9b8d629f6f999b12bdac4315777d3aea533da4b759165366c5ccf400d94318c3411fec1bec8c9a7412e6cee2861a007fe2f9288262766e40da151b6b6307c39510ddc853942db8906b160e0c3c383d4dddda5d2a1c58a690969edf0175c434323d4d3dd43d333b30c2802ebd2c8c82a3a3636c69591e20d60a262b1930606fae9a8bc21087d2b952a83260c08a7b3a3934aa54e067100c40516a7aeee2e5226120cc262d3a6a9e68853c9b18205058853608f985a2d6c76ad26b3e7943ae458cb3c47653d66142a757630502961f05427bfc35e188b013f01648232001e01e804562c80b900da02bb958955f81afe3a8d744a3b80e0060607187052e5748d481fd9cf3a619e01ee1993375100c803980773d321e70020acc3878fb00eb87937393129fbe9a0217943a8582ca9349f722c13130acc856182cd0cec670074c1a701c09a9d5948a71e4594eacaef71a458bf92aad25ba7f42b14e12f1ddcafd04e2838a60c28ab4abdbdfd74de7376d01557be40eaadfc40c57d426e68c2229393d3f201ed5e05ae641f275abd7a84fdeaf1c71fa7c7e48b84df776aa906230636a1dfdebe5edda7676d17510a128c34679b9aa7587f26a527336289340ed11ee62b62b01e35219eeb27fdce4b00c8f8341b1d691d90f79041b0268eb096c6b57dc85e1fb9a2b51ef118741f286fd6e244a7272ce8f48642afdb608d22fd9d814f1a30a59a8b14bba0eeca4e2b2b34d36155afa189eeabca7d48bfd760d958af99464ffe1cabf4a6a63d6313b3b6324b56a1900276d314b1495257a7ceca71cca907f157abb6ad56d2fedf4ac9afffc3d7ffed26f9f57feba41daeff9ffffce7532b047f8c0106819516ffbef908ed19eecb15d76eae4f34232b29fe5b25eeb85cbf31c7ecb28b15ff59dfdd767c36f4f9f789ecff44ad9f973cfe5b7f8d626cd98afddf5da75ef082d6ed895ffef2975754fcbbbabbebb52f0e4d59fbdd375e9f34b3ffdf76db6db47bf733b4d8827b4a575d7515b396b75bfcbb7dd9c797cafedf6cfc876ce7fa26fed8f2eaabafa21b6fbc913efad18fd2ce9d97523bca5d77ddc52c5879fce7d7ffbe36f2f83fb9f877c7ea1e0bf981eb37beb2211bd9fa9a636e0cb971ebd677750e7dcfe3bfbe4e1effed1bffb17dd2e7483ee7f619d755daedc85d644283260a3b5428f04313ee5b10b2dab4dbb5c7e95b147dc1e68e31ab2dbb9ead87cf0eae1d7d76f02d94eeb9ac00f3cd7d482fb79edb86cf37dcfe5d9d6df105a25b37b418f9f436f57db68a3c8ba87bced6c96d2fcb97ecbe5c1ff1f5e9f30f779cbef970370f9fffd8fafae2c4d5278fff3cfe437ab9f5dc365a15ff78c8bd76ed1a6630520088b82ee5153e09f5850136152b9595ee44f563de6d1df467f3909e75b4c66cb3bbd8cc568558a75e3336c7395d27b63e9b948742b7c3ed313b8f708d93b25471df44750ff4cd31d2e350bc3e94f64bc27f91c6ec5182d2f488c54291d6ad5babf48c151b12832c225d876a29ba6c1563527a81fd68f4e8114b71d57f6a7fcb768a052ba1e19161be89234485d6cb795cb37a35cdcdcd113ac53bf44167cc2e246d3336768c69cf01009a97e73b4b25ad6fac40418949f7483c3eb4333d33c36016007770ac4bde3c2acf9799b92922c5ea83f479e803e01c5c22195006e97946e6e6c6977ea75f14d0af4a15a1004360568287e3380047180f03af2a890686a8b9033318ecc76c5751813ae4386ba938156314e2060028d8687a6a26659003200be7199047ca77c68e8df1e7c1a1019e7780e8e6e666a943a7f6038b59a7bce983b6e6659fb03f00490372ae314700620d0c0db29f953a8a343535c5e77a7b7bf9bd22f5efebed633fefede9a699e9495abf6e0d1d1d1d53ec5eee5aa6d3c3f178f05dea8bbf102e16e4380b2562879327aae5b27a97e354409944b7153100adca40ac2aa765bcf8920b1810954449da5ffd5fa30b06c3cd97abb467f71e1e036c06b0d99ab5eb186076d34d373383179918a55a0c18d0a85a538853d0bd50de50c7cd4d9c4a12671fd371b34ec6c9c0c030c7a7cd3a4586018b542a4f6007915e3386feb1026d0dcbf9dab07163a62f47494486ed8e2c1e2afc03a31cc68b588a52605bcd37cd1ac9edf07a44291089440df45ab7d69bbd4b19479db3da1106c0a4db149ae950e8cf58fbcd5a6aeb90ee4bfa33afcb9ab58a8fc56addc0b182065e25fa5caab38911ccbbeed7ac37865111ac57c23acea90c19ac1ba580ac90287f29f09cc63648d6d87085ecffad12df35a991fcfabf3592351fe658c8ef56c2f57faba555d7ff006d634f6f85e0c110ae475762fcfbfaf18dc5163b1edc1872ebdb7d66d97725c6ff624bc8df6c6955fcbb71213cd7336e5f6e2cbae57c3ee9ea542b4f2d913cfefd3ab732567cf3b058fbbfddc7739ffb5cfe83b556c8cd37df4cbb77ef5e31f16fc793afbcabb7bb47f8faf4f9873bce50fc1f3f7e9c3ef1894f502b043eb763c78e96c7bf6f5d5b4efb7fa3f877c7e8f33dbb1ddc5b7ad5ab5e45d75fff49faaddffa6d797f751db593e01afbf6dbefe0cf79fce7d7ff79fcaf8cfb7fae0ebeba79fce7f16fd75baaf19fc980150a52b7d3d060ed8edcc9f22d443e85dd32599f437a64398a2fb07d41e05b9cecf6420e69b79d155cee185c69664cee67577f5f7d9f4e76b066f59ba55fd6024f193a98e3ee42e593468b85ddbe6b2f5f3faefebe85a9197f71cf672d2eeefcf87c212b5642fa8562c4b748e6f19fc7bf5d76a9c63f1e9eaf5db7863ae50d011c633614cd708597d09fabfa217ec1a427343a5b3634cc2586f9242de3a400c0f1c8d6d1d83a51e9dbf8217ea41ef89b32fc709d3480433fe417fa417d1d00c06a5be831445adfaa0196396083d496e80b2f215256add4ae0c6088ea98b0b87f14e71752d3156860608062003b440df425925817d2fa904e9318090b9015d3e4d414bf22a052226529d337ecc27535e30fb3e8c8126bd7aca1f939a4c62bd0f4f42c8378d0c6f8c47166573a74e8100d0f0f317b5359b6d1d5d5c30c49807d019f0296a539597f6e7646b333cd33600800a072b9cc6d30e0aaacd2b2e1189b4a2a305f466a3e69ff5281ad9ec83194c1ea94a8792a3082c5ccba4e59172fdc53ea1d542c78713a38eba5e1712726668ee1cb5247e4448c38e5a0f221800be16133d20e050052180458a0593c34d4be131522ed17c463c7f83846191ca5987dc09004600f98c35011e0968ab41900561080d60a9cae4fcec77c85816c3d3dddcc20d5dddd4d53006e494d4647c7a42e2ab5215c070037956ab2ca8c569807dc6c999999a5bdfb0e506fdf004dcdcc702abf6a755ede282acab215e993b26dd9407fff203dfaf8530cd69b923e92441a1888b003184d4f94010d02ec857493ca7f15588de394e3ac964690fd9341658a49a91017558cca76319e0b2f7c0e03c4d0870a2f13732a2d9ed02e7060ff219d1a31627f1b1e1ca25d4f3d4ddfb9fb7b24aa60d3533e96fee33543b7a7ffad5dbb8ace39f76c7836eb5a00b84ae015f1cbf4bb7af51abaf8e20b6411c5d805a016c714b344a9f49fc475188faae3548d7dd3a64db4fdac6d646524556b9eee034c5d6a4d8cf9b3f25fd947acfc1f6644da4800f92212b5b497427d8e35f048c57d6d5d326b55d53055d9d74400fa315852ad2f895e830d00d2281be9cf910d7e222b0e8548c1a486bd8f815bd6fa69c0b4a853d5cc59e91a2a441a9955b38f500df82af49a58d0296d4dec993a91ee43a47b8f48d779c3aa28ccf822c5c488b6fbfafa19681ac7b113f22b67ff6f07c9afff45cbe7a5996bfefcfabf3d64b1afffb76cd9d2b20746b866c15fe6435652fcdb7abafd64ad176edc846c66bffbf6b1469f437a2c87f86f85a89f8f8bbfff3713ff76b9901dedf6ddb9f1f5e3ea9fb5ffb76a8ef2f8cf8eff56884fdfc5dcff4d5ff83d7cf9e597532b64d7ae5df4d0430fa7df977bfc67ed17eef9c5d8ff1f7cf021bedfd50ac11f8b5d74d145f9fedfe2f8773fbbfafbeae33cfe98e1bdef7d0f7dfce31fa79ffff99fa776927beef95e9a86d0953cfe29a8df4addfff3f85ffaf7ff42b1e64a1eff14d42f8fff85e3b3c7d02ef1bf20174a6840be4972df7d81e22aed3ab53b18db08a1600d0ddad74e28785d1d5d7d5d83bb8b936f4c8d9cc1e784a180f2d577dbf0cd8b5b36cbf17de5b3e6c7eea759bd7c62dbd2eed737c7eef7acfe43630bb56f8f336b3cf6b12c7f73c7658e997711589ced72a1fe7c36b0fb109e853c3497bef872f5f5b56d9fcfe33f8f7fb7ae4f2f9f9caef887207dd3d0d0000ad4da707461e055bc300d9829c5a0167c1742039314200b0fdb6d1613d64b756e0f8e5f9cb630d27368e96bb416a69e63e734359769c7a4fe8b3c1bbaa5735dff5a62a580aa6bc698a62f4c144005a0039dda0b65196c20dfbbe54d95a1a1412ac68a374cb5e19b53ad4ba213100a556e62628666a666eacd8232a25e577c6570431c31e311d2df95e767e9c8912374e4e831eaebeda76a02605417f5f4f631c312ca031484b180610975c034d4d7d743478f1ea15247891988468687e58ff7e3345f2e33304528aa2dee17c018a4109b9999614a6efc3518c6097058c436d63ea226588136aa1595f28f8466c769bcb650ca1c147a3516f4125b2fee170c55529f02a7098cb8298c036c579c5630a9320355c460320504447a338c1d6024d335d8b0c09605501ac60e7be05895d992229a9f2b733a43660d9376061bd57c593386c9f373e5f95449e8897676efde47d333730c969a940f2ba7a6a6e5cdc12a03b1f03e363646fbf7efe336302200e78aa54eeaeaeee3d47b070e1ce0baf09902eb3047285a92730c401c4079604bdbb7772f4d8c4f6a00a2f26722c346a7018af2534f5717a7a7649628692fa41e5469040d1352319d8b288a53b010c2c4d80e48b50d1b36d0ead5ab18b4a6d8c7546c989934e09bc9c9491a1f9f906d97650cf5cbfe3ae80b9fff123df4e0a352ff028f59c57094d64ed70a21e7b418d1f9e79fc7e0a86241a7d7e3c60df8314a8144f8ebd3b7beed3fd0fa0d6b99e90a0e0a001cb22d622c09457a2951eb8901020ef6f7d22ffffb37d3ea756b94f6b62beb3e18889528d0972910c51a3428db96aec0733a25c71be9b8121a8865e286537562ed889dbd47af4948036ad69ed818ddda17ecb4b0f6da1f51fd1e6aca719948a738d436aadbcb74dd82591b35183672590aad7a36db60440bf740216a40ddba7d4db751b5530e6a9dc82a437a6f819fc1bfe0dba16bb495b0ffb783e4d7fff56db7425c9d8d3ef9f57feb24e4afadb8fec75fefb742c03671dd75d7c93d7e3cd5dfd67339c6bfaba7dd8faf0fbbbe2f567ce55ddd4331b192e37f31c5d7f562ecffcdc6bfdb7fc856a1f6cd395fdda8e1fedfba75388fffecf86f952cf6feefce0bee9f6cdbb68d5a21f803aa5b6fbd65815e3e591ef1bfb0ac3b2e73ccbc9faefd7f6cec38a745c675492be4677ff6e7e47d8ea17cff6f71fcbb125af74372e59557d29ffff99fd3effffeeff31f38b4831c3e7c98c19d79fce7d7ffae1df2f8cf9ffff9740f8d2b8fff7a7be4f15faf93dbfe62c47f6c577627d0272163926720eeb966da3c115dcc00dd31f89cdeae63b7e90b822cdd7d4110d2b39900f3e9edab1fb291ababdbb6cf0ebeef769baea3f8dacd726c7703f12d3ea13ae6b36f51b1f57503c46dcb1754be79cd5ac8b3fcdaf73d6bae426dbbe20b669f1f35b291db1f11652ee679fce7f1bf1ce2bfbfbf8f6f44a1373b351f19262bab2e33b010a5286493b62c6539d1c7cd03f4c44a67684058894e4fc7c734588a22ebe22052ec2689c58662da3163acdadf3500ca665a31e7c8e824443d18c0f61b4b6f5b12534e974dd37f69305aac59b71416236226a391e141d5a4ed2b4d808600ba387e7c8ce6cb156f0c2eac400c881a1cec636015e66560a09fd68309a650e0f954362ed0aa55aba954eae0875f28373a7a8c01276025989faf302b02ec823ae313e334343cc0ac4dc562813a3a3b38651d005ad5aa62c001ab13eb98a87941db001555aa1569938262e9e2f46d09a7deabf93f2d9ab871b4600e2c1f330c56005001d451ae96b58f556bfe490a4c6698aef09793180fca60fc1837c058d3d3330c5032c02c032a840d50178c55a562896f005619b49550a7b4f1baf5eb783e262727d8d6bd7d7db28d32b3328165087d7474749182dc08fe0ed0d7d4d424b7d7dbd3c54c559b366fa6aeee6e068001ac8532d333d3cce4b567f76e9ec3f25c39f559059111696c1ae0ced0c8a0ecafc4e798010be824a13e5700564aaa1a68a9e637d10c5968b7182b8623e80eff61d60d6d6f06173982e373b3f3d2ffe5c3d9a883366edc44fbf61fa47ff9975bd99e0c04d440b12816a91e6a4d10ccd8d629757dd18b2e973edcc3baa939f63a06d779ee2517d1affcca9ba9b3bb44c0e229e060a258b984d4156b02406391eaa3283fbfe6b5afa66b5ef7b30c42a4460f9334100bef499ada50703c1c3e749801714435b092c207268a518a59fe04db52c5995a2b99558f99d96afba959a384e5f73e89d2b5aab6a625f63a29440d7c4a94824a0de856e83e580fb608d5805eba5dc39c186996ad88c87b5d01bf4fdb41795346b72384a8db2b78add560ab48afbf266d215e60eb1b1850a9b556eafedf4ac9afff1bcfef624b7efd1ff6a9c59646fe6b9f3bddd7ff175e78215fe7b542c08075fffdf7afb8f877f715b7ddac73cdaefdcdeab292e2bf55e21b93efb8afcce98eff904fb9b60c5d2bf9fc2354ce6e4b9da3964a1eff0be3bfd5b2d8fbbfebabf843aa8b2fbe58ff31d9e2cbd7bef6357ae289275644fcfbbe67cd55a86d574e66ff7ff8e187e9f39fbf955a21f0b92baeb8bc4e37a27cff0fb5ed9ef7e9edabdf4cfcfbdaf6d9c1f7ddbce30ffadef5ae77d1f5d75f2fe7f54a6ab5e08f3e1f7df4d1ba6379fce7fbbf2997c7ffa98d7fd79742ed86e6c41d973b47cdf84023dfcee33f8f7f536ea9c77fec33a8ad80af4377b0ae32590e1b724eb75fdbb9dc73cd0c3c6bc17125e4a4be72ee84bb9f7df6b4dbf5f56b2f26ee1cb87dba125a4c7c73190a569f1db3ece59e0fd9c4f69f4681e8f3a34601ebf3bfd062172a63dbc56d3f14886efd90b8f60ef98ddd87ad43965db3fcd3fdec3b6feb677f27cae3df3de6b693c77ffbc63f1e5eaf5bbbbac65a82b2382f142b9579f88d1a716cd2e7e99729a7eb9987fa050b90859e13bbcf284a534ba16e8d61aa5626b6fa300ff9f5e69ba6db4af5b540323c369da290accf7c5e8fdd8ca5ce3e3e9b89fa8b614166b896df72ffaa767757070d0d0fa7c75905eb5f4884b609588c948e3a056486c022b0037efc27d50a0d0f0dd3d0e000ad5e3d227f804f33eb150030dddd9d347a6c94813d1d9ddd9ca2b0afbf5fa53094af8e529153e91d3c78886f0ce1552957b987d9d939061a199bf17c2682cbcccdcf694086022199f460420326980d8b629d86501d2f7818d44e9b88fa57a4dfc17ec56c559c5251a4e9ec120d162b97e7f99c01d899b8419db9b9794e19087015a716aca8f46bb025403f459db20fe9fb6098aeae6e069bc05ec66f70a317f30bb01c6c06a00b6e98a00da4af04580e8037b093158b256673025807a912d6ac59cd4c67e8bb5a29d3f4f41c83af5076f5eab5724ecaf4ccd34f33900b7501f8aa700ac90e1ae8ef6336b4871f7e9c2a60abe3f9472cc79667d6e277d5aa55ec5b063005701d8f15003bd27f89a053c961ecaa1c406b156ec7b062754a9f5bb56a245d0f7c7186fa681f80b5bebe3e1943abe9d65b3f4f8f3dfa18298097d28d59a4848e2515e0291b1680872f7ef18bf49a6118b93c8c7d8c754a64acf4d27f7ac75be9e7af790df5f6233da49ca742ac9d8574ca3d8c4b48dbf5d235d7bc8efee08ffe3b6ddab45e9eae348c4fab47ed8f580b0bacd7933fd945931353948ec0b087e9f4836a0891067c1634f318f1d80ce0d5f46ed667b3bef17aead3c2acd566cdd23e8f7612e73ac1663ab4d7d5b46f675d2c32c830d27369a5afad57206d5b58ed993118dd85b34f11d5afb7694cea72004cae5bb7866367a5efffad94fcfa7fe1fcb54af2eb7fd1d6b1613eb7e2fa7ffbf6edcc78db0a01e3c7273ff94979dd321d2cb39ce2dfd796aba34f37b75efefb7f61bf8de2bf55d2686fb33fb722fe7dfee78b91904fdabeedf32bd7e7423ebf98128a47481eff8b2feef5f762efff6effaf7ce52b5bb627622f047003f7008c6ecb35fe1be968973fddfbff5ffdd5c75bc67eb579f366be0e3392efffad8d7fb76d5f1f3e7bd9ef90f3cf3f9f3ef6b18f312b562b05d7d98f3cf2489a950292c77fbeffdb9ff3f83ff5f19f652ff77c969fda2f733edffff3f8f79d5f89f11f9b0f2167b41bb21570cbda6542c7b21ccfd7af3b113ea3f802cd1e8fbb00d8e3f04996a3b9fd86ead8f5dc32ae03f916df501b6e3bee7c342aeb1ef7cd955b3fab9ebb28b863f2e9d248dc4d282bc07d7eeb5b147c7a64059d2f981a89adabcfeef6fc84dacdf273b70d22ff62d18cce3e3fc9e3bf7eacae6df2f85f1af1dfdddd2d6f0a6caaefdbd2c166a232c7b85dbceb3124565f89d5b7497185522990c0b17164fa8bea2fbc529d8d9d4c1fba2e699deaec84cffa21bf01d17019ad2fd78b6a40903a7f11224dc365f433e5c9eedbb24b6419abbbbb97fafa7a5589281da66ac7078be0629106b4101d3a7c944435494141da24b5948d5194ce0bceaf59b346deb89b61a6228058a6a667687e6e9e530c02c82290fe4f96ed979f7bfb06186082b27dbdbd746cecb82c33405333b3b28d7939ff1b19b80370cfecdc2ca7ccc358c063d3ddd5cda02358b1582a32a8076025008b009001c8084020bc04a72414cce443a458a4549a45414dbaf3a99148d481dfaa89021d298008e62661fde701b8d2ac6e6a4a543c608c9c9a50960710adb34bd994d9bfe4bf0aa7188c99f90960b498515882a66766181402f6b032d238caf3b02b44d92da2e363e39cd2104c46f03730521c3e7294cf8f1f3fc6a92c7163164c586c32596672729c66a6a752e6b875ebd6cb3213cc3085e89b989a627016e60c7a8e1f1f23e4e09b9165ca523f306f814deabbdfbd87545a3e4ad301d67c53318241ffc181013ece2924a388e7dbc4a84a475a48e3ce00761458aac06b013366696f2d751499a58ad2585211afa3926d877e4c3ab9a77ef2147de69ffe992acc08a5f45155f5ba6dd083b190e7142870e76597d0a62d9bd378622093037ae4f8071f554c9c7a70f3c6b5f4df7effbdf4d6b7fd0a6d3963bd9cf70e42f6c258b6dbd111d3e0602f9d77deb9f46bffe96df4077ff0fbb46ecd901c7b85d9b0d2b86fe4d35c264a7dab2c7deaa9a77651655eb1c5096d09d396301fb1bee04c64d6dfa49642906a6be10230a9bd5ed96bbd5ec363fd998b6a463fa1d73c7bcd05288a99dcdc39a6daba6a74a85a65105344d6dea5f783c8daffecb6d2bd447f8e9d3db9a0c15d76ba57c409afe1e84f9edfb4791db3019a7e57dafedf6cb9d321ee75477efd5f3bdf2ac9afffc331dd0a71e7a395d7ff3b76eca0ad5bb752ab042c58f7dd771f7f5e09f1efee37befd23eb58ddef20c76f7cfdbaf19cc7ff624b7eff2f43536ab5e4f14f99ebe56248969f9deefddf9d0fbc060707e9852f7c21b54ac082f5e4934fae80f85fd8aeabef62ecfff635482b64e7ce9d9caeae1de27f25ecff8de23fabac7bdc3757b6e03efab5d75e2be7f8526aa51c397244dd27a13cfe5dff31f5dc6359b25cf7ff3cfe97fefdbf7cffcfe3dfd673b9c77fd157a1d981669d0b05bdeb84eee49b733e473b9180b0db6eb68cab937bce75245ff966169a6616565b9f902386022924be71fac6dbc87942657d4195d57756f0851695460ba7af8e7dac193f6d143876b9504c345ad45c71dbf3f95aa398f3b5e5f39790bdf3f8cfe37f39c4ffc64d1b189c03961fd6d1ae2b448a3540ed343d943d6ff81c4529984a08919677f58cf5837da1eb98fa0cb4421b7a6c26c595493748ba6d330ef3e0bfae5ff9b9e0cea76ed3f6327c067f0fda803e06d862ca439f44b817162acd5a1c295017db00f50a0a50d0d3d3433ddd3d7c8ec128a4d2a6a1676e3bd2f3666c69e657dfb41f3d324ad52a6c122bf048ea1ba94aa9bd507ecbe68d0caa0173107e6c23eddcc4e434b7bb6efd7a9a9b4de8d0a183b466ed7a794ca58d0360aba7ab5bde741ca6f9b9595ab37a8d3c3ecf2023007e00bc1a92372427272759efb818532551ec471da50e66c4eae8e8e0ef9c2e4da874787a2024587f9d1e92ed4e0c86514c6427b616d5a62fece7d91589c139a80fb05124d45c1434380aa02c00a538d59ab4b90167cdc93116187084547009154b059a453a41f8891c737767b7820f691f064b11699a2d308a1d1b1da5be817e2a7616686a6a9ad9a89072b0bb47d61365c6e3202d20a703d4a02c66ba9a9ce43e8e8f8db19d91d2726a728a7544fac809a953774f1f8d1d0778aeca40b1e9a929f64bfc5dc0a6cd5be49ccf481f9864a0dc943c079fac54e679bc005821fd240058b56850002805c8c21e2218d4353c322ceda4521056799e1366e92a32ab564229182aaa01eb840674d5e65a793aaf2789d0c01fb54f251c4a2af6103d7807e316c0829ffbdcadf4f0238f813f4dda2a4e530d6acfd7c9fc54a70076c1bfdef88637d0aae1610d0c329e962c7089588f3d56247a74f6f66df4bedff90dbae28a17d2fdf73f400f3cf810a7df5cbb762d9d7bee39b4f37917d345179dcf8c59688f5b5781d8d00fd96fe173da7d914a11ec743ff8c18fd8e778d9903e403a6d27c09775c1aed74705be9276c029733c05bda9149506d464526626cac0b5f51a76b1d65ca1262ced8bd753cc33c60546bb4801e68a9afdb060a7a3c54b3321261abc28743b8214ef981a9f62ab024093ac5486e65a85818f362b9eb5671b20a0a8b3a548df0d70ac28d7ac2d9bcf208aac72b4b2f6ff13bdc63bd5d268ec6eb946d7e03e7b66f5dbaed7ffad94fcfa7fa11fb4728edae5fa1f00d997bef4a5f4ad6f7d8b5a21b826b9f3ce3be9c52f7ef1b28fffacfe7ce77c3ad93ab8bab86dd8faadf4f86f95a84baa6851f7ff5adf27fffbdfad631f6bc64f7dfdb57a2e6cf1c5751effad95c5deffedf36ebb6f7bdbdbe4efcecf512b64fffefdcc0cf9d18f7e7459c77fb331e8ae91213999fd1f4c6337ddf4cf2d63bf82204d9d6242cff7ff7689ff90f8c6e91bafddced9679f4d1ffbd847e98d6f7ca3bcff7a885a21f7de7b2fdf33c41fe9e6f11f1e8bafad95bcffe7f15f2f2713ffae6e3e5bb8edf9fc286463a27cfff7b597c7ffca88ffa2dd714809fb5ca8f3d0c261ca849408057c336db8bafbbebb6369d641ecf18626c36e3bd47f686ca1052c14783ebd7ce3b0c762eb13b2af4f4273eb9b97661718dfd8dd73ae9eeedcbb81e9960bf5172aeb9bcfd05c67f9b07b2e6ba1b3cb34aaefb6e56bdbd62d6b8e437e9dd55f1eff79fc2fc5f8070352a9a38381385ca7beb0ea9f6a8c56e929fc4f3f9c4f7419bccc03f44483a762eb01be56d01e08bff1c37cad6fa48f2b369d44a54114f5178d91b18f66635169e4a205a434c21a6f62d54d74ba3c1768c5e7007a88746a43a15b01282151cc34065c40cc2c43cc36b571e37a4e4377a2c2f35b4de8e8d123a4d20f46aa2f1fbd0e3ad3630110aabbbb87cae50a03c8caf3f3720e3bb9bd4a05e9df4ad4d93540cfec7e9a568fac6696a1e1e161664b5ab7762d038046478f3023567777976c4330831658947ae431f802001886d9068025807300a601a0074c5b00d1542a65ee132022b617ab5f48e71bc027b0fd2ce61f40b34d6113a96781d97c8a2a559e661b43cabb829dfa2c51ac569c4230563e342fc7552a76705b006d0140133158af4a1393533afd6392fafe1c406ca522fb00a72a4c14c08a8179a4ca203d21d20fe23cb365c9f6c07c06bb636ecae8b3d4c12c64488708600e00607dfd037cd36ef4d871eef7c891a3accf7c798e012fbd72ce7ef2939fb02f60dea3b824fbeda3bd7b9ea291e121eaede9a57ffacccdca5785b326885a2cc01ebdbd3d34b26a88947b0b2ec0a9073945a302e361ae750642cdd0a4634c1886a348fb44c174a14328e2fe35ad9bf267790ccc6223ab86e9d1471fa35b6ffd22552b0903b0949ec2b8bc4a21186936273067497d769cb783ae7ee9d514812d291d92f03905fb226617c04030b3a18da1a15e7af5ab5e4aaf78c5cfd0c4f804c74197b461774f979c7f394e6e5481a0220d284b1aff66309da6318b7f8f3ff1143d0a701998e312b5a6b0b719962c6d9ab4796d6b9b6d0fe762331e1c8a6a2c5394aeff5483a9a5eb9448d90455190d20d46bb30118422d15be353014d93a450a142b12034855402c32fb9eddbee95f8b01bb160c3b21d5f638f61aed43e91e6dd666b377520d008c7360beda88b4902b78ff6fb584aef5f2ebffd6883bfed0f9d0775377b95dff37136fa753dae5faff924b2e61964c00be5b21d75d771dfde22ffe226ddbb62d3db65ce3dfb70fb87b84dd4656ecbaf542bae7f1dfba38375d2fe6fe7fa2f1efd6f1f5172aeb9bcfa8e9fdbf75fba2eb3f79fc2ffcdc2a391df1ef9b4bf7bc2d480b0776c8471f7d945a219ffdec67e9977ee997e8f9cf7f7eddf1e515ffadbffe7ff0c187e8965b5a03b483e0decdbffb77bf4046af7cff6f8ff877d76c57af46fbb5ebbf00617de4231fa1dff99ddfa1c3870fd362cbbe7dfbe4fde4a30cc0cae33fbffe0fd9caf7dd1d4b1effed7dffaf91efe7f19fc77fc856beefee58da2dfe63b7b0db906f803e07b315721d362b405c71957583cf7e85dacc5abc5cc385c6e9d3c7aedfec42901568be36edf2be05da7ecfd2c7b5916b17b7ac4f7c8b8f3bcfee18ddfedd7164f51f1aafaf6f5707dfb1466371fbf02dba5931111a876b2fdfc2e5b69335d78d160777ae7d7af97472258f7fcad4c7ae9fc77ffbc6ff19676ca181fe7ec5208336757dc342c2c094627161ca2bd437202bad03039aacb613bb4f534729afce272ad59b3966da48dbf1f8aeb0da2868e08869cb9e37d36faa837e19b049cd266a1cccdec3632da5e35280a2985215acb1b1bd400d240b6ddab451da2ba6131560ae90c60e7f9597906109720a45ea98390e400cc056cc1cd4ddc500a9a1a1219e2f058c8aa9bbbb8f66a7a768f3a6cd542a15687010209e0e66e99a9b9b638009005c1d1d45b6e5d8b1630c163a76f418034f304e954eafc4760548a85aa9f27839a51fd48a35bb944ec707f011602149550175009cb332ac2d9a08661e8ad82eb1064d41c02c05f44d77570f03b2cc9830c7003881390ae01880d98ada0678470abf796933a181855d5d1dcc1484f966c62c3d0f9c3650247c1ca9f7906a705cce2bfc7772629266a667e9d0c1c334213f03008779e8ebed61db757575727f1313c7190c363535c300328ca5529e976567e46b96c12e4a37e92d952abb23fa1f1c1864062ec0738e8f4fd1e14307f9865faf9cd359d9cfee67f6a8704a6192daffe58b635b28df1b1e19a0dede7eedf3f67eabfc8e5991448d5d4aa5a48b18d416174a3a3da14a67085cd1ccf40c03b9c0569568e6306e3522c52426edd8d9d1c1fd7df6b3b7d0d123a39a554c03747419c3b0c560a34885218067ffeedfbd8e3aa5eda2864e8112b576394d25fc34a9c81848a8bb33a2b56b8769c3ba611a1aeca6ce22d8f4d097ca3b8ed08e75fa434e7168bd164ac4ac646c3f1173df55a9ffedb77f93ff0ad7d816ef0a4095d4d5b50306b64bd73352eb2139d741913e66d64df85facd3f791b5b6c67abd340028a1cf259a01d0a4f563509549551879ae93581f0578e575da8c27aa3117f277aaa52f346b73a4db4953199a3d4cb7cb403aac1bfa7c6ca54a34f54c7bab568fd0fa756b57f4fedf2ab1f5caafffdb674ef2ebffecf86f95b4cbf5ffb9e79e4b2f7bd9cba855826b9ef7bffffd7cbdbb12e2dfee2b4b5f5f5b59fdf9cae6f11fb53cd6177bff3fd1f8cf3ae6f6e13b17f2f92c3fcef2bfc5923cfea3a6da5b0cc95a434ef7feef5b8f464646e8eaabafa65609fef0e57ffecfffc9ac35cb35feed77f7fc62ecff7bf7eea58f7ef4cff9faa35582f483b827036987f877fb5f2efbff89c6bfdd965dcee783bebaaeee78613db9e69a6ba81582f5e4e9a79fcee33fcaaffff3f86f4dfcbbfdb973e3f36dfb159a0fbbffd0787d7dbb3ae4f19fc7bfdbff5288ffd8d7a86b4c9f12aef15d67f239bc5bde7578b75c6811f1f5e98e2174ce9d405f40dae7427d3623be05ca375657179f5ded76dcba3e07733f870235b460f9f4f3397e23fbb836f0d573cbbafa99cf761fa14d216b6171fdb219df76dfb3160d9ffef667d7debef8f01df3b5ef5bfc42e3f69df7d92f8fff3cfe5d598af13f3c3c486bd7ae560ff013c5ea8312f821c76da1ac66c9b12146c2f413d51ed833204abe2a9ad1ca3ca4b7f5e787f41a08605ee803e012930e50819bd4b9547721d207fcb1d6cbb0b1a4ac5442d42026f86ceac40a94c0ac5786fd4abfb3a9cc5ca12f2b8597b1055e5c5f337ba1df5a2a45a233b79ea181232726d07672629ac6c6c6d323e4fa97a03a00535c24665beaede9a6dede3e0d981034d0df47ab578d50a100a085fc3ed0473ddddd0cf6c1d82a952aad5ebd1a2d301004f58ecb7e91ea6e7878886dd1d1d949f3e57966bb420a3d305c0198849477c47394f0e772659efb04635321aaa53e83b50adacee813a032304c25da97d470049d5e110c4e32fe5bd040173059a16ba499c3fc8245cc806a002002400a9a0110c48c5ef21fd2e660bc003d81a909a0a8b9d9796e0bede318805d4805393757663013d8c300da322c41a56289fafb7b1964353232c4602d74db296fc6616e107b33609f63b6b14e1a1a1a61080ff40058abbba7976d08b00998ea86060618400650ddc64d9b68ff8183dcaff2e92aad5db34ab65da2e1a14106733df4f0a3f4c0030fa7c9f9149b938a12c35c47dacb376edcc07a22751cdc1080b474dd63209bfe4c1a1ca4b1433ce705c5dcc6c037f6b70a4d4b3b245c4ffb75a4d727524c522adc04dd7bcf0fe8fbf29554233214531c737a7d31cc5ab1063cc1b62f79c995f40bafff8505739fbde3c494189ea9824a4788b49922aac8bee7e5990a95d2350625ab64960521e2a652690aebff066db66bd76efad217bfa46ca98168059dfe91c33dd2ebb250c035067e464acf582b20d2948eb20cd8aaf45a1dbbfbb4298f57540f74451c72da42bd3f1434f3156ba9d7eaaac56e6518b24cfa43d65b033385f61d334749ba0650daa7498758d48c73583fabfa780a9845f948a795d5eb6f64ce2586f12caef5a1f7adf5ebd7315b5fbeff2fbeb8d77cbeeb48733eabaeefb8f9ecdadb777dbc14aeff1753f2eb7f7ffcb76a5edaedfa7f7070905efef2972b10778be4f6db6fa7ebafbf7ec5c4bf7bdc8d45df58ddf6dd7dc557ceb7b7ade4f86f85b472ff6f26feed7a217bb9f36fcf6b23df8e02fbbf68d1a58a6fac79fc871f6e2e8634b2ffe9d8ffedbaae8df07b066971b137b64abefad5afd2673ef399651bffae9e21fdedcf6e4cf8e2c377ccd73e6c7bc71d7750ab04bef5d6b7be953fe7fbbf5f5a15ffbecfbeb2be3972c5d4c19af2ee77bfbb656b0ac09c903cfe69c1581bed3f2b69ffcfe3fff4c4bffbd9ee23cb3eae0d7cf5dcb2ae7ee6b3dd471eff79fc2f87f88f4d01b7926d8c135522e4a459139a65685fb036a373489766f532e77d7d35e3f4a1c52854c72defb349c881dc7359e3761720d769dd76ddba3efd423ed14c70f91648b79c4f579fdea171d8e30ef5ed1ba73b8fa145c3d78eefbc5bc68d2f57b750bbeec2e2c689cf26219fc8e33f8f7f5f7ff6bb7bdc2ddf4ef1dfd3d34b4343833afd9eaea7cbc51a00851e994d487f2e186612a214008007f9289bb29b582c276837b674300fe06b20a628edd7066cd581b7ecd8327d0228025d4d5a2bb46ffa312f526032000fcc38ccd80a563da1fb3663340c3591b677555415482056402c1cd550145ab76e2df9a61469cf14c842fb0773e3f00102a0031086f189499a9e9e2143b0c95c3f911cbbac9a4482fb65961cdd3e3ea2ffdede2e9a903fb415904c81e0c6c78fd3ecec0c952b552a7574d3e8d123fc577715a42aac56f81c005a988bf9729966e7cb0c6200906a7c7c82d3113238a69a48bfe856735e51fda7768b0a2a3d9fd405cc5060018a75da419ec1881858c4ec5854cf74c37660c365af33272bec339cb63256690601a941eabcaa4a41a8401f499a3a0f0c565008c0aa62b1c0a021cc397f2f1419685288543d80b18cdfe01c3e239d20c0516017334042804d007633e9f5aa3a8d26c048006701b015eb947933723ed01ed8adc05c065016ca1c3e72947d03690cc1c28436c0643536769c8e8c8ed2aad56ba9abbb8f8e1c394cab56ade2185631d721fda297d3f821c51d80903ffef1433425fd4b08b39661c8b19a9b48b19795183445b46efd5a7e386bec146b209a7266a46f2cb3afe9958899be3824a258652325c398a7c06e3353d3c037f1e722d74bd8ff159c0aba44d28727e981fb1f96be54661b9006f4993e94be6a9d492a8a4dabb3a344af78c5cb68cb964dba2d808acafc02888a13988aaa1d8964404e9c06518021aac07da5b68855880134885487149b984d34fb54a24163a2fe15b92fac33f3fc8e0613d9fe77bffb7d7af861a41f54a93a158e2ce117b36c319b965e4f84ee4f03ae0cf053a582d4eb9b01a69a75c5622234eb15d96bae7e576b4b54b79e9af599ada4db46bd1428a5358df4abb696d5ef4f290ba2067419fd0c280b7a257a8fe07e352836d27522d5508d0d51afc306dccbed6abf807fadd7be9acef00adcffdb4542d7712bf5fabf55e28ecb487efddf9a7969c7ebff0b2fbc90366cd840ad947ffaa7cf707a96e51aff597eed8aaf9fac38738fb9fde5f1df2a89167dff3f99f86fb46eda9fdd7642b66fbcff53cb248fff6c9d175b42ebc3e9deff7dfd9917d2ff6dddba955a2937de78231d3b766c99c67febaeff77efde4d9ff9cc4dd44ae9ef1fa033ce38c37b2edfff5b1fff59e3b66d1a8a23df389102f0aaabaea256c833cfeccee3df6a2bdffff3f8f7f5773ae3dffdee8b0b575c5df2fd3f8fff3cfe6b923260650564e8586830be80f17d771d28a4b8bdb084fa3575ed36b21cd4d77e28d8dc71672d4a219d42edb99f43ed85faf69d6b1424ee58b29cd42debfb9ee513be368c8e8dfa34e57c016adeb36ce7fa8c3dcff677bb9eaf8e4f579f8ff9ece0f32bdfb990cfbbc7426d86faf6f5e79ecbe33f8fff66cb2e85f8efededa6e73def623e17bb363375ad145238668051e6013f4ae15c5daa29a30b51ca50c56deb7a864dca1c23ad9f4639a70c2fa4db1616638a11c3889578522146d667b41759e9ac0c8355d5a41ab4c66c98be0c4396b6408a1bd2a3d3403435ceed676d2305535828c24abe086087501fc98c6c72729ac13daa57d84903c984b6aaea2e057b75767732280ae027807900d63970e8108d8e1e63f0cef4d4048c45070feea79ebe411a1b1ba362a9837a7afa6866669a5394f5767731d868d5aab5b2ce3c03a6907a0e202106e054d558d84e0028cd97d99e48e7072017bfe467b063a9b1a8b9291463069d81398b53116af806cf5b92a4f3957d9977222278ac0c10838da44e60b84aaa0af8313f3f4711006705c5f4c33e1403380586a78459b060074eb709bf065b57a5c2e58f4f8c33d00a20adf97285ed52e114800569f35166ad2acbe373b2bfc989299a95b69b91b63c7c7494ed85f901400eb4f7601ac379f8dcb163637c1e60aae9e959062feddeb357fac1243df5939fd0133f799a8657ada35159ae47ced3c18307e989277fc275aa49c4ac5800e34c4c4e51a1a39b81767b647d8a4bec2800d2757675c88f05dab3ef10ddf3fd1f31204bc5a9a0943f4e871d6c85b1f7f4f6d0b66d5bd9f598f90af32fe7906d22ed6a58e4d847918e90c165826dc8be5249b81e69e01058d5f6eedd5ff37700a200ca538b0833a7a113f8dff8f1096e57ad01713ab7e81731a698ba6206290128f5c22b9e4f3ffffa9fe7148bf0af42a400578aa9292112c90237312046650780c10a142545f92aa89728323c2cc17a821745b5cfb25e95bd39a19412cfbcc0ea65bda238e1921566a9223a70e030ddf8f79fa189e393ba8e0643251a991691f65f1debc6a7130538e22ea2a8c6ea478a71cc0098cc79322ae9b523558fa83e35a0b507b05d35cb95bdbef3daaed7be824e676880a9c25afb6bf6ad5d3bc44efa4ab33e9b35d9807749af0914d5520b926160d4e521b19566d6ec21f0b9e73ef722063eaef4fdbf95e25e67866cbe92aeff5b2df9f53f05fb6e95b4d3f5ffd9679f4d175c7001b552f6ecd94d1ffbd8c7f8da68b9c57fe858d678f2dfffa73efe175f44d0e7da29feeb34168b77ffaf155297e63a8fff863a2d86b4c3feefdab0afaf8f5efbdad7522be589279ea03ffcc33f94f704a6f9fb728bff505beedc9ecafd1fa98edff9ce77d2eeddcf502be5d24b772eb8e66a75fcafe4ebff463674cbda754413b168cabef295af6c09dbac61c08288151cffa136437dfbfa73cf89fcfa3f8fff26cbfabe67f984af0da363a33e4d3991efff75ede6f1bfb05f53d76e6329c47f6c2b6037e8064aa853df607d926538df007d136debe0ea131a70c8c97d6d857437657c0e19eadb3d9725ae2eb6c39c483b76595f3b6e7b21bbb873ead669c65e767beed8ecbaa105c8ed37ebbbbb08fae6b6912d4336708fd9e54373e55b581a89ada72fd6dc7184fcdd17b7aeaf8616b8acfef2f8cfe37fa9c43fe4dc1d677b7d9fdfadb19055c73c34e7b4812863a59e43d9c433bf060c259422756c59a4dbc33993f2b0a0195238cda04e639582c1a08b0622d829ad50dea43e24fddd8c3bb10057913d370600806300ea181d4d5d94119166ae5159d26006ce9656001bcb7a6a1a55e4941b3b3e4ed353337a4c5692c7c8fea0d20c5635dbd2da352334393941478e1ee1ef1bd6ada3b5ebd6d2d6ad5ba9a7a7872626c7a9a354e4bfbcdbb87113f5ca1b8c60bf1a1e1aa643870fd37ca5cae5e6e6a699ed098c4d60b2422a3de851ea2831b808f628c445eaecea64b017c356c096548899f9090010a42c64800f001b8902932095611c97740a494a99b1d278a1532591f249405e04b15ec55241a79653df49cf3bfa8d4b259e34666862c722c5f05555fe065bce6b56abde9e5e1e1bc05700c3cccdcd33e35ab952a6c18101ea9736350c711dd25e1d5d1d34373b4bdd5d5decab254e7158e0878c68376246ac22039390e66f786498998f00981aecefe7f6d76fd8c8a9d58e8d1e21303501e485ef483f383d33cde5fbfb0769cfdebdd2b60975cb3e0e1d3e445bb66c96ba95e9d0a183343131cea0b2f1f149faf18f1fa6bd7bf668504ed58a05005f00a2118ab94abe6f3ffb4cea937a28c08b50a6d56c57f04bb4a9ea2be010e28f333852558e3f667d8a2569ffb84a676ddb2c7d7a8ad36bc61a682512dd66a40c6ffc22e2f88f782e9244d4e655bfa39c02ed092e87349a6f7ad3eb696870906330a6b8a9d053eb57ad6991341bb04e3b0ee315e70cb45e2289d85e008c017877f3bfdc42f7ddfb0366ceabea748c8811a1d73fb687a8a57f8df4b1744db4d6a9448fc300606d06413346f86855881a6315d580b142a76ce57afa7c9aa235aafd683396e16354ff208b6725b00755354b95b0d67913f3a64dd6c7e86fc6acdbaa5af54c1f894e9d98eec1a458e6b66ddb9aefff6d20b60ddc634656daf57fab24bffe6fbe9dc51077cd6c248b75fdffc637beb1a5690821b7dc724b66daa52c69f7f8f7e964b7dfc8277cc7bdbfcda2fcf7bf2ffedb4196caef7fdfdc365a4b43d740eeb1ac361643f2f85ff8d9aed70a69b7fddfd47fd9cb5ed67266c82f7ce10b725fbc5531712fb3f80fcdd5e9daff61c30f7de85a7af0c107a995d229ef85bdfded6f27a35bbeffb767fcbbed84fc37e4fb3e1b0f0f0f3323fd620bfec0c1d6c3969512ff6edbf677b7bf95b8ffbbc7f3f8cf9fffe7f19fc77f3bc77fec33ac1b1cbe800c19ca0d609f63355a649a09ec2c8387c6e3eaed337c28005c09055628209b5d604293ec2bebea9355de67f328b0c866cd836f9133edfbc4371fae0ea1f1642d5ebe79758fbbfe6b8ef9da0ae96c076f338b8b6f1e6cbbfbceb97a84fcc13d97f5bdd958cae33f8f7fb7fc7288ff8b2e7a0e75683691b41cdac3e7a8968ad0a4ec13763bc280316aa027aeeef465c494c1f9f4c1bfad1359f689d4c3f8340d57540308a4b64992b4ae011698f3ea41bf6eca6937b2985adce364d8af34602b9de748032212cd84235bede9eee69b2c27832a0257cee4c404037decd9834514c38c156b5a3f807a8a1d004575d3aa91111a913ff2a15b5767173dbd7b37a7bd034068449efbc94f1ea38ad41537a3068786e8e0a1c3b47ad530a726c4183a659dee9e1e0624c106c343030a7885aee2888154e5f939fe8e3475e8070c5300a381fd0a7538255fb1a04170d534ad18d8a852b332894daca13ba756226d13cc358054ccf02474ccb102ea3c8055ecab0042c9b18051ca8cd53064e155a956d85eb0ffd4e424f549fb004ed3dbd3cdb6450a46a40c3c066631395600dd2060b842da46d812e91d915612e02b304ba914988962c14a04338d1dd87f50ea51a019f9195a76cbba60b7ea9473cb00ae924a113831312d6dddc52907bbbb7b69e3c60dd4d70be6ab1eb6e6d9dbb7d333bbf730380fb6806bcece5579ae7ffce307681a690045644cb160cfc2bfaece123def79975064d8ddd876602b2aa44c47183b58af700a730e504c0ad61255795efa4454a5b3cedac68c70dffef677686e765e7bb3f25dcc8d308a30884b553768a02832e78535b7b18e69f5ff975e7d15bdfad5af96b657b1c8a9064563af5ab09e9f4cc03a6c57cc7805462beba5fe4ea320e75cd0b7eebc9baebffeef186457e094a78833c50216d9eba85e6bec75c888d0ebb0596323f7a5cba4ab45a4815306c8a5db8fec35cfb557e8ba208a5270ab396774f2ed370cce2c16537dd8efcd35a1a96740b0ceda1b6bb059da8f638314ac265f838383d2cfce5cf1fb7fd675d26289ef7a3e341fa1f2eef9a57efddf6eb2e03a8756f6f5ff6249c8275a7dfd7fc5155770daa5560ad829aebbee3a7ae8a187ea8e2f87f877e7dc3e6ef793fffe3fbdf1bf98e21be2e9deffdd7261dddae7fe5f2b258ffff6ba3669d5feefebe3cc33cfa497bef4a5d44ac1fd828f7ce4c374db6db729c6ee6518ffbef2eef953b1ff03dc7de38d37b04d5b29af7ffdebe9dc73cfcdf7ff805eed12ff76799fcddd35ddedd7d71ed24ee20f065b2d2b31fedd7359df9b8da5e5b4ffe7f17ffae3df9c6fc646be36f2fd3f8fff3cfe6b12bbca654d7ea8335f47cd04765630bb816ad7f39df34d6e96ee8dced9fd642d6c59e742df4ddb8da499328d02d3373fbe393ed97eed769ba963bf4cdd46657d81ec0b2af3d93e1e3a96a5bb6fa171cffbfcdb37962c5fb7fdd8b54b3312b25d566cb8af3cfec392c77fe37eed769ba9b358f18f7cf59b376f5265518f4cda2f2560b91256ff890516b0358a4c99a896ce301135362c032030e7d18e490728ac322ad599ee47f70d6617bc1be62d3b45a13986b6134efd95a4fa44b17581a4f535764975b35f965d48ebaa3e8b74907c88c757e5bf70c277c3f4932d51adb948013666e50d2226c6a9e38831b637fa88348d5867a944ebd7ad4999c3d00e038600e2406ac04a99d3d9214d5e7fdf00f5757733ad3d52e2cdcd4e53b158a2ee9e5e95460e6009066855558a368ad9ce184bb7ac0746a78874bab4b8c64803e61f008acaf3f30cbae0730ca62bb00d156843a44362d392a89bff532586d507f45b0054191015f17c2b1f444a4493ae0c7600eb17f4c03100d7c08205999a9aa69eee9ed41e0025c2b60047552a890271610c72aa07fafb643f0ac485f218235240cec957776707cf3598c3c082043016fc6c6eaecc8036d875726a92c691ba90d3f0c46caf91e101a9cb3cf5f674aa948a5287ceae1e3a72f430cdcecc50a53c27c757e6b9451b1393d37474f498f4c11eb639e6aca3a3c876deb37b0fddffa31f73bbecaea90b0afec2ec64dad7366dda2c5f1b95ed9092312e6aaca1e07955291a89c78a7852ac49c4ec5791a8323317dadbb1e35cdaf9bc9df4a31f3d483ffce1fd94a6fc13a4e7bc9e198fe390441a9be9faa15ddf7c8f0b11c7c059676da55f79cb2fd1aa35ab157b56440cfa6397d223cc045609abfd385c2c8e2865bbf33692be6a8655438cb4cfc5f4f8e34fd1f5d77f8a7ef2c4933a6e745c479482908cef3288d40247f131933253d418dc12fb5a846a29574d3905528a6b7b81d152af6d519d9e6a4e12776dd7e512ddaf291bd5cdd9c2a52ed2fd080dd842bd826145a4fabd37d6690d531df59e4151ed079f495b186b96437bef3be79cb3f8af4beb666505efffad94fcfabff6dd7e6f85e4d7ffed29ed76fd3f3030402f7ef14ba8d5f2e4934fd21ffff19fd06e79ad0259cef19ffffecfe31fd20ef1ef9b6fdf1cb97afa746f66ff6f07c9e37fe118db411633fea38cebffaeae2e7aeb5bdfda7266c8c3870fd3473ef211dabf7fffb2897f9f7f9fceebff7beeb987fef22fff925a2d9b376fa137bce18d9ce232dfff5bbfff67c5bfdb86ed676eb966fb459601ac2b8b2d0674b852e3df27f9fe9fc7ff62c7bfaf5ec846be3a51beff2f68278fff951bffb1fdc53ee9737c9f01b2262ccba142eddaf54201d88cb3baf5dcf6b3ceb9fdd8c77d63c8eadf94b33ffbdaf1395d68110d49686108d92bd4ae6d9f5019dffc85f46964a746e29b9346f66c74ce6ecf3ddf4827d737dde3cdfa856f5eb2e6da1d5768410fc546282ef3f8cfe37fb9c53f1e92ef38f76c5516c72c00932e947e4e743a40d3aecdca827242d7b3eb2ee85303af529dddf276cce887ef421fc72bb6caa37593da30226b0e9531eae75403bb22bb5fdb6fe3d8364c9a7e8beac6121145265d6244434383b24c91146084e14ace803c521b2c4d4d4e50259927adae1a22211d975ddccca160504fa958a29999693a3636cec7e7e6e7f9c61d301ebd7d03cc86343b3bcbfd1c3d36caf69e9b9d6130034046a8736cf428158a1dfc631d432ccaff81d5a9abbb8b66e66619e003850aa52283ad90b2cdf629b40516ac2a405f48d307e09bd40f201e4e05582c689fa03a00c6a916b6555501a80c8b1b74c55c2bbfa0746e6007b038e118ec83948b7807700af5901a10df4ba502b709300b6c8bf48b0071a10c979f99a3f9b9f91448020016d20b32f3959c9789c9292e0fa01704f182be60e7a9e9199a916d00acd605e6349ebf39391fa3cc9434353545c78e1da3f1890906173df3cc2e66641b593522db98a223474659af89c9499ed329d917f4c7b181815e06db757474d01d77dcc9fe910815d389b67f91fd19718af10906545d70e179d2afbaf49c910666c57529e2f019354bd60deab8a07c02a0b58b2ebc802e7fe1e50cecbbe9a69be9c0be03da6f2d306444cc26a651373aa6297d9935013a09534923a130a65ffdd55fa29d975d4a6a2809c71a679374d7f18ce04bd78b844e4ad866514235e095f9a718ed7062ff81c374ed873f46dffce6b719b847224ed72eb31e9ab5c500af2c33b0a1f85dafcd3c63668cd63a5bd56053b3d691ae5335802cfd9d63cf6ad3f4c17312c7756ba001afa67b95299f8e5fb3f345510ad23260a9c4be2631e965f598cc3161c662ed31550d38b367ad60f423aa8179f5b8b66fdfa6cdb0b2f7ffacb2a7539a198f919578fddf2ac9afff9b8fffc594d315ff59651a5dffbffce52f6396d456cb9d777e8b7eeff77e8f76efdebde0dc528d7f7b9eed79b5db7163252b4643eddaf5f2dfffa7e7f7c589c862efff59651ac5bfdd6fa373767beef966f6ff564a1efffef86fd5bcf8fcac15fbbf2fd676ecd8413b775e4aad96471e7984d3e71d3a74a8eef8528d7f570ffb78b3fb42b3fb3fc0dc1ffef047bcd7138b2db837b875ebd6b68c7f5b96d3feff6ce2dfb5a55bdef7d9b64fb37d9f6e191f1f5fb1f11f1a57beffe7f1ef6bc7d5e374c4bf6ffe42fa34b2532359e9fb7f685c79fc2ffdf82fba85cd77fbdda75848dcf38dbe9bbeedc1ba93e23a6d960ebeb2f639b76cd6b92cdd6dbdb274c9d2c99d74bb1f5f3b59655d676d64cb9003fb1cd16dc71d5ba85e969fd8757cedf8da129ecdc9b5b16d87ac7e7d8b902fe05cffb3cfb99f7dc77cf66fb47084da6ed457d67c66c5581eff79fcfbdab3bf2fc5f83fff821d0cda00d30e97415b6edb9aa9455660fc42ecae1d9162c2314c2d06d0c480ae483db837e000722f0e84a881237ca27548d42035830f40080933f090ae6b7412463fc3e2a2f522db6616b84a683d45544bb99882afb8bda82e21a0628512ccc4522844240c6b55e6340a0bfc1031c86a72728a014ecccee31bb6fe7f1c0b7e07d0a652ad5247670f0d20b55eb9c24302186864608026c68f3370a7bbab93e6e6abd4d35392df2b342de715e912c190056d87874764d9514e2918c7bdb2cd84991184b66da943a59b0330a22a1205b82aa8548515d92798b4f002a3545c285231c6dc5615e392506c586008e379880b8d0c73d252ad1a9010a54c40f82b56f859598e157f8156d50c55488b38c760a5884aa50ed608a02d8cf5d8d871eaebeba1b9f23cf514bb79ee6767a718948534813c07729e01582bcaf18e1d3fcee352402db0fdc40cba1a1a1a6290d33c005bd21600651d3d3a4abd3d3d343535c97f0d393f07fb0fd1d1d151064c7575f7d2b0bc59c7a922070714d316a954886bd7ae63ff7ef2c99f5057679176edda4523ab86599f524727f5f5f6d0818387a824753b3e3dcf00bc3b6eff06fde0befb19e3c269ff48c7b39c47c40f404b600983ffaf5bbb86ceda7e168f016021f8170074ec9f0ad7c66303380bfa80050b615188a44d2b11ad59b39a9e7fd9a5b465cb169a9eadd22db77c89eefaf6f7a58fa85edd708e3428a78ad481a4d79fc41412696c28a6ae48bda4ef5ffd332fa137bef10d5492f315716c56198015370e3a1575ba8bd894b517b8a64590863191819aaa753456f19b007c75883ef8a71fa12f7df1cb32aeb11e150d8f1d2569124e055662e09819a3bb8fe954a8e6b3b000abf04db38e1aa0130090e99aca0a8a147457b737983dcaeacb064e1956ad82d14bb7a546609537fb8cd54e6cef9342d4d66352202bac1f5561fd20b5fa8cf45acd75d4201550578e0bf16c98ba005a3cfffc1dde3dd01eabfbd92ebb5cf6ff568b6f5c59d7918daec997c3f57f2be726bffe6f3cc7ad9076bcfec7c366a421fcea57bf4aad963beeb89dfee44ffe84fef88fff98d6af5fcfc7966afcdb9fdbf1f73ffe8000cc63b88e1c95d79f7373f3fcb0faecb3b7d305175cc0d7afab56ade234bfb62c95f86f07598abfff1b8dc537b7d109eeffad963cfeebfb6e27598cfdbf99ebff5ffaa55fa4471f7d8453e4b6526ebdf516791f608eaebdf65a790f60edb288ff283abdd7fff7de7b1f7de8437f4adff9ce77a81de48d6f7c23ad5bb73678be95f16f9f773f9bef6ed9ac734be1fabf99f80fe99cb586baf168eae0de85f9e3d0c51464b4f0e9b7dce33f74ae99f2a64f5fdfcb71ffb7cfbb9fcd77b76cd6b93cfe17fa8f6f6ca17a597e62d7f1b5e36bcb8ccdd77f1eff79fcdbe7ddcfe6bb5b36ebdc62c47fd1d780af63b7515781acc00829e51ef3f59d3570f7bceb543ec932986fe16ba64dbbbcaf0fdfc2175ae87cc71af51bd2c3f73934fe9093678d23d44668216834cfa1b67c8b62a33aa1be5ddff1d5f595778f379a63b77fd35696642d32cdeae08e2924a1361ab59bc77f58f2f86faff8dfb66dabbce9de4f53d3d33a95951ea355c63c1cc7437ffcb014967ea4fb46c982063019469b82066d5535180a650d508a012c9cbe4e1d2fe8e32e10cb000322abcf84c7a1fe070082d19bd3e9010804b61e946356a602eb5ed560306ec3001ccc18cd3835d00ce9f8521099866071311161500c26181a1ea218889626d11cca56e673429393131aa01628afcb297b25d4d3dbc9e029d93b150a4506c7cc33e5b4a07dfb0fd2d6333753676717b32f012c736cec186d5cbf8e99896667a6696470843a3a4afc1de90cbbbaba6906735e285167071894a638951dea777400c455967d7633480c76c57b49b34855b47d8ab045a2f489a38453182600f8609e01be3ab1906c5a529f471f82d2d4823c490cf850ec5d9c0a4dbe30b6829cab52b183ed353131c90c4bb07f2fd20c025856500c4f005af5f7f5d1a4ac03d63133bd95b2025cc14600570df4ca3293d3fc200bfe81075cb00bca83e90aa093bebe5e36414f4f3f03c0c0a85565bd62daba752b1d191d6306b309e90b3bce3d87c7b566f56a4e2f387a7c1fad5eb596ff9a729dbc113b313e2175eda323478ff23ce221dadab52334766c94babb3ae8c0fefdf4d97ffe17d60320b824414cc0e7647f008c01ec94a818ee94f52fbce83934d03fa0d709fc6893a72b82f95e6137c33a04ff2b14a5df2096a20a8f6bcbe64db473e77369fd86f5727c45baebebb7d3dffeed0d343589548d9893c4efd1981fa1e64fafa20af468c240e8f50b27a5de679f7d16fde7fffc76da20fb53f38b1389063099b52cec2711c3a60c3fd5c93b636027e43506ba1f387890aefdf0ffa0cf7dee0b729e2becfb692a4ea4118d1d3db5af46b470f5e071c671ba1609dd3fe6218d3ba410b5d7ae48f9328e2316b072c51a786ad665066a596b35c406aec6ba4fd6c7289baeb51a2c65eb687d16ba9ff4bc3e8e35bdcaa94e55aa54037035fd44da16a62ceba4d7739caf6a762c9c5bb57a156dd8b82e784db092f67f219adb734eb5e4d7ffb4a02f23497292d47aa748f2ebffd374b17192d2aed7ffd8239072090f2b5bfdb019f2c52f7e912ebef8627ad7bbdec5df976afc877cd35d1bedf3a198f11d6fe417a1f8c775e20d37dc403ffef18fe9befbeee33937a96a8c209dfae6cd9b1998f7abbffa1fe8fcf39fb3e4e2bf95d2c807ddb2edf4fbdfd7976dfb50dd66f6ff564a1efffefdbfd573d32efbbf6bdb57bef2950c4afeca57be42ad16e8d02f7f9bffd11ffde10240ac2ded1effcdeccff639d35696b8fb3f58c33ff8c13fa1ef7def7bd40eb27dfb767addeb5ec79fdb31fe1bedd500698381cd80ffc0966a33a62e97ebff906d43fe699f73cbb9e3804f82d17eb105d7702b2dfe1bb59fefffe4d5c5d76f56fbf63957d73cfe9bf3f146ed876c92358695beffe7f1bff0d8728bffa2cfb91b39a15d3e648cacc1bb8e66b7e72b6f7ff6e9e41a2034b9beefbe36702c6b2171fb0a058eefb86feccde895650fd771b2163b5f6065cd6f68b1cb6a37ebbbafbd464eee7360dff166fb0bf9a72f16ccf7909ff8ca37e3d3be85c4e7b7211f77f5cad2d5edbbd198f3f8cfe33f5a06f17fe6199b69cb96cdcce062f70d46a84815520fe7f151d4d8a178fef5310873bce8b458361b96eea83e0d160e991458fa38b9ba997a761d7b7cfa617e01602bcda0c28c34b1027c155250966276326006b27466d62b6327dd279fb3eaa628095d80ffb249763302062cd415c902dd8dfec27c1205d94f354dc38587b59393339c268ec13242a4a5edfa516498ba04037700aec07b6f5f3f1d1b1ba5c1fe7e9a9e2dd3b6ad67726ac2e999391a1818a2ae42917a7afb68766e9efb52e91205b39c757676ca7604a776030353b55a6690d980bcd187f3da320cf20188097f8d098015c0570cb0d2730c6013746126b292d233615632e31fb25c12a5a9e44ead0805f48a0b0c02437a41f85e797e5e037ad85bd92618ef8cb40b18a366e766a9439655c0aa5a3c80790aa023a4f4034316040f2a3b3b3ae110747c6c82ed817627e48d95919155cc60d5d3d32ddb2c5357b1a0d98814fb16da85edb9afce0e66d282fdcae5794e6db84aded0dab76f9f6233937647f903070eb0ae478e1ca533ced8226fe28cd1e434faeda2871e7a90cedcb69d1e7ffc718e5580be26278fcb07679b65f923f266cf0cdd7cf3e7e8c0fe03cc7ca5b0009a0d0ea0414a524635f8ebfaf5ebe8c28b2e64509a61b8421c292631e2e3c9bc024d82750d4c6fc01aae92e3de7ed69974de793b185c3623c7fe8d6f7c833ef0813fa5271e7b5ac56b520f7512d6ff783d108631c984958eb148f9bbd068ac4d9b36d2fb7eefbd74e58baee4938ad10eca09522c592a20d5ff037b298327636e0f20c248a74174cbd762d5ffdd7c86fe70e744b73b5faed29d77de4dfffbbabfa32f7df1abd28e4283bec85aa392da7e41067465af6bf6beaade53a092e5a3895e7b51c7009804007f5403975635e01435aa29e04ab19a2156c004272c5097d9272aba9d748fd0eb32b7a3d771a1f522cf5e6aea0867df54f68a98918bcb085177dc6984cf1774ba58b643a190027e376d5a4febd7ad0feed9be632b69ff5f2cf15d0b9befa1eb445ff9e578fddf2af1f9bb39be92afffdb41daf1faffd24b2fe507ce37df7c73baf6b7523ef6b18f3110fdb77eebb75236d6a518ffa1fdc11d43968f66c540237dcd675c1f209d1500057ffbb77fcb20ac2cc1c3c2471f7d945f37de78233362fdcddffc8ddc733779f56de7f86f85b4c3fe7f22f1df4c7f21bbfa62c17c6fb7b517b212e3dfa7533b88cf6ea773ffb7ed211a5cff63df79ef7bdfdb16002cc84d377d86f7c43ffbb33fe3dffc4b31fe7dbe6bbe37e3d3ae3fb8e5efbbef07f49ef7fc57060db583f4f4f4d0fbdef73e6622f2ad0d465a19ffeefceddfbf9fbef6b5afd1dffffddf3323a60f947fce39e7d065975d46575f7d35bde4252f49afd1da71ff3fd9f80fc55723fdec32467c20f7c5908181c11515ffee184365f2fd3f1cffbe761bc59969378fff70fc346a37ebbbafbdac76ecf379fce7f1bfdce23fb69d26e4d0ae13db0df914f2d571c51750befeb226c8d5c3bc7c6d659d339fed31f80cecd3cba753d6719fa3bbedbae77c360805ababb3ddae4fa7d062e02e0c7659b7dd2c9bbb65ecb66c3b6449d460916da4bb4f37577f9fceae6ffa7cde6713d77fdcefee7b333113d2db6dc7b700b8e743e3cde33f8f7f5ff9a51aff23ab46e88c3337e340fd786a056b63d74027dce067c6071cd7650c302732ec2d8efe69db666c4687483d30c083200616182689284a995c22cbf648b3079614c18c3c91ea5bab8aef866985414d5a4793a22ed2e5d136e9545f299841f76b74e28d1f69d9f82452d925fce221c977007a1412497092317b9c511c3140038c59cc9a2500e68a49594bd990d3cd01a084be009089123ec763066842a8b469388bf6d0468119a784fc813f46fdbdbd3439354df373b374f8c8619a01135354a4ee9e5e1a9f9824357cc1c01f008b0a607f02a04bbe7073a05ba72534e904ab5505c24179cc8520c5520636ac5207d8a114b04d918015d4dccbcfa512d8c6526749df905e2e7b750b8b1dd33c1f42cda37a4f34d04ff94799d3f6510ad4e0f1cccfb3a5bb3abb78ac3c57668ea3884163f005009900f8e8eaeee11481c5520703aad07aa9d8c90fae904e0e002f80e5ca952a150b259a9a9ce4f7b8d021eb97a45d704ef90de67562628281576817733e2ebfe31dcc6478d835767c9cce3cf34c068f2555c52056916d03b485d489478f1ea549d9c7e8d123726ea619b88574923d5d9d3439715c8e7946dabd9bc6f966594c8f3ff6243df4d023d29d98a74eaa51503123449ae68dc14b5465a6af6d679dc9290f31e7f0ff580385d298d52102c00ea6371265dab07e355dbaf312bae8c20bf8464f1477d0bdf7fd98fef27ffc4f7afcd127f59a504d63a136995417f7fc919234d613aa725f7c0e002cf95e94fef6d297ff345dfdd32f512c6642fa26ca5159af471c451c0b0cf4349d7098e8b52212b643519620bc005b32f18ab82be857517e2b0887358a908e735ede00be9f3e74ed47e9ebb77d4b8e27e21a980ff65b2e5bd5eba26a939712cdfa65d60703ee8c0c3c4ba8f49a6a7dd5a027b37699b5ddcc2d597b9c5e73855ecfc0c26560548214102b96b151608096598b6aac7f6c87d80029ab29e694d754a420d565ab369396d6c980694dec629da81a362dad233361e93516e046d431602ba3b75a0b49aff309eb61f68acdf221c4c0603fd9eb43beffb746f2ebffa8ade6c4e7fb4656f2f5bf10277b05f2eca415f17f22d7ffb8c6b8e69a6b98f9a81d04ac9d9ffce427e9baebaee387574b31fe7deb9b6f2db4df1bf9a7dd8f3bafbe384399f1f171065dbde73defa18f7ffce30dc1573e79f0c107e9bffed7f7d0adb7deba64e2bf15a2ba5d18b3ed1eff76bf7619df395b379fce59fb7f8ba6c51baf909510ffa1369ae96fb164b1f67fdf67bb9c6b8ff3cf3f9fc1a7ed2277df7d177df4a31faddb1343d28ef16fdaf6d9dadd3fdcefeebb3b7f000b7df4a37fde36e02bc84b5ef253cc220969d7f8372fdce303d8f0b77ffbb7e9831ffc203df0c003414654fce1dfa73ffd69faf55fff75beaeb8fbeebbf93aa39df6ff5311ff3e7ddd3d232b86cceb2b5ff92ab388b542564afcfb24a477beff2f8c7f5f5b59e7cc67df9ce4f19f5fff87c695c77f1eff3e9db28ebb7e5f0c05b55bd99d70f75c68603e2542dfdd85c7a78feb60eec07c92a55fc830a1cf21e774ed90e5585936f47d0e2d56594eee5b9443bab8cee6dad53717217bb86dd8ef6e5d572fdf785cfdb37409b597e51ba1feddb185fcd2d7874fe74663c81a93cf96a1b61ab5ebea99c77f1effcb35fe0706fa69c78e73701085c827386ad8af525d23fdb01cf5ecba4281224ad6789805c5e88877c348251fb0574c9a293ce4376d9b766077f45b28a48c5a00ff083d1e060aa00e40055a87c4b2ab61c94aac3147c616e61bf471c76d8e45e65c64600ca4f110d42fed668013224a6aac3a4277c29fad3988cc49c16c3c60424a8c8d8462d5527d6ad0560c604f85014e00b6acdfb086013c5bb66c62800e4040f300f44cced0f0c86a6e07c0ab62a94013f21c58b2ba3a3bf81d433f76ec380d0d0d4bfd135abd7a0d1d397288e9edc1ba74f4c811c5d0345f667628a1812d60c9aa80d1292e5279aeccba144b2acd19a78f64ab288018cf8bac57d1c03880c528bcdc9ca068b01c293ba39f342631dfd5848ad051ea0a5be10123f403380d2c564893084047676737cdcf5739e520d22f963aba183c0516298042babb7be48da5e3dcc6fcdc0cf5f4f432e8aaca4c60f39cba100f2da7c627e5d44ef3bc15651b0052eddf7f90ff021216999e99e5bec04cc5290dfb7b65bb636c1b00da007c422a4100ab1053434303528779b63fd8b18e1e1de53223c3ab186cb766d508edddb397d6ac59c56d83250be328c9f979fcf15df4f7ff80bf961d95562aaa78844b45605d136a8638e422062c8dac19a6e7edbc84816a38069f2a69b6338dbf6110595c407acf32f5f70dd2f9cfd9413bce3b4fa55494eda2efef7ffffbf481fffe2774eff7efe7b49f4254d3d8f021ef6c57905d31e8292a446430489cbe54ce6f87f4df37bff9f5f4bedf7daf1cff10c72003716203ccd4eb0429e0221a306186b8e1fe4184079091016109754ead5b7efdb898304035476bac5f683132604d92f339411ff97f3e4a777dfbbb542a76cb229db2d30a2f19f0377482fe0c6b94ea33e2578a313506579d9361071371a4c147a48055dab086110beb9d014915741cd42d3b0049213a230d8c12dad68ac6904baa357961da3400a5187465afd9540fd4223bdd9ab5be323052c6515103ab78ed35e95ec9d95f450da029b48d794f74c0bb666d7ece79e7c8b8ee5cb057e6fbffe289d1d9487efddf1ed28c5d7ce5dc3aa1ef6efda574fddf4a69e7ebffabaeba8a59b03ef399cf50bb081e36ffebbffe2b7df293d7d3669376d892768effd0da679f73750fb593f5dd1e9fddd7134f3c41b7df7e3b7de2139f38250ffff0f0ffbefbee651696b7bce52dfc3b6129c4ff628aea326af9fe7f32f11faaeb1fa7bfff46fb7fab96e19518ffbe32ee679f1e8b29add8ffb3c66bd731dfc1c2f85ffecb7f6909838d2bd0e1339ff9276629fab33fbb96b66eddbaa4e23fab0f9fce8dc650dbeb9ea477bdeb9d0c1a6a17c1bd34304481fdaa9de31f0280f5b5d75ecbd70b2722f04780b6f0baf0c20be915af78055d73cdcfd3d9676f5fa05babf6ff671bff215db27cdcd5e13bdfb99b5a216026832cf7f8f78d49acf0eb7f5f19f773d61c67e9e7d60fb59fc77f78ae5c1fb2dbb0dfddbaae5ef9feef1f531eff0bcbb89f976afcc71450d655bad1a2e19669e4f46e3fae317d416bb71b0a44fbddeecf5d685c3decb23e7d4375dc733ec3bb657d0e10d2c10d62fbb3dd5f967eaef86cd9cc626e7fce72fc4601d2c837dcb2a1f23ee7376371cf355a248cae21db873687503086c610aa139246760bc589af7ca3beec3279fc2facefab93c77ffbc63f52ab9d7ffe0eeaede969aa3fd43120276118abecbef578aa9a2d85c7c78c2f544ba155558018d42bd4a5fbd3f38466f0d2cc55552bb5218e1ba62b4e93a84156e817e722fdd28aab7787dd8a9976a856d066dd322812d53f71d974dc9ad5057d767775282da3c6eb85651aad4e4233b3333a1562a4fb10b6c1b92c2cc1b6e2b46c310d0f0fd2e8e818eddbb79f26a7a678ee56ad1ea6f2fc1cc3c3d6ac5dcde0a3f3ce7b0ef574f7309b1358928eca87286c53cd1e0666acfefe41ee03690701c203c0a853d605f806602c4e05472265e201300b4038630f661f4b8dadf4c50b73147399382dfbec45a4ac3ee8cfa4cd51e920958d00b6814f601c8a51adcc60a7f9ca3cdb116c574841887175747651776f7f9a16100f97baba7a98116c787898dbe891e791620e2016dc00852095238046cc482635eaebebe771028cd3d7d7c3f3807ec02e8614859081fe3e9a9a9e611784de981fe8797c6c8c0154d061d7ae5dcc96353e3945870f1de6d48647464799056bf5aad53c763c902c96ba1838b666ed3a66cb7a7ad733f281e5d768ffbe030cfaa9ede3a4fd5270ba3c4c05ce777496e879cfbd44de2c1c52cc52f2d5ddddc51307705d877cc9c8a55231a121a9f785725db8eaaa9fa64b763e8f7aa58f082ad0f44c856efbfa37e9031ff820fde8070f283625c4bba8b1362d1405d413a9bf83560bf153926a16396d21806e0303bdf4e297bc90fedbfffd3bb461c33af67bc382847a1c073c2cc198c52431eb506d4de27483cc8ea5fcdc52815f3165adab19eb3b83bb22d26854eaefeba517bdf84a5e3200d8ab54cb0c62e379d0ffd41aa38049355096f667b32e536dbdc2a1446560acb15109eb81962e64d6ab822e53d5cc84550d5e52711a71f8f1fac94c67ca1f748647257a5d0560caf4673edbd77af6de67d670fbc5c03052cc56e91e69f608d2fb8701a291def36a464fd7f9d84e4b6b9defefefa70b2e780eb75f3bbc72f7ff5689efda2dbffe0ff7bbd8925ffffbe778b165a95cffbfe31def682bc60f081eaafedeefbd8f1e7ef8e1f4d852887f5f5b76bf2733c78d7effe3faf7a9a79e62a6abb7bef5adf4277ff227a79479010f5c018a0333999daab21de3bf75f15e3fbff6e7768f7ff79c6b43df775fdf769bed28cb35fe43e3f2f956337d2e869caef8879c8aebffcb2fbf9cdefce637533bc95d777d9b7ef5577f956eb8e1465e939752fc9fcaeb7fec41008cffc66ffc7a5b81af20975df67c7acd6b5e5377acdde21fbe73fbed77d0affccaaf9c30f8ca15d8fffaebafa777bef3bf30607ecf9ebdfc8799aeeeb6aef6e7768d7f5f7df35d34f1fb1fec60adf24d0384b36539c57f481ac554284e7ce597fbfe9fa5bb1b23ae1e76599fbe59f1b552e2dffeec9b739fce27e31ba1d8c91a471eff0bdbcbe39feada6bc7f88f7d93e26bc437609f2399726e20f806daec4214d231742c3479befe1a390a51d819430eed2e06eeb9d0c437b33035b35066956bb4d0b8e71ad9deb5736811f2950b2d6ab61e5901e2ce835d2fb4009a36dd45c0b728b8f543760b7d77756b14436e1d5b37fbb86bd746fe9835c63cfef3f85faef18fd7a64d1b68f5ea551412d7e226bd54dab76d9348b1a7703f263595fe6c5213729a405d3eb6cb403ffdd2cad7e98eefc24e6d65e99038764debe165e65ceb51e08ef471dbc7ed4d3f65a7a9d567388150a9097b7afad2d3272a0cc002284753f7442acf610ab0306a158a8ad98b0167f2140039005374944a0c4a00b8e7cc33cea07979e3a3aba383f5024b5322cb1c3b364a5d9d9d0cbaeae9eda6ee9e6e4e7f07a00b4047ea2f3c23066614b91f9586aea3b343b14575753210895396c571ca2255917d813129d2691ec134049dab9ac92c8ee205b63c156266009fc0c2c576d4a90463a9fffcdc3ca7d703e88a745966f1a90a39e60a83acaaf2bd542cd1cccc2c8f03a919e7e40bf69c9c9ce072b0cbaa55abd81e60889a9f9fa38d1b37b21d67666664b969065101fc025b03b435375f663b29ff26eaefeb63b628b080018825749abfe1e111cd0c16d3e6cd9b99dd6a767686d3111e3d7a8436ac5f2beddf49fbf61fa0756bd632886c66665aea5001c914eddaf50cf5f50fd294d4e182f3cfa3cf7fe18bf4bdefddabc13d8ae14a58fe9eae213a2de096cd1be9c28b2e60200b7c2b61a624655d9562738efa7b3b69c73967d3d557bf845e72d55574c6d66d14174a6cc77dfb0fd2dffeeddfd1fffddffe80bef79dfba4df497f40ba3c3090150c88502c98387687c4845cc4b6553aaa17ecdcd959a497bfe26afac33f7a3fad5ebb36d53de6604d4863de52e6a76a55d4af914281cda08b1ab348415fe65f2292ccbda11931bc4d48cbf9fa5ff839dab06923a7512c444a4105242566da522952f59819d8a95a08aefb4c0b4629d0d32a90aeb7e4ecc942af9ba4d7525e0bd9df949db99c01959ae530b626866aa905130b4c5bd5a028d3be0150f9f68fd8e8154529e8d6e86bd679b2dbd265f9bb6e8fec3dcd301a2a2d69edba35b471d3fada5e119095b2ffb752b2ae55ed977ddc2d97d566a36b68b78ef9dceaebff56cd537efdbff0bccf07165b96c2f5ff79e79d47af7bdd35d46e72c71d77d0affddaafc987ad37a50f9c5d69b7f877fbb1cbd9e7dd7eec39f6cda32f1e2148c1f4977ff997f44bbff44bcc6681544cd5f47aeed409d212fd8ffff13fe8b6db6ef39e6f97f86f55acfbf434c78db46bfcbbe5dcbdc18d055f6cb8f5ed765ab425a6fa18598ef1ef1e6be48359c75a218bb1ff87ca98775f39301981f10fefed2258d7b1be5f7bed87e8c61b15086b29c4bfab8befbbab5b288690f20eec8e4881f7a31ffd88da49d6cafb151ff8c0fb696464b8ade3ff539fba81defbdef79c329036ae0dc0a6f5077ff0017ae73bdf49ffdffff7717ae081071bea651f6fb7f8b7fb0fc55168df868f020cdf2a39fbecb3f97d39c67fa88ead9b7d3cdffff3dfffa132e6fd54c7bfaf1ff7bc3ddeac765cdf70f55b69fb7fa88ead9b7d3c8fffe513ff713346758f87cebb93601f0f19ac51bbbebe43c1ebabe3065fa3be420e933589597dbb6db80b40234739116966bedcf97517bcd05842f670bffbc6651f379fedfe42c7edfeb3c4aee76e02ee58dd7ab66eeebcf9e6c6d79e5d3eb478dafa85f471c7e3f6edd32bcb56cdc4631eff79fc2fe7f85fb366356dddbaa5063af2d83665bdd2750c0b9579c01ed577c66d445aef6aa2d0174233b598f1e081bf0dcab2fb8e9d3125babee94b50fd055b6c7d4f5cfd230d8030ac327a0c24c402bb70ff69df35d0003ffcd7a38c0b31a7e78b74aa2dd7c60cf7c860d3019865660637d1123d2ef41da5a917d5dcd40010f8d637d04f9db24fa0273abbbba8abb38b900e6e70b09f53cd1d38b89f0e1f3e429353d3406e51a1549437ea6678cc00a5948a4566694a34180840a04a458159386da0fc6e4054956a8514208b38555d797e9e59860a26b55842da761a2c2214280d634e412e114f1a9d0a611b703a4852ec3ffc51308848137529fd913a10a031791c6903c17805d019c0576803e0a6f18971eae90538aac8a02a66c592ca0208d5c920366276aa4a799e015063c7c66842de5801c80a60ac41390f38373b3bcdcc47515ce0bea7a5ddf10e9b80018b747a37058a49183807500cfc7afcf80403df00fe82ed77efde4323c323cc5a8634881b366ee25487434343ac27de0f1e3ca8ed1ff15f82feebbfde46dfbce34e662c1349cc8c5035363ac489201d25dcffe0401f3dfff997525f7f9f9a2f8a14684c28001e740698ef8a2b5f442f7ac98b68f3995ba8a3ab5bea27a85c1674dff7efa70fffd9c7e8231ffe0b7af289673478b04042a7f6336c53226579d2aaa869abcd25d5ef378299c47ae8b53ffb4afaaddffa4d3a77c7b994c8f1896a992251e1b47f185cb932af62c730eb914899938c8f90815a25e695d458b310bd224ac1584a41f532f1573b58d3563880b28853cea8cf1b366ca2d7bef6952a0e4881e000ccab0a13bb09bfd47aa9d79ec46a5138d701966dcc1a47965666cd330ad481a6ac75cf064be1c5204ad302c792ea1cfe6a18adcc8455f55fb1da62d6c0ac5d46e8f5d980a4ec3d265d2348adbd662fe179d46b73bafea77e41e93a72de8e73528070beff37deeb4fa7e4d7ffe1ebff564a7efddf3ed2c83eed74fdff6bbff676dab16307b59b8015144c5878a0e57b60d88ef1df2876dcb52dd45f689c78e08907f17ffee77fce2c167ff1177fc140acd32df823820f7de84329136c3bc67f2bd603d3e562efffa732fe7dd73a6e3d5bb713d9ff9bd1eb74c9728cff50bb44cdefffedb26fb66aff6f66beb66fdf2e7fd7bd96da4db0fe631d7ec31bde40f7dd775f7abc5de3ff545dff03e4f3dbbffddbbcdfb59be0ded29bdef42606bfb473fc03b40700dfa964c8b405e98affeaaf3ecea921717d0230924f2f57df768c7f5f4c98e3a139c23db93ffdd30f3103562b64e7ce4b3341a34b39feb3c6e3f6bd9cafff43ed1235bffffbead87d34d35728067dc7574afcbb6d677df78dcb3e6e3edbfd858edbfd67491eff0b3fdbe5f2f86fdff88fdd4abe0eb282cdedf84482c767c846df7dfd349a0cd778a120b5bfbbe3c8326a96642d6a217bf9c6d668a10cf5e50b06fbb86f2174ebdb6d3733f7ee626397c9d2bb99c00ae917d22ba49f5dc71cb33724b7ad90ed6c3d1af97f963d4281ef9b6f9f6d433abbc7ecfabeb279fce7f1efd6b7db5e6af18f9b08cf79ce399c0e4d17a8b76114a5e00006efe0b8d1c32a63525131b38ed586614861c092593f9442691f8639c51e6fa4ebc5ba7f6e43b3bcd863e207fc3867dad56da7633565d297d65ba8b457b1c5b862f456608628051dd81603c8a2d4d14905a16d8aa2c22a98e85740a00b52d5a5e38dea7b60f008d5fb37989b06060658ff9e9e5e061b1d3d7a941e7ffc094e85572c76322bcf405f1f3df6c823049405c058607ae29477f2e61d988ff019f38cd484483f07e00de66b766696fb999b5729fb301f484b37373fabe61ce9c534c002ac4f82923a7f82f9aa9544cf85b64974627e1a92da2c28500b03474c6a484af8e6879a073511d353331a6456e0747e6566a99ae7946b481dc96c56d3933cd79d9d1d3439314983d2b69393537474f4184d4d4f31500bb603d3d8b83c6fd29f4d4c4da9948202a0ad59661b03b3d6e4f40cd7875e481189748e1313130cb082bde7cb15dabd772f1d3e7284d347eedeb39b19b7caf2f8ead56be4f77d723e47e5c3ae29da27cbeddeb397c16418fa4179d30c7f650910ca5ed9f6238f3c467ffff7ffc8cc582a071c5e911e7ea4d37516a9582a70ba4480ef2ebaf8223a6bfb36fe8cf92a4bbde1c7c3f2a6cd85179e4f575ffd5374d55557d3b9e79d4bddbdbdb29d988182d352d7af7ce92bf47bffedbfd33f7efa9fe9e89171e97b30b5ee2f32ef9432c499cf2a4eebd3129ad865a01e55a9a323a297bdfc2afac01ffcbea250e735046b4185416bb0ad6006ac98db344c6c8a592a4ed71206235593dabaa43da75a56be61c058e9f58b05ae52c7520deb7caf06ccd22deaaf0030767516e9e77ef635b469f3260524e5a88d28d6290853505adabe5efb89d2b5cde869eb4664ed73719c6a65d641b3eea66ba4a96306e1ac8fdc9466bd62db5744ba6e997685d14b83f252cd02fbac01cfaab914a9710c7036b18e731f5867b1fe986b481cd3fb41a4cb9879e275468f1d40d7f39e7336ef51757659c1fb7f2b25bffe6feefa7f3125bffef7c77fab6365295cff635d7df7bbdfdd568c1f464c0abcd7bdee75f4f5af7f9dc1f490a510ffcdec01bef3aea01d5c6782150c2c132f7ff9cbd9264f3cf1c46961bc0a09fafbc217bec09fdb31fe1bd9f17488e972a9fffe0fe967d731c74e64ff6f95846cbb54e33fd476e8bbaf9f56ef855972baf6ff66fb72affff19be3377ee337db2e3d2f047b22c057ef7def7b3925a101ba6449abe2dfe7838dfcdfee0f63fbc77ffc477adbdbde465ffce217356b7b7bc915575cc9fb723bc7bf02ee5d7bdaed87f6719d80eb93b7bef56df4877ff887fcbd55d7ffcdf6f56c7fffe3faecfffc9fff4337de7843cb7c74edda35f21e4bc17b6ea9c6bf7b5e2ca1dfff4b6dff5f8ebfff9bedebd9c67f7efd5f7fded4cbe33f8fff66fa6e14ff45df493339b653b9816c2b101a84fbddd76ea87f9fb221099df3e9e62b1f2a97258d16c3136d2724be6069b6aea9dfa8bd46f37122df43e7ec3e439fb3daca9acbacc5246bae1b8dc50dd290fffbeabae7437db8c7b3f43d11ddddfa211bf9f4c8e3bfb1e4f1efff1e3ad7caf8bff8928ba8bbe773fc03921f923b69b0389d9b101614862ba76d01c4c3fd586c26a62c334e5917c786f50aa016a17528386c5242f765189b487f26770cf2159bb601ced2e5f9e1bfc5ae95d8e38feae38cdb16a2564fb7a500448594b94bb1195559d7aeae0e35ae055c358da5522d4b3bcfa686542c3989f9aaec4db539439a3f004f00820253d3da35abe8c08183f246618901435bb70e725abc52b183d3032285dee0e0004d4fc5d4d1d9c5e7124ea137cbfda28d356bd732b315801fdddd9dfc202e65198bd9400c3289a2127f8f48316561ecd025967651600a3d0f82d41c82688c743aba84cd770a4468404bc2ac5dc548a516c4fc00ac068013c6519e2b730a45f8154059288f7353004649dde6cbf3542e43296903d0f82733cc34d5d7dfcbac57b88181b11c39324a9b376fe2148514cdb32d63cd24061bcc694630b083cdcecc70eac3c181419a9e56690c8dad61b392bc99fbcc337b6878688036ac5b47f3b2df52a94383aad6d1ccf494d4a9cc2c7460c8c2cd9c21f9301400bd0d1bd6d353bb76c9f95c4d8f3df6040d8f8cd0fe7d07e8a6cf7c960e1d3cc25ec22161fb0c7f8a18bc049f2a75c6f246f2f9f4533ff5221a1a1a947a0c32a86cd5aa615ab37a35f7db3bd0c7ae5e282ad015807448aff8e31f3f489fbbe556faf217fe950e1c3cac808b1ab52384062d0a0da63161c031c691cbeff55042753a2e54f9e8e0601fbdf14dbf40ef7ad77fa60d48f308701d319517d7873d501ff308df32cc5671312693aa0f6252291ab62bd620566514802952b1ac19b0386a8548f553cb4b0d241535011c5465ab74d14517d0cb5f7695bc01f7f7242a6690a459f314f35495d724aa67b1e233664d147a1d517613a94da3ba758df4da5a4dacb542584ba29e0fa360624ee270457fe6588ed8d64a4f55d9661c14c6def6ba0a802cc6e163fd336bab1069c8173450569b4389d63bb28f11a5e058c38465d679bc23f676ee7c2e6549beff2f9ee4d7ffe1ebff56497efd1f35f51b6bb164a95dff83ede3a1871ea2bffeebbfa67614b03c0124f69297bc84def18e7730db04fe18c1c872fcfd8feb40b080615e6ebae9264ebb8407a9ad14e8f1b297bd8cb66ddbd654f9c58eff5648fafbc7d127eb7be85c2b7fff87da69349666f6ff56c9528effc5baffb714e464f7ff66ea9afaee77fcfeffcddffc4dfa8ddff80d5e8bdb4d006c41eab7ebaefbdf9caef7eaabafa64d9b36b565fc9fe8f5ffe1c387197005f0d5e38f3fde96c02b0840eb607c32e0f5768d7fb032dd70c3a76831e5eebbefe2d72db7dc42575d7595fc0dbf93dfb76cd942272aed7cff1f20c14f7ef293f4bffed7ffa256cac5175f2cef65f678cf2db5f8cfda77433a3473ad94efff0b25eb9ab1997259b2d8d7ffad88ff50fdacefa173cbf5fa3f8fff3cfe9f8d146d07720dee76e69346937132d2a88d66fa0939c6891c6f66dc2732e613095c5f99536ddf5080359293d1c3ede344db389985bed1c2e3f3fd66da0d95b1c517a0be85e544fcceb790d9c7ed3e22cfa611eacb3d9fc77fe37ef2f85f3af17ff6d967d1ea5523f226ff780a12e032911ff56ec001f82b6c93768acf518dfc499863baadd839ced9e334d0ab60a5ae92ff4b014f91064531fb8b7e381f5b6da02cf7a9dbc1e75803168469dfd2cfe8c8e38b15b08921093a6d17695697c894b36c18ebf4620027705a3bad4bb362c607204fb552add387525bd935845249d6e92815a9a7bb9b00001b1d1d6390d18cbc310840cda14307a9af7f15979f9c9ca4f5ebd7f3c39a1e4e55d8c12013806e00be1a1e1e66e62cdc603c3a7694d6ae5b27cb8eaaf48652939e9e1e065e81350a602380bc18f454547f19caf3ad539a019885b47c71a15301c498d106cd00081371aab3671f89a929d8fec55251cf65c2c0a7c9992995f60d8c4e0505fa00ab54c229ff148804a02cd441ba3e55aea07d8c18a8353d31c97aa2dc0cc0696bd6d0b163c7b84f216d78747454da7988e70880ab751b3632bb15405615a43a94758aa50e9e1fa42ddcb871133f3ceb933744a6e47c9cff9cf3e8e0c10372dea49dbb3a39c5e3d0d000b38f295facd2ba756b759f827afbfa6962723f1d39729859c8f6ed3bc8ed3ff9c453f44ffff8cff4d8634f493745babb92060c6a2012bc5168df2e2824d273ce7f8ebc41fb7f491dce953768ba19cc85947f9d9d259ec7584f50c22c4cc4a0b3471e79826ebdf50bf4a52f7d95763df58c1c739990fa528104c17e166bb62793becedab3c900c1e2f44814d74f24603a1bd6afa537fde29be8fffa8f6fa18d1bd6c9f99a67e6b4b810710ac24803804cd342b34919800e18b100b852b18ff543830101781235f0650a24d2e01fc5764796be35c0156ccac722d8501dc74b787d5981c1d6ac1ea6d7bfe11a69af2fd1914363756584a19832b0a448f5c7b8a898345b965a77cc1aaac903957f526d3dab526d4db619abc800ca182058d573aad8b77462566b4d319f8566901329f0c9ac4ddc765c9bb0481943ed03a615f7bac7cc09dad1fb44ac0158a86f520e26baaf744f510d70fcc651b4604f82165bb79e216fda6ea490acc4fdff548cef54497efd5f5f66b125d46f7efddf3a31d7ca4be5fa1fd775bff88bbf489ffdec674f5b8a9a672bb89e0503d337bff92dbae28acbe9b77eebb7bc0c25ad8cff4671d0a89c11a419bceebaebe8dffeeddfd2b47fed208f3cf208dd76db6df4f6b7bf3d3dd66ef1bfd812ea76a9fdfeaffb7d1c9883d0bee06b579da39688bdfe2ec5f86f464e666f6fa52cb5fd1f6d00d4f49ad7bc866ebef9666a470130097bc5b5d75ecb4097b7bf1d40acab52b6605b5a11ffbe32b6d8b104c11e7ffbedb7d3dffccddfd0030f3cd0b6c02b233ff7733f27af45aee0cfed1cff0008b5ca96b89efccc673ec3af0b2eb8905ef7ba9fa32baf7c11eddcf93c6ff9768a7fbb3d9fffc3a67ffee71f65705b2b7d15f76c712ddc6efbbfaf8c2d6efcdbfdd9f596caefffa5b6ffe7bfff1beb9115ff59925ffffbcbd892c7ffc9cb4a8aff6233866da6035f708502aed980cfaa1f2aeb73fc668ce573a4935d2c9a919399b4ac05c227aefd4c9d2c1b35d3dec906886ffe7cfa9d68bbcdeae65b8cb3fab5cf9dccfc672dfecfa68e1b47a13a217b358a25f7781eff79fcdbed2db5f85fbf7e1d9d7bee767943e729fe8e87e6055356285054c166a2d2ed25cc5a13d7c519caa6e027d3afae8777f3c03ed28c4af88c54630ad843757d1a4d138bcd8acc18b53d120d8a32cc29cc381329c080e0beaab24fc5d853c32208c5b862600c9a6126d275f42c10c0318002808d27ed537e86aedc96668b129aa5294b00d840da348023006a2293de2c36bd2951a08f2af749a4fac54db5fdfb0fd2e0602f4d4c4ed1405f2f154b25fee1dfddd3abf58ba9b7a78b41405015a90901a42a4afda76766987d093fd6d7afdfc8c020302a013c8494861db22dccc1cccc34f705262da4254c2a31a73f44dabed9725996eba0520129f5cacc2ec576102aada328caf1090dbe61b0ce89c0d3b20536a84a9b54ca0a58c5e0b9aa02cb411708a7d0142aed5d67573783d10ac50295e72bd4d50d9098603fc03b98a90a60f591f58a9c76adcae02ad87062629cfaa5ed5006761b1a1c62df9a919f7ba5dd91261000ba1e696bb06b01bc8676c15a5528469c4a10202b3096f54a9d0090831e237dc30c9a83ad379cb1851e7fe227cc828539feeef77e40973eef224e5178f0e07e5ab76e1deddbb757d6534c6b870f1ea4bffbbbbfa7071f7844fa7a87664f8ad8deda42d28d048398540c118dac1ea23ffde007e8452fba52ea082257863cea57c4f6ab709abf2aeddb7f841e7af851fadabfdd4eb77ffd1bf4f4d3cf487fa9aa3e4881de12d2ac4991027da9b8d66c67224efd3f49fb89c85d6ae057975e7631bdeb5dff855e78f90b6964649092f23ceb8e9a4819a818cd6acc572a6e4dcc2b5fa830c397f2f91ad59a62aaabc5a250cc7cb1b0ca0ac6abc16c0ac8a9fe27c8fe31a5ea0a0bdb9046a850c03611abb611833b9ff75c6618fbfc2d5f55716dea080552a42456f11129e09360f095e6e28a239dce0f8c6a35d62eac5f06dc9a58fbb6206b3f206b4f204ad74f88024011cfb1e92d4ed73fcc6321ad8b77036c33fb4355331af2bceb753cb6d6d69aad6ac0b8549f284a59c95250960a62758c7d8952b096a9c39a592c872873c1f9cf913e324259b2d2f6ff7692fcfa9f32cb2ca6e4d7ffed33274bedfa1fac52481d03862993eaaf1d657cfc387df5ab5fe517d2ffbcfbddefe287a0ee43e7768cffd0ef7f80f9c152f1c31ffe90593fda1504876b821b6eb8810158ed1aff8b2dee96bc547fff87ea1b1f359f7de5cd39f77c2b2f579652fcfb7471cf9d489b217d5a79fd78327d3fdbfdbf9936b2aeff71af042c58f7ddf703dab5eb296a57017009fb075e6019baf4d24be99a6baea1eddbb7d7955becf86f46501f7fac06b0f1273ef189b6ddfb5cb9eaaaabe9d77ffdd71bae11b6b422fe6fbdf5564e59d90ef2e0830ff00b72e18517d2e5975fcee9a5d7ae5deb65c66a75fc877e231d387080bef6b5af3130f37bdffb1eb55afafafa68ebd6adfcb99df6ff6624b42f3fdb3af9fe9ffffe6fa68d9389ff66da3b917e7dfa86e6c63dd76cbbcdea96c7ffc23ef2f8a7056d2e66fc177d1ddac75cc932b4db4e338b876fb27c751a0548969e8dda088dbb99a07e360b46b33a9d4cdb2127743f376acb9cf7d50bd5f7055f56fd46e742ed66d5f7f519aad7a8cd138905b7dd46e2fa5ea376b374f4e9ec5bb8428b631eff79fc87daf3d50bd56fb7f8efe9e9a64b2eb990bef2e57f530fdb4d0a2903b642bb9a61858f0bf5d01c2c44a85fd50fef71dc80a4c04463984d0c8b153fbcb7d851200076554c3a31fe9ffe9cfabd1e976650b153214218d460c668c6463a7da1668431fa92a80106f04001dfc1b693325f1910163367a141052c419f2aad9bfa6e522632338ec6b408ca5acb22526681ad34d040443595a22865c7893528234d5f5688e9d8b1319a9b9b910fc72266772a97e71550a3a393d97f8a05c12c4968e7f8f804ff281f3b7e8c015385b853ce9da0aeee5e66812a16152b164012000df5f7f732500be3060b14fa836e5df2a1566a23b04ff13c57b5ce09a9a95400acb85850e02b9e1bc1763a75f457ca57015289e3384d4d37cb7f7556f3230693f5f652893a686e5efd451ae671523e5084de18f7d1a30769647884a6a7671914353d3dcdec608f3efa183f841c3bbe9f9990e601a4892a0cd69a9f9d27430a542c9498110bc09b42b14ff6d74f878fc8ef8532f7417309b789b48f600fc3c3b5bdfbf7d345175cc000b8e363c73945e4bf7eed0ebaf2cacb69dffec3cc7ed529cb3ebd7b1fdb746eaec2c0afdede017a461e837cfad39fa5471f7992e7177660e62baa3200aab68ec109016424a6237fe31b7e812ebff28554ea2a69cf3469fc14a066be4274ece8387de7eeefc81ba0b7d3f7eff921ed7afa199a9e9cd531161b18938234c1d9854ee92728cd3888b916a296724f28f42517e3d8540e2efdb0402f79c915f4ebbff94e39f617c87352d16a997d0ce18431d8eb8b6a4bbd479a5b8b01910c642c701fec8bbcbe248c634c99f322057e4259b40fc638f55db3301169fc23d62913a1c6d7ec6f6a8c31e3ae22b6210a540c384cead3d7d3412f7ef10be9b6af7d936f86038c475a77f45f88355b1758bc22592f8ad3749f05cdc80590981a835a67130becaad27eeaf47d663d6560598d818c97379cb780543ce3b15aa04c5a51559a5be67eb116c53a15a20de8aa0354696ba0cda26e3fb1f600b35e9a3abeeb3506f4ea7dc11839b2c05905bd470835e834ce2fbae87c6668f1ad07f9fedf7ac9afffdb674ef2eb7f7ffd7690a5f0fbffca2bafa45ffee55f66f6a5a5204871f3e4934ff043e757bef2550dd93fdaedf73f1e367ffad39fe6f48278788e7436ed2e480fb554e2ff748ba8e1c697fcefff509966db6ca7bd30b4a7b55bfcbbba66e91d9a834673d78c5fb552dafdfe1fee09fceeeffe0ebdfffdef67e6f076973beeb8835f9ffbdce7e855af7a35fdfeefffb7f45c2be23feb3a7d7e7e9ed99900127af8e187d5bda62522007e23e5a34fda25fec1ee0e80503b328981e10c2f00e9376fde4c6f7ad39b1838688306db21feddf2882d80d0bff5ad6fb5cd1f2a6cddba95d72948bbedff2bf1f7ff52d9ff1bb5172ab754aeff4f47fc67f5e5ab17aaeff3afacfa8dce85dacdaa9fc77f1eff4b21fe8b591dfbce35e3fc8d3a77eb862635d4b65bdff7bd9193b8e55d6337e380bee35941e7b363a34009f57ba2ba85ea98e3be3937e7ec713572ec661ddf278d82c1d5f544e72824cdf8986f5e43f3e1f3255f3f5973fa6cc79065275fdb79fce7f1bf9ce3ff8a2b5f485dddddccf413a986f8b879a0cf40070ddee03634438b618ec28b1fc00b05be429d44d41ee0170c6b1429d6216e1b400280a40032d0ac2bb101410961b1661552ee1e7942e94f943251e17bfaf0df8001a21a68cca4c112662c5aaba810ebb4890a51c2e9125197416209158a31831f9036b0081d22857488230fbb53836b49069428f5d3b291ee57b146b1f1d2c600b411a480167dfd7d34343c246d54a6822850774f0f757476d1f4d4a4d4a59b0e1d3cc0e31c1a1ea1e36313ccc4b37ddb5640b8f84657a90329132b3439392f1f52755357570fdfa0e9eeee6186b04ab9cc29f15096013e05419d25a4ee8b9915abb7b78f84fe07fb017897ce5764e6dfc42429b00b9d1a61dc8f66f04974da48c50e84f4395d34333dc30fde000082ae60eeeaecec6043cfce95393521e61fa035e84de91a1631006d6e7e94b69e79263f102bc8f3f3e539f6cb1939e7030383f4e4c1c76970a08fe7edc0a183d451ea64962cf8d8d8d818ad1a594563c7c768cdea35dc3f18c3060707f81ce02de79e73363db56b17f57475710acbe9d9393a63eb363a3a3a26752ad30f7f743fed38e72cbaff818769e386f5b2ee101d3c749881586083fadbbfb981eeffe183dae78b3c5fc6d026519d61a52a9662eaedeea25ff90fff9edef77bbfc376a9ccce4b2f88393523f40393da938f3d46dfbeeb3b74efbd3fa4071f7c58da708eaa15a1d2e2c5511d764ef9a8025129a6298866424af38a920246c57a15d0f6c57f003f010088f482dbcedaca7ff168fa11d5488390127e31c3592156001c4e7ba8ba66562d06fe44fa1c31288cff4b14004b44510d6c098fa91a605681d9ddf0b9181735802b4a99b24494ae06210fa47a6faea50c0420ac43dafc652ffd69baf1c67fa2fbee7d80e701be9888a86ead32d464662d5231ae720ed6d8a96a00259e5f0d0c157a5d62609cb57fc6dace381f1ba096d0205829f0372e2f281db36a5ec771ac5236a637bfedfdd3b582a8a5a12d68b62d7b9f31ebb00d0073db12a9cea480b67aec8955ce00e47a7b7be59ef4fc7cfff7e8d96e12baae5b49d7ffad90667f3ff8be2ff7ebff7692a570fd8f6b85f7bce73df29ae0deb6614d6824768a1b5cab810d0b0ff3b66cd9bc2045e1e98e7f9f4f1a417a69bc60db279e78821f3ce27da90900e63ff9c94ff841693bc57fabe2de0c6531f7ffd315ff59d2a86ca3795d4c09e9d1caf87f3673e0eae7c6513373d30eb2d8fbff89ae4da13aaf7ce52be9073ff8c192012643909af0affeeae374ebadb730b0054c43dbb79f4d679fad002e8b15ffee7c60cfc3b5c557bef2154e678b7b504b49060707990172e7ce9d6d1fff5353d3f4e8a38f523bcbeeddbbf905003a04607a5cb75d7ae9650cccbae082f35b1effb8e601f0eaa69b6ee21499ed2648930a69c7fd3f341fcbf1f7ff52d9ff1bedafcbe5f7ffa98a7f73dc37e7e69c3daefcfadf5f2f8f7f0ab6d748ef951cff45b780af82eb48a681d0c09a317e6852b302cb57d7d631cbd8cdb667be87ce9d681035eacfb7b8b98b4db312b25d338e149ae31369a3993edd73ee429fd5763373699f6f36887d7a36da649aa9ebfaa6ab4f482f9f7f348a0b77de9e6d1baefebe3ef2f8cfe37fa9c4ffba756b68c78eb3e4cda607eaebebcffca01c7deaf2855289ebbb0fdc85d5078e269a75a6aac1549014986598b6f467f3509ecc31a25ada416bcc91eec74e5965eb6b800848032784ae67db2dd6a9d34c7aad5831db90f127fe4c0cbc023e03ac353aab1af7cfe9eeecc19ea0f05aa76d14334cca1d42cce01ae885147100b1407aba7b1834d4d9d149e5f979dab06e1d8dc91b04d0070fd02627a769f5ea35cc6806262680c8060606e4f1711a1e1a628015529dcdcdce48db94d2b10250512815f167890c8e03c35325a9f0f800ca433b55cdc6433a2d22dbc7a4a04c017235209b465bd0b396443396898a620d8a1508880160f21fd8a638d562a1443333736c07009b0024eb94e3e8ebeda1f18949b625fc616a7a860167c9ec2cdb10c0ac44de0804506b66669699bfe01f7373653a7ce428f50f0c6140722809db10690207e5c3bea9a949ea9576debf7f1fdbf8d8b1a3ccd673f0f011eaecea64b6b1f189095a57584d862e6a48d6431ac2ad676ca4471edb45fd7d3db469e306dabbff30ad59bd9ad3298e8e8ec9f9eca2a7773d49fff22f9fa7471e7e9ced0c60d7bcd455183f2413632a851d5c746868845ef18aabe90d6f783dcdceced103f73f24f53a4e47478fd2638f3e2e1f9e3d454f3cfea47c7f9a41639532e244a58b2ce858346c768d668ef550f82fe5b3b1d9c349f9880667017cc55042194b9fbde916a9c763f4ef7fe5cdf482173c9fce3863b36288625f33cc472ad6389da230290e553f485508d092880cb35e6d0d3329f2b81cc5263b28b2edf1f810c74835a9f20fc6698c375a2b7d82a8851e85480135b76cde286f84bd4a3e60bd5ffa5149ae1b4283b012ad9f5ecf4cdcb34ab14a7d6ad6239ccbd82bd2f5549717ce7bca421845e99a2b340b20997a66ee48af41a6dd0c1b78f7f34401decc355ba4d7ec42a1e0df8fa3a89e51cbe84894a63f24b3b7ab4ee8c20bcfa59191e17cff17fe1b3d8b2dcd5e7b98f32159ced7ffed22f9f57f7bc852bafe8700f4faee77bf9b7efbb77f5b5e878cd25212959ef02bf49defdccdd7633b76ec90d7182fa017bff8c59ce2067b13c697a6353e0df13f2bafed70dd8d07cb60b4c2c3e63befbc931f3cc39efbf6ed5b524c1f3e0190bfdde2bf757ba2fd7971f6ffacb69f6dfc372bcf66ff3fddd2cafddfed23d46e561f6edba6ad90ee8deab6c3358a6b17df71b7fce9d8ff43b6cb8a5ddc5f78e73bdf49dff8c637e8b1c71ea3a52406e002e0c86a797f012c976f7ffbaf314019f7494e77fc63af9b9c9c644018405760bb5aca7b20c041f005c3cadccef18ffb13b0f55212c3e006df34a90901763bfffcf3194808b67adc0bc579fb3aae59c98a7f5cb3e13e04fc15d76e4f3df514c73c58c470fdd68ea9b90152fb999ff999f4fb52d8ff97f3efff76dfff57faefff93d9ffdd32a16b9966da68a64ff7dc72bbfecfe33f8f7f5f3ba13a455fa55027be41f80ce65b449a591442f5437a8402cdae6fd70bf5eb96b703e6d92c0a8dc61cb2ab4f9f4672a28b4db38b584857df6641941d40215b366b3bb7cf46722265dd7aae642d6059c742e36c76c1b4bf67f5e7d3bb998dc4f5bf3cfef3f86f46d7a516ff0303fd0c8af8f1038f32634c5a06ba9877a15349e9cf693fd0453849f87cfe2d6a6009d22ff4958ec5026419801224d1ac56dc87edd3013bd6eb22b81f570f7b9e0c030d8fd1e86940201aeda2fa45bd985fcc9245a7e046926a7ee1e14831f8004902d01140454879d6d3ddcde908017a42aa3a6639923644dac1f18929f9decb3788005641aab6e363a3343c3ccc29147b7a14800be9f7c09a54adcc51b1ab93924a95caf23886887393931334d03740e56a856f7480c61c372271e36c6e7e96ed891b210c98a198db06b829d2e02ca15030a744783a6a94619a0d2b66c00fd8b0e01b00a921dd22c61d69d017fe0290a76c5ed9776e6e96e6e56b70689866a41d2306e729d0ca841c6f55335e95643bd3d35384749160baea97c7709e349866d5c86a066be1860ceae24611ec13c545699b0a6ddeb89ec68e1da3f5ebd64a9b7572fa48a4042c4b1dbbba0a9c0af1f0a1237cac524d749a4ea255ab461824333a7a8cbef2e5afd26db7dd41fbf71f4981562a0d5d0dd84486f98a480379621a1cece707647ff77f3e45c78f8fd3fe7d07651b07d84f26c62719fc459aeb29d2feac407e8253d171aa3caa810d9b923470cc1aabc6934e18ffa7804a6067bbebdbdf9537b29fa0cb2ebb94defc8bafe7548c78600aaf51384813a36070aaa673cf292e53ee3a3566800801b0829fe205bf50e023e6e852a0510626e9747dac63c2290539ee75da443a6124a5f241b55644cc8a76f5cf5c4d1fffabff45e36393a42609ede25d81cb482f2b699ad39ad9140089027b8e3906bbe8b47d662d4ef707aadfdfecb5d300b16acd19456aa0ae7454be7d4397c139931696190e458d698bd9b934239e4f22ad7faa23515ad7ec29062c86b5acd451a2abaefae97a9deb54caf7ffc59646f65ee9d7ffed20f9f57feb65a95eff43704d07c60f3c7cfad8c73ec60fa3969a80b1002f3c78feb77ffb373eb675eb36dab0613d6ddb86f70d74de79e7d190bc0edcb1e35c4e096dcb89c4ff9e3d7bf8faeae9a777c9f7fd6cb7a79eda45070eece7ef4b8de1a319d9b56b97b4df8eb68aff5689adc252fffd7f22d2dcfedfbaf969c5feeff353d39e5bc7f58b467a84da6ca4afed8bad942cdb647d6f97fb7ff88dffe10f7f98def18e7730ebe25213b327020805101418b15ef7ba6be89c73ce6660d6894a33f10f10cb2db7dcc2cc41607e5cea0230d07bdffb5ebedfb614e21fd76ed8ab97a2e07e9a010fde75d75d7c0c760718abb3b38baf3f464646e8dc73cf95b1b98ebffbd24fdbe2b31740f1783dfae86374df7df7726c83350c80ab764cdde80a520f9af86dbffd7fe1b946e55d5f76f50c5d43b9e3c8f7fffcf7bf2bcf66ff6fe67c7efd9fc77f1effa73efe8b59957cc60b29e13342563bae22cd4c901bfcbefe7de5b326bbd9fe4dfb59fdd8e79b59881a3961a332a13a468f66e7cf77ce67e35079d7594f64213899f185e63bab4eb3d268816d14686e39dfa2da684e1af95868d1f3d569b4789e683f79fce7f1efeba3dde31f2c4a3b2f7d2efdc3a73f4b931313757a4312fbddb683615991dff1101eef06f800e17480fa9d1f48a02efe7a49a894807c0e6d09f580df306d590ad6cee97afcb05f359e0213846e8fcb5b3ab06d7553b12907885022d2d48768af96064cb5abd2baa9f48411914e7b4654901f624e5d284e0abc81fed55f002a3b320b8d1a281fb3ed1d73fabc847519ecef57e9f1a4deb06355cc31386afcf8b87c9834c8375c56af5a4b070f1ea2356b56534596edec28e91b090a2c0116253c64530c5831150b3183bb646bdc56ac59a03a01648a1438ae12450c6e02c809ede006889a1ff88002eb283005fc25d1202ccd8474827b0b8b655295f29098a98c19af18bc1233be65767e8ecf810d0c54482a3d66c429e660c3befe5e0696914935089094ac805430789f07a8acbb8b75c4c3b80ed90e1895468f1ee131ce95ab9ce2706c6c944153dcb3b4118057b3b3d3b2ce10b759ae54d866038343343d35ca003900dda6e531308e75f77453af3c3626e7094c5c8383c33c96ddfb9ea14d9b365279be4c1b376e90313749cfecde4b5ffae297e9bbdfbd8726e47c0240a7dd84416ea4289bc81c542c423520cdd3bbf6d0dedd07d86755f99859dce033c4f11c5b6b037b3521240dc830e654982ade78e6ead6c544cd879ed7149828b43a7ada58bb342410ab440628065f4eca4407f71fa12f7ff15fe9873ffc11bdfc153f433ffb73afa6175cf63c69bb1283dbcad2f68833e80d4020fa4e12bdeec491b516252aa5a04e0bc8404d4e89595b07ab0cde4c58278edbbabd582bcbb16fed070dd06722f57d058a433ac5f3769c45cfbf6ca77ce8fb0d5e2b12a1da465cb1eea973eb3d1036d4ac7f368029d1f6175ac1740f915fab7a4d4d53a99a7242c5b7599b4dbd14b49aea4db5c911b5f5c7accfa4d77bb3662b7bd66c51b018b7382cf57a2d2c26c2bafd2e3532a52c5f460f61f565d8b9cc9eb17ad5085d72c945f9fe4f277e7d7eba25bffecfee6731c5b5437efddf7af1ddd85a6abfffdff296b7d0238f3cc20f4f9783ecdaf514bf4caa1bfcb102f6655ca71976d96ddbb6a5c7366cd8c8c7ccbcecddbb8753b34370fd68d8c1f8faaf5c4e59af963abb5533823f1c80e4f15ffff366a9fffe6f569addffa3e814fd05ce49482bf7ffa574ffaf15b254efff8181e7831ffc208370962230d908805837dc700303b1aeb8e24a7ad7bbdec5a9de1a01588c348aff0979eff0e69b6fa64f7ffad3f4f8e38f2f09204b23816ddefffef7738a635bda39fe979bc08fccf5db1d77dcceefb88ec37d6bfc7128aee3f0079800cac10eb886eb96f7dd8c6df7eddbcbd7706807c07988b966c3f5db528c69c42ed2621a69a7fd3fd447e4f9dd6febdc4c4cb5f3efffa5b4ff37bb872e97dfffcf66ff77cfb5e3fdbf3cfe69415bbef3be728df46eb69f3cfe4f6dfc2f00606535fe6cca859cfa4427a6d97259fdd9e79bed23a4ab3b21be05a959fd433a86ca66e9e53a5fa8cd66f4f4d9ccedc3e79876f0fbc698d56fd66690d5a7296f9769e45f27b2c09ea8346adb3dde680370eb84ece4ebf3447cd2ede7d996cbe3bfb1fe79fc2faceb7eb6c777a2f17ff6f66db461fd5a7adc00b050c67a70ce7550df8c8b1990ea195c847eafb3af10756907f1d0bea2414f29500ae70c7e40d73369aa00d8121a4c15ab46b92d1b3465fac18b4119404118f617732ed22021dc1c8eacf93263b2ca02549118f48b06f6303e451e2f1591d6ebe419b094fd0d4821d20356d6e5b47e91028a245ae74a25a1f1c9291aecefa5b1f109eaee2c5149da647e7e8e599600008a0b459a9a9de51b0a782804401174c55f88cdcecdf338aad509dab87123cd9260e01558abccdca11ea76c94df3b3b7af8e604a751c33c31204da56a34202b068454144396621222cb1a27bf1fa8b6b5af68600f033e78ae14780e69ed3a3a4a3cced9b959bef902762705c251a9092b48dd28759b989854c03f6953007026278f31a00a2c3b60890233565f6f1fcdc89b30d544a4e010a42eeceeeea4ddbbf730d316fa43fb870e1fe28774b89903400ce205ed832d6bd5ea5574e0c041063f0d0f0d72f96e5916c750bfa7bb8b0e1e3a4843c3ab99f509e028084074f7dfff63fadad7be4e3ffcc1fd722cf31aeca246acc047a9db2b1fd200406d292e9b9431f7b5bc80aa0ce257d9cec4b1607f56c522e5903518a1a88164eae31e8c4f49fa2d055f3973672998ea503b16a583c03cedd9b3976ebcf1d3f483fb7e48bffcefdf4457ff34d2056d621632441fa7b6c4daa1d3181a3010fba38e49930235b2ce1949d2f555f985f2695517fe6fea32e008fe1dc5c135d396381dbc1e6f2ca847facbe557bc80be71c7b7556a473648cc0c5d2289d2948842b3f99975d2a401b4d7b4c49e07ad63410395f01d7a174dbabfa89672d0ac8742d4e6baa21f0aa7f54dbf68477e47ec14a2284d116bea1b9b9af5d9b010b24df1b218ac188865d8b0dcfdddf4a9bf1b50ac299baedba65fd9ce19679ec1a93953b7c9f7ff057a2fb6b8f6f29d3b195929d7ffad949578fddf4a59aad7ff4690aaef031ff8003df4d0c3f4e0830fd07213f3b00d0fa38d806d2197c682344c3e6997f85f4c31435b2ebfff437d34daa3db690fccdabf5ab9ff372a97b51f379a9f46d20e7ba291c5daff4ff5f53fee73bce635afa17beeb9873ef5a94f2d796011f6beaf7ce5cbccfcf3877ff84774cd35af5b50e644e31f7be8273f793ddd70c3d2b78f2daf7ded6bf9e593a510ffcb557ca029b0bcad0401fb15528a4296f2fedfa86df7f852fdfddf6efb7f567ff6f9a5fafbff54efff8df4cb6a2bcaafff8392c7bfff7c1eff4a4ee8cf78428d86822f4b420ee22bd34887660d68deedf2eea2e2eb2fd4af5ddf3dd6eca2e6ead6a87e68616d24596d869cd23eeedaac9939687661f0e9e39ba3acf2ae3d9be9a7d10662daceeaab9134bb806495cf9a6bdfa292a5cb89e8eed3338f7f7fbbeeb13cfedb2ffed7ae5b433b775e5cdb2439b5990dc250e5396d9a6a3865be4179300461c34cd98ae4abaa1fb827fa986ca0ee813d24d6001b006400f021cbae29084b7f4ed4899afda3a8c688a57521cd9263c66c004d661476fa2bd48f2da0966917601ca43d2b0041410638a0eaa7b6a39313f4070008713b8216baa84859c120fc975ae50a4d4d4f31486c6ebecc75916e0ea955f057e96064821d000ec2cdc262a1c8208d79668a8a19900576a6f2fc3cffc557b154a4594e49a7d8bd787c89e0bf2203e0086c5bc56281eb952b6565173962a4ea13549f32edd9ac990d453b980298c43c26189e01636030ab542d9bc60c0603a8a4520133c10cff851cf4ebecece063bd7dbdd43fd0a718b03a3ba9bf5f7e2ecfb3ef827d69fcf8187f3f7cf8301d1d3dce2c58004bc12f61ff35abd7d2c8f008757476717d6675aa56a8a35490f69ca3c1c101f68b43478eb27df6eddbc7be34766c8c0e1c3840403e1d3e7490568d0cb1fd9f7aea69baeebabfe5d7bdf7fe90d99f00764a1203a631001d5ae070caec0058c5a4d1810a5c25d42b056d45f61a460ab6c5e935137e6fc6911978570b76f24d39b71d2f7c09a1d7ac985268188f49ea383f5f61f0d91ffff1b5f4ebbff9bb74e33f7c961e7bec499a9953ec650ae818f1bc1a3d0d8847ad390a1c688084bcd6485d393d25a78d4c98890cfd21262060d55299160503b3ead6df938a6a1937a5885ef2e2cb6964d590d55ec420bb28624eb27a70aa990f9376358ad25481698a5612e9dcd9c0a5d861a2aadb0b4c7d559b3f17e27a6099d0ebaf610d4bd74a5dd6f465af41eeb5915093aac643e44f65a8c761eada0c5a062c6683e0cc9e72c9c517b01dd331e5fb7fcb25bffe5f1a925fffb73e7e96eaf5bf2d48f1f2d77ffd8905ac0fb9ac5c01d304fcc296768bff56c872f9fdffffb3f71e00b21cd5b9f0a9ee99d99dcd7bb374afa42bddab9c2524904009494620210964a28906634cb6447804fbc1c336606c307e841f3fc0c60687e7f04836c1041105c8088390508e3787ddbd1b672774ff7d4ed5e9aea9adeee9d91cea5cb566babbead4a9f09deee9fef69c34fbb2ae8b69d7ffe5721d5dccebff4a7efeb798b298d7fff9beff7fd5ab5e05575c7105ac16c1b46bbffffb6f82d7bffef5f0d0430f359d6b07ff183113c7e6affffa33ab8a7c75f9e59753d4b33cb2dcf08fd76b27ab4f3022dbe597273e68395efff34a9e6bd34af9fdbf92afffabf5f7ff7c5fff79df5c57eefedfde562b71f84fb745ffb4d5590bf8f7d24eb4235983385b3d36491b843c32e3e54aceb679d1e65d78b632edb4d78e5e9ee8d98e492b3bda71f2590bb8ddf591b73f594e3a8f5330e746079ba937ed6294a78d566567d3df762f0c79db72f89f79dce17f75e01fa3295df8c4738944427629c2146e31f742d58da360451b9262289547285fe2d398cb46e47745b012aa3cb78d2ffe9900a0191ab7c369b1e2085bd09c168bcaa9e82fa1d636f0c55bf0cbff849c01da4d16b7ca3a84eaab75cd2061451a2fed419db35856a897082042f648b6d55c268875cb13481c2a7594a85d8ca284c4a8de9e1e2254617a3d24456184a54d9b37d18ff3e96a954827a5e87b3d3a578eeafabea0085718296ad3a68d44c242b215925090a4843a30ea1791afaa352897bba08aa9fbaad3d051eca071a92b22166e14a90b0973482d09162ef58a5c3332cd1b12d1901c45a9e9546a37ec03dac869e828325234479d1d65e8ededa52857f8200a235c614a468cae3439117def2c4329da0a8562b4ded711310d096c18310ca367e178351ab2bff55a9dd6f786f51bc80e8a3a363901935353f0eb5fdf4b3a3ba3f1c2c8114876dbb26533f47475c704978e6281c6abb7af8fe669e3860d30323202b7ddf613f8abbffa147cef7b3f80a143c380590383d05351ab205ea0d2475806078957a11f6f580f496a68a3243fa96b5a134546806452056a6bc484bae681c7e84c5ebc0542e11ee9509e9c8f19e684cd9bde076a374cd206e279d9532469024c8e4fc14f7ff25378df9ffc29bceb0fdf0b5ffcf27fc0be0387a0110a9a2321903cc57e44ae3f49a06a8ef815474fd3708ea7b13c45420b64843c4c4f2914b18b6a05e1ec53080919316fe7ce1db07dfb765a039c0a93a29c85d23ecf936b14db9163297f60849abda1b68f11c0d48e4cefaa5fd39088a8e6494f09c8d7135ff933f6d76a1862bfc67ed7bc46864df319c691b16292ac104dd1ae4c69ba3704d0fc45b31f67bbe43546966b44fb7d7dbd70fe13ce219f94266bfdfabf54e2eeff97fffcb8fbfffc65165356e2efff134e3801defdee7747f7449bc08993f3ce3b6fc6b1e58affc51036d13dffcb7ffd5f2c594ed7ff95f0fc6fa964a53dffc308e2ef7ad7bb60c78e1db05a0409539836f06d6f7b5b9c5e17252ffe91c4f58637bc017ef9cb5faeaa34bc4f7bdad3e0031ff800a527665929f8c7e7801b376e0427ab4f901478cb2dad4981eefd5fb32e77fdcfae6bfbdeaa6df7fecfddffebc71cfedbb36bb67a6cb21af1efa59d4833a655635c66a1277a2e75d3f4e471963670b6d3762ba0d8ca6795b1e96b776ccd7eb4dacf638f09783ed6aea39cadc3ce734c77a6e6f7acba5973d80aa8691899abe3caba30b4e3401cfe1dfe5733fecf3eeb6c38f6d86d122fb201dae825bcbad8722a2cd0fac00f5e427940a60c44b214eb509fc4278024320b96e5e8450d45ae427df8b29e2269a9f3fa678c21ee3f6e22215f85904466d1c95a42d91a28dbb97cac4bd92c8f85c0bc150a128451b4a2e34868aa379a2383b523055f86b38fd7403c36f12cd046a7143104a35a2121018944d3b52a0cf6f753bf30ead5c14387a0582ac0f4d424ecddb3177a7bbaa0a7bb0c3d3ddd303a36019d1d9d71a41f240a75777543bd364d64a452b1406904c7c627a0dc55a690de1d8562f4f0a69388234814231257bd2623f9a8791220894e4245f809c310668cc6ec5d5193c8210a8928e369a908914c5328fa6aed14225b3d7a9048b60618ad6a92d20422a1038f632ac0ce7227a56bece82c11a12aa8572982d6e4e4845c13c2a772fdd1f896a371eaecec8ada28d2daa2b1ebee266216da82638fe90b31651eae878968ecf6ecd94dba76edde43a4b4fdfb0f44e3dc41691237ac5f1fd5ed88ecaac27ffdecbfe1c31ffa18fcf55fff2ddc7fdf8314f50ad34cd2385220365fa6df53690365c43999f6920737bea641f2c9441f7d69cab473984652ad67222025c90b953aebc00b8a7a25370f126265e2ab3415fc3d4cb6189a5a242c1ff1e461dd24dda30854d4aec08791a151f8ceb76e85b7bde51df09ad7bc093efff97f825d7b0e814c3e8ad1cc8ad15c256b10b96332059f4c49c8c427225761a320f1c6dd65521c45d4e2316a042ad29efd1e21343b666cf2ff758a6a76e113cf97e94fa3ffa85da1a27781240c22de681c55d4bd508d35fb3df65772fcc398842a7c3f213ea995c091fb74fc09d5bfba226c01fb6b3549e45d945e5fe9d3a360d1d8446df138204eb02e93c67824f4885c60bb66e131eddac6e90b79fc517f41a596458588674c85b87dfbb170d2493b73df67adb5ebff5cee83e72aeefe7f6ef7d80b2deefedf5e6639c84ab9ff37f7cf38e30cf8f4a73fed48586b5cfafafae115af78c5b2c4ffd25d13d3af87334aae50fccff6f9df7212f7fc6f79cb4a7bfe77fcf1c747bfddff1a4e3ffd0c584df2a31ffd8888549c9a370ffe1f78e00178d18b5e0477dd7517ac26b9e69aa7c3fbdef73ed8b66d5bd3f195827f7c5675f2c9278393d52737df7c0b45e54771efff5a8bbbfeb7577735fdfe77efff1cfe1dfedbabbb9cf0df560a429be435c8ec581e202eb4b4bb005b9dcfa3cf044ade7ec72f87c2f4707bfa319bfe76c738ab7cd6b9349b66e328f33a8e3cd2ee5867d56b077cb8d92e4869fadab9e8b42a9be67c751d731187fff6f439fccf2c9b756cbef1bf61e37a38fd8c5329ca12eb0f35fb7c26e1084a60265fceabf47d4c5e228b540a422e83e77163a201a52654d4052612e0c5368ee2a26cf29000102624027e91cfe4054f91b8f0e5bd603b1591828852d802bef00f93d457648b6a13cb714a2f221e785e4c7c921cac84784255a31e2189026c5183d247963601a18a8223d47078d008656a4319210628b2186d929d42f60e0f8f10b108edc4e84c070f1f8eea7950ab556173f462ecd0c1c3d4c2d891218a703535311e1ddb1ff5b92153f14d4c426f6f3f0c1d3e14e93a4c1185501a983aaf54829eee2e224014a3ef688724658d49f2851a7b6c0bc7b01ae9a3ec76a12441612a455f235264dd7ccf4a048e7410fb69a1d659b15484a9a969750c88208504179c43b4b7bfbf97520622016b70708008521441acab93227c55a62629352046051b1d1d855a34ae13d1b80d0f0f537a423c7fe8e001a857a7a2fd29a854a6e0f0a183d0d3d303f7dd7f1fd5d9b6f5e8c89606ecdbbb1b86a27a48f8aa4d4fd35ac1b1c1085c3886c71c732c4c4d07d1bc0dc13ffee3bfc1273efe7fe0273ffd79f4a07332b235b2b7e129829127d702a5c50b24814848729260ec00938ffcc467a8dc8042118e842a4761ab68ec90945697a926d5a086a107324da18c46d5a035d81cf54adfe2c9506d804e56527343ea9058a57da2190dd21fca805de837a88faabf01a74b94d1bc90888663323156819ffee876f8e33ffa53f8933ff900fce0873f461a19144a1dd11c17285218eac0286c1c5d49783c4ecd29f724e14ac8b5a47c4ba8d21386a116454bad3513b9388c75f427291ba62a0d1b028a11ae4e3c7127f40d744775ea91ffa9d23c341a68939ff834fe4148be06688e630214129d14113450fb20382daa4a938a64d55046ecf234bfc70459f6b5c28854c5ed8722898cc5c4aa269bc28458c5751157741dd0d31f0ad1940e923793dc8576732a596e4b4f111bd789b082d79e8d1b3724e3efaeffb0dcc4ddff2f0f5968fb56f2fdff52fc5eb2d96393e57cff6f93d34f3f1ddef296b78093b52b1815f998638e5996f85f3a3fed9effb5536f31c53dff5b3ea25f9b4d59a8eb7f9a3e3e66d3dfce186304acb7bef5adab8e988c24ac5b6ffd6e66197d9cfeeffffde75547be4279cd6b7e0f366fde0c7395a5c23ffeb1e625975c4291b09cac1ec128a4679c717abcefdeffa5ebcd53d65dffd36525fffe4fd3c7c76cfaddfb3f877f9b8eb988c37f7bfa4c1ccc9a80652ea856920524db2299eb84cc6611b7b2b1557bed4e4e5a9bb6f3fa673b4e692ece4c771c59628230adce5ce6342fd0b3246d8db5720eadf667d376968ebcfab3d65cbbf86c571cfe679673f85ffef82f97cb70eeb967d10ff878decd76b4e3bc31b10975e8e9a69018e5ab1459f8b25f4f372823d828c215bee017441b917308f2653f92818858a38e05fc325f2317d08b7dd53eaf33793e24a282af8855543f94549750d9187f4242fe428287cf217b382216fe87a9cb288a4d8df6454e0e564c7c004904296154a5507e179c3e4d36419f9228232350a1194818da7f7084882bd3d3758a86355d9980c71edf0d070f0d51036318bdaadc09870feda7fec8145e21457fc2fe0c0f0f416f5f2f0c0cf4c1d0d0b02282f930393945e9f790a08404abb189090a53d445a42c49de4262169223fc4291082f44c6c27945f20bf6492d025a77f3ec4e49b5906924793da22d74263a8673cb04302465f92a1de6d8e8180cf4f5c3aeddfba0de4032540f1c3a7408ba311256f4500aff4210893a1811abd45186ee9e3ea83542188dea850253def9848503070f52a49e6a751a1e7e6c371c3a3c12cd5f078debfdf73f4463b97efd7a6a13c9681831acb3b30c3d91feee680c0f44f3b36bf701f8f297ff03defbbfde07dff8c6b760dffe03c069316955849c920e0936d177dca0011047590ad47e83169d100ddaf07b08982691234ad5698330fac40df5858d782043fa50290ef9d636f6155ababa20c138c456028d376fc97c2010146ea80d45aea10854529527d4b91094055e9c619352da8924e21b45fe0a64bd6abd01c34347e09fffefbfc21bdf784b347e7f0cfff9ad5be1c8e8385596e9eb0ab40e13ff0f2a256612e18a304efd92d1ae303a141140435916b14271c290ec64609a672053d0c72832d5e9a79d02c71e7b8c1aa35092da4091dd9481613ce272231f2914119475727f9834aaf505c97d34deaa3cd5d1a2f7c53e1a37e5934d72ab5c6f09b12ad07dbab291236985ea18cd9322527139b0dc97b08f665f4cfe17b46b94d21347e68a04db41bce0b5474f3fe8aeffcb4fdcfdfff290f9ba5f4c93d574ffbf58d2ca87acc4dfff48d27fee739f0bef79cf7be8bec9c9da9377bce31d44c43365b9e07fa9c43dff6bfd107da9c43dff5b3eb21a9fff5d75d595f0c94f7e124e39e514582d82cf35def7be3f81071f7cb025febffbddefc2c73ef651584d8284ba0f7ff8c370fef9e7c35c64a9f18fbfb99ffdec67c3ba75ebc0c9ea10bcfffaf8c73f6e3db7dcaeff736ddbbdff5bd8ebbf7bff3753ff4a7efee7f03f531cfed365a5e0df6ba7f07c8be95cf4f6e7ba70d811b43311732d339bfaf331d6f3315eedea9d8d639f8b8ded38a5bc62aebfd9cec552d55b68e7bad0e2f0eff09f57e60bff48cc39ebacd3a307111b631206eb8ea39d58d6a3be6179260dc4d14f847c111f4769e172d131243e48a29524560915d58aeca14f759107684a85e86937b3fcd29fea455b4051aa64f906476151ed4a3289242730398b8e6b69b5988ca29316846a67ba5a5369c5da5f9bd8df5267471c13cbba36e419d5271935ababab4cc4af75ebfaa0abdc45766178748c628529f37a7afba339db42fa0606faa1b7af8f8602d3f421310953ea35907c526fd00b3549e01050abd588d084c7b00292932090e40e3c87e9fa283a99ef413dda47bb0a7e41f59ca3dc48bb3102513c5ff3e423918823e2884b5eac7a7aaa4204295a938124a5e089ea74956ce888c6b85aabc3e0401f1dc3e854f830eac08183d4af6ab54a51be701c7b7bca918e3a6cdab8211abbc1e821e4149141a62a5314610ca35e21a9ea98ad5ba032350183ebd647e353a471ed1f1c84c9a969b2a5a76f2032cf8fd6c734ecdd7f00beffc3dbe14b5ffa1abcf7bdef872f7df13f60d7637ba37630329517cf34e804abb0413428b9c6b13b48b452a9fec240ad8b20a917ca7a44bc12927c8575422469619a3f4fe20e494dd1ac51bbfc035568d1ac786db349c415327f10c9094e36d023e379c0d1a5507c4f70b643b99a4315e58a23cba906a9b8f0543425e0405e448c132a3a166360d7e3bbe16f3ffb77f087ef7c37fcf9873e023ff8d14f61727292ce518430c5300b558f0224a0299fe0a9754391d4208cbb862432b49fc9a23c236d0b46dc6ac808771815ed986d5b25b992a6395018d722b881f26ba1242b2149348855794d1bf68ffd56dc9cd0a30826e4d7309e5b397f81f291dc8ede7ea8facc8457391edafd80e65f63e29510cd65b92d7d2884d052852a3b545bf13d07f7012089de15d5d9b869035d7b905017cf87bbfe2f1b71f7ffcdfa57b2acf6fbffa5c2926dedad86dfff4830c75443989607ef879cac1db9e8a28be0f2cb2fcf2cb39c7fff2fa4b8e77fcb4fdcf3bfe52573b97f58eef7ff175e7821fce99f7e9022d3ac16d9b3674ff4dce24b99f8c734859ffffce7613509ded7600ac69b6eba29b3dc4ac13f3eefc214d205fa8345272b59700eafb9e61a8a426a13f7fe6f663d77fdcfd6eddeff2d9c5e77ff3fbb36e7b39ec37fb6ee95807f2fcbc056ecc8b472739d401ebcb94afc42660e0b3d6ffdd94ed07c3896b9cc4596f39c4b9ff2d4cdc32acc237919b8ad64a1e650276ff07e9e7a7ad976db9b8f75e5f0eff03f1b59eef8c7082ee79d7f369185a88edab896d0f4922ea32f42d3ef197d65ca09bfdc8f5fc40b993a8c88528d063710a70c43e1e361d299f8653e186dc6760b8d94a0880cd4ae2209301181fb41ba024572d16e80a83c9290221ba62b538a24d0fefc6394a4eeee72dc3fbbb0fd728c2a5395a8ef325da0e715a05aab424749466142d250676717943b3a20a8d760e3c68d1421a8d108a1b3ab47f1741a94568f22438d4f405f6f0f11cf8e1c1985febe3ea854a6e3873418f9ecc8e8287d2f148b44b6aa60142d2df518916f88d016506a359d5c02eafcec982ce98263e11764ba391cfb42a940ed2379cf57f382a4ab5291d32b06519f3c1a238e96562a1660ddfa75508c3e716d777795a95f13135344e6411217a65aa1a9018fc6b4b35ca6369064d4118d7357771fb5b5f9a8a3225d1ba3b9ec83c1751b229d9d9412f1defb1f829ffcf417f0e94f7f1e3efad14fc2d7bef60db8e79efb22fdd1188a82643785328520d15f909c44cb4d454a92f4ab78a3685142a5280c9bcf312d30945c2c88c73de4f1e7320a230254fa4ba14654408214f66969ab9acb689bb67ec340a6328c3589840cc5358446dae282ec1a24c6e4418a82054c16f3237f12ed373c181f9b82fbef7d003ef57ffe06defef677c147fef75fa96862229e6349c29224ab0094ff507a0b117692feab7b8e46a0226671f5592c5c6c4b48825277f410f7a4934f248226b925ec8908d550257686f160ab0108ed2d735a45a1f934d07cb867f83616cff089dc4eecc33945a0aac7d705f6f5315955f38fb1bf16923065a3a0f2b9b84e54dfd7a20fea24d8780b25c9eba28b2e8cae3ddb66e8cb236bf1fabfd8e2eefff39f5b09b29aefff97f3dcacd4dfff786f78e38d37c27bdffbded417414e5697e00b5c8c08d2d7d737e3dc72c2ff4a92b5f6fc6fb1c53dffb3cb525e1357f3f3bff3cf3f8f7ce4ce9d3b6135083eeff8c217be30e3b83e16bb77ef86db6fbf1d568b601433bcaf79f18b5f4ccfc658563afeb14fab2942db5a95abaeba0a7ef7777fb7e9987bffb77c7fff2fc7ebbf4dcf6afdfd9fa663353dff73f877f89fab9ee58e7f2fab62d620a7959b8de45964ad164a2bfbccc59f47b7d05e502f86b43bc67af93450cfc6f6bcf3de4e195bd934db6c7dc99a3fb3dc62cd972eade64ebf28b5635f5659dbfcb7aad38e7e877f87ffd5887f24a85c78e1f994d20ef8e608805ea4c73ab5f29e865f8fa3b7a835c5915d38cd1fdb832fe21b5ac429791ce48b7d2dbd207d32114c88a6e8551cd585f7893c1026a430a148437add50914ee27629ca4c20ed56c70359b039cd5628a3f7d46b7598989884dc534ad5b93f181a5c4047474945c7814c3d923712c2d0f03091cf9028d5d35d26b2d0e4e404d4225b9050552afa10aa3476bb1edf45d19c0e1edc0f43870fc1e168c33e4c4e4d45fdac43b15080d1b131222875757511c1a333b2071fba15a279c7cfbebe5e19a9089b8f3e9184859178fc6221266508354f326d9e245220312926c98539f333da868cc65c0e181266404d818c8e24d4710f6ad59a8cae137def2896c8568c4a85841ab4110959b57a1dcad18bc3c17583642312d1701c31d5e048b46134acdefe3ed8b8e9a8685ecab067ef1ee8ebed8efa518546d4875dbb1ea7f4831313e3f0e0430f623c231a774cdd383e3609c3c32370e8d061f8fe0f6e83bffdece7e1237ff1bfe1939ffc0cfcf4a7ff158dfd3060b64a225b054c6d91842a08a3636121fa5e8cc6b1233a538cfac65b81cee116e28691ab701f701e30459f1f9f874045b70a91aa8584b972a45396c5e33877c5a207bdbd65fa54ab2e063011bb04db068a1003337e1085149e4a344586939395cc19cd95aa2fe72a2922e2b90d25c9490259b611b22df24b4084378ce8c411cf0445c4c2310ba27e4e57ea70ef3df7c3fff9ab4fc1af7ef5abd8468fd3f5c5ed7b2a7a93f435f5a04eb635e2f4781ead7f228f718427a54baeb99ca954547b188dacb7a72b7ae07972f4b2ba0432c2169e0cb482728084f2516c33b70dec9fd12781f46781661ba7248c89522ab29f50e908f5b47e42f948495af47962e91cfb3b2ea775239977fdfaaed643434b738891bb6845b3af57e793c87d726d052a8da1cff32364a43c4f4baf883ef1694f7b6a53fac119c3ecaeff2b5656fbfdff5248de75b856efff978baca6dfff781dc108111ffde847e1ecb3cf0627ab57aebffe06f8d4a73e4591766db29cf0bf34d27e0a9276ca2e47fcaf2471cfff9273cb0f3bedcb72beff47f21592b0ce3d777544c27af8e187e117bff845eaf9af7ef56b70e0c001580d8251cc3efde94fd37d8d192d6aa5e31f89f26f79cb5b5cd4d2152c987af00ffee00fe2f4dfeefd5f73dd3c6dbaebbf7bff672bb7129fff39fc37d7cdd3a6c3ffcac4bf671aa02fe0d94e6abb926791b55a28ad1c54d660b633d9b3117381a42d98b43eb45a2059756ddf6dfb6992676c16c2a1a4d56bd556abf3f33db74b29ade6df94b4be3bfc3bfca7c96ac5ff79e79d0ddb8e3e4abed867dd5ad494204ca2a13041805ec29be9a6b8ac46ac62e197f181aa47e51551cbbcb9e1f6887ca0eb09c3f8853e7e86ca1ed6c3fd89d38c117341120a98c4a0f74dea44529116d5290c25a92794e490a94a2521a0e492a42cbec02a97bb804959a14a2c37a306b51952249da9a90a8c4f4cd24394830787288ad6c4e414d46a558af074e8f061181e198dbe5728055eb55a83aeae1ee8ebed8beaf4c291912330303000e363e34450c2748698920f054942681312269098848204238c10353d5da528535807fb5dafd7280d21065862fa46a3de90e3aee62ee0e8619e07b391783eb4311161b2b6d1d6e968fcb1bd72a78c6085a42a64fb605a413c8ea4238cd680e916cbe54eb2697c7c1c3a3a3a887886fdc1ef38361d1d9db077f76e3878601f45c41a89c671747414903cd3a0349602468e8c1231a4bbab131a51ff8bc512b555ab57616262026ebffd67d1c3ca3be1ee5fdf13d51d279b0a85128da15ff0692da13eba762085cb13aa8732ea9b9c6fb5ec38a255a84641f03efe4f34250cc4a529098bbe223d4a5d646b77276cdab40ece3cf314b8f2ca4be1c52f795ef490f8d8a8524392e6a09920c770a366f05345b49229fe426056a2e049525fd0e6505bb31cdb88fed1891098401782f6e38febabd2441c52194429a254c07f1503cdb1b7688c3c3a8e24b87a9dcb285f148671d4286e07d7b0d02381299d94feafd11ce149f544fd3f847c01b1b05fb23ea6f3dc12f9cd725719b887d24525fe1055d6d947b1dfe3b6b532014f8caacf93c4fe97c9565c9ffd23e330d66b5c23292215ef8b661218a52dd4f63df20dc95a699e8bc4c7e227d6f115110cc978a1ea0ff90735f6a1d64760df1c7d9e76da29445c9b391789b8eb7f3bd79cf99585bcbf5d2859ccfbffa5107dedbbfbffd665974a56dbef7fbc077bc2139e00ef79cf7ba29743678093d527175f7c31bcf39defa017b8cb1dff4b29eef9dff215f7fc2fa9bf9cefdf56cbf33f4c43f8810fbc7f5544c2c23fc4fbc10f7e907afed1471f81d52038577ff4477f04dbb76f8f23379bb2d2f18fd1935ef18a5744cf83368193952578fff58e77bc83d6a7bbfeb727eefa9fd45f6bbfffcdf28b71fd4fabe7eeff97461cfe93fa2b15ff05d380b4effac4b492acc1687722f32eac3c033b1ff5da19072e6b969f2f8795b72edba1dbde8edebcf5da191b9bccd5b676da988dce3c6dd9cab46be35cc7314dd2743afce7afe7f0bf3af07fd4519be1d2cb2e86071e78885e94e32b78c111ab2cf8d5d72f7fc7071a9c720a850903429e94c775fb4442e6125a8416bd9d265b61a61001c1f7e3d459ac57f01c932132e215911c30459690515a9874107a2af28e48da650247bd51a70854b1016dde8b2039a4a7a75b9148b0af1e24ac16fc60c2463c6cd2906887d20816318de0286cdcb01930ead5e0e02091483082ccc8c891e807fb56181f1ba5e856480629757442c1f7606a720afafa07285ddfa1838761d3e68d141d0a6d9942bd8a6821e732a0c850a813f7794e82a81d7cf9e6aba861480043b20fda4911b43c5f853d9a7fdfcc126ae90ef13bda89a919d14e5a6f918db5688ef03b8e4ba958226b30850adadcdbd74b8436ec074656425ed1e0407f341621a55a3ce9c49d444043a2547f5f3f94cb65ea2fb6b379d31698c47490f54ad466076cdbba95fa7de5532f85cb2f7b321c191926a2dcde7d87e1b1c71f877d7bf7c1fefd072985e4f0d00811c2500feac71491121b2afd9dc7a41f8e1c2524e604108987465544ebd6172a1da34ff38a7fb15988bef7f5f746fd18a0148a9bb76c86534f3d1976467d3976db51b0399a6becc7c68debe0cfffeca3303e3ea5521a429c161034a213b6af9302e9bb08931482bc6901b56c7e21b7086dc584b215ec2b9f93f6a968771e1f9429feb0ff32556a1047d062ac4a62907e5d0db57f222658d139d507995613094a907f19e3daf3e55a42ec1c7ffcb1b061fd201c3a3024d7aaa47bc9a244aa4cc858d67b01e59f88f8446b5dbb46e904d798542af5e138507b4c620550ebbc39956becaf959309c32492954eae65dc13b6510f68d720ad5db211121b83a01147dc8ab1aab563f61bd3865efd1b4f85dedeeeb86f79652d5eff9742d21e429865d6eafdff5288edded9266bf1fedfbc575c0eb2da7eff5f70c105f0f18f7f1cfee7fffc43b8f5d65bc1c9ca97fefe7e78cd6b5e43a926b746f7b7365909f85f0c611fe39eff651f5f4c5988fbabd5f2fc6fa9e7c694d5f8fc0fa3d57ce6339f813ffff33f87af7ffdeb50a95460a5ca238f3c623d8efdcd8a8eb552e4da6baf855b6eb9054e3e59fee1cf6ac6ff1bdff84638ebacb3e0ed6f7ffbaa895cb6dae5a8a38e820f7de8c370f1c517d1fe6ab8febbf77f4b77fd6fb7ae7bff974fdcfd7ff6f156651cfe1dfed3f417f22eb6b92e8885585073d53d9b7a8be53c5bc96c00b1d0e33497feced6e9981796b9e8d275b6735c3f371f8e6036b6cfb6cf0eff0b57c7e13fbf2c36fe9158f3d42b2e85bffbbb7f82ea7455b28184162a542469a7e8c57d4c16104da9ad38fa95900dc769b27c2d65962a2cebaab455c2de19b901f33004e96c2822421c1d0bdb89fb279a080d6c3bd78d1927485ae0762d37044c8fc1083c18690ada21686882e40e8c642514e1062c375ea43ae6478430353d091e926d8a25a8542728147547a910952b10c908091d18116bfdfa7530367684883e48f8181b1d217211a6ad4362c9d4640d3a3a4bd0d3db0d474646a1bba78bdac3b4844c24a3085d7e174c4e4d52ea3e245fc8e84a92ec92a49cc32839a16adfa3d48ab406028cb05358506c973a4a680145df42f29550eb0fbf072af52092e4fc685c706d54a2f58bfd9a9aac50f4af69cfa7b532347244a6572cfa706474127abbbb88b886692671a50e1d3e183d04d94aba6af51a119db05bb876919cd6dddb03d39569e8ea947f5978f4519ba1abbb07868f8cc3e4c4384dedeedd7b297adad8e818a581dcf5f86e183932129d9f9491cdc6c6a92eeac7b16638e0320e42ec630d3a3bcb74ae5cee80dede5e4a118911cdfafbba6170ddfac82e0f366edc0883830330d03f107fef89ca7948da827a648307575ff554f8ec67ff1ec6c61f93f3137a8a65a811954012ae30a253a0485958d6d37266860ac75ecc50943565d9e6b90a5bbd070f21f6219288850a82e85f0358ab0813a882161b2d0c648426ec0325ec0b65242ae01ec5d852119988c8081a914c1021518eb7221361e42d5fda827d66ca6898c5b46403554a9a8deb37c0ba75eba231bf8f301b626aceb86cdced386a5fd360d1c433c9caa3928c252cdf50e42a2241a20f6d3462dfc93e8ccaca4ec57e9a5b411d92a0e85319222a727b84633ff21f92c018b7abf4b0afe53521006670502559ab105f1b889ca6740561f35f0fb10b5dbf611d5c74d105a97f813c1b71d7ff851377ffbfbce6a395acc5fbffe528abf1f7ffce9d3be0fdef7f3ffcc99ffc09bd70c688ac4e56a65c76d965f09297bc04aeb9e61a98ad2c05fe97da8fb8e77fd9c79783b8e77fcb4f56ebf3bf1d3b76c0073ff84122f67ce4231f59b1d7c43402d6f0f03045fe5ea98224e3673ef399f0ce77be93fe302f4b560bfe31e23b5ed78f1c1985f7bce7ddd1e71170b2bce5052f78414cbe72efff96f7efffd578fd5fc9bfffddf3bfa44d877f87ffc5aa37dff82f642db6e5f810368d49d7cad6acf3b33d371b992f7dcb6d5ed2246f7fe7d361ae94b16159caf5e0f0eff0bf90b29cf17fe659a7c369a79d0c3fffd92f9a48513101233a2663cfc897f4f8e29e484c8a70451159907c80241ea1525d0134a7cd5275898c8551a8f497f500499bb261b9be859e1a30688ac652474282aa0bd44f593e4e99a8ec95d163547419b4a15090a9bb14f14910894bf643120902e2ab800828359fb429cce060e90c2dd59350123d304a11468b6a881a341421449b1d991e4c86419299fca2862726a66072b20261a3060383eb8820871ba6cb3b327a04ba7b7a61ba32095bb66c8172472745b7c2f47b18710b1f960d46754a4524575423d5981a4f92adf6eedb0fc71eb3954847854291a2d7606ab7ee9e6e08ea01457ac26344faf03da8556b104695fd0212b00a8a10122ab24a20237b61f4a2c04815391ba1e96b26bfa07e4c85c8eb098f20410ac957d3d5691a134c0fd819f51dab556b55e8eeee22b25357d73a2261f5f674457358834d1b37c2f8f804ad9b81fe5eea1bfec56a231a235c1e3866f8a02a6854695d747674d1dac035363e3c42a91e8998158d63476727d174aa9571e82c7ad0b3613d9d43c25747641b117a227bf745e3bd79f366188be6acdc59a231c53268078e254639c3f491411061291af77a2388ecef89ec9f8e1ea21528dd2046af42d215da829fb85cbaa2fe16a3769040d6d1559684a2463d9a8380d66e34f570dae927c1d5573f153ef399bfa3d47d122249f42239be6ab4855a87423b16266449999b3b9927b425d09938a17d3a671c13ea5aaa3257c69812ea2446c4f24011c50245ac927da7e3d1ff10431e45c84aea1271cb13443cc43d2619e1f881d6df6a3447455af7ca1f1412e211d2c0a4bfc96691c91499d227e17c62fac7a38f3e3aee2ff9284fc4c4a598081a6a244c212347792a9d64fc57594198440b5476d348b00e8a701528fca9f482ca3f3694efa53482e817959f458214b02d9a4f250217fa6815b9d0533e15d73713317d50fe1b121259a0116041ed4b0256a0d62bc4045cee5b3cc5d1f684f3cf895e5a6c4ffae7aeff3384cbe0cb83b526eefe3fbfb8fbff9525abe1f73fa649f9c4273e01b7defa5df8fddf7f938bb0b0c2e4eaabaf8697bdec6570f9e5975bcf2f77fc6314e0c59710e6a3dbabf5f95f974ac1bd9882648a0d1b3658cfb9eb7fabbf84591eb25aeefff18fe430ea10fe0efc8bbff88b5432d34a14fce322fa63bc152878af8284f1b46b9d29abedfeff79cf7b2edc78e30dd19afc087ce31b5f877beeb9079c2c2fc1dff89832f2d5af7e757c6ca55dffe753dceffffce27eff4b71efffd2eb3afccfbd9ec37feb73b311535fe61bc4e5b890f9e58bed78ab7a733d371f3ff2166b4c43ed8556d6f985b68181ccfb79eb2d95b4dbf67cd93a9f7d9e2f5d0eff0eff73b56139e3bf542ac2a5973e19babacbf14b7f264fb1dd4c948a094ef8029f491d443c902ff491048044047e591f841a918a3a054434922fe83dcacc26091d4828f041b19fe23af10d5018369313848c8a43e42fcf8bc9606c63c8c48750a60643dd484812aa1d8a8e13401cbd85cc44fb31d5998769fa42fa2b32ea6f58507d161047b6c11482b87941fc5da6f5a27833c8e8a6c845e03524618952107a14cd87facf5187681364e7d8d8186d184d676aba0153950a8c4f8cc3be7d7b89b853a95461f4c83845539aaed660242a3b3c3c14d9394664aa46a30ed56a05f61fd81fcd652f0c8f0c1351074401d6af1ba44852d55a2d2a334dfd45f215d16e049247ea8a7c15cd5f3d901187b04f8d80d23172a4a0a0a1c83a2a0de06c57a85e4f801a571e5fd22f28b522cdab2fe7b7d4d1017eb1081da50eaa23d30f36683d20a90abf572ad33477f80f530d86619d522822cd0649338de87b657a1246860f4565a76062620cead13124ace1c346ec3fa633449da55281a283a17495cb508bc60f8932580ec9521313a3910dd1dc466d6c58d71b9d9b86ce920f3d5d25387acb06e8ed2ec1405f5754b7037a7b64da352489757775c2407f1f74761428d2554fb46ddab8018a453f7a28d30d3d14a1ab33eaa707c582a7228f15c80624db0d0cf443397af1e10b19350ad72baf2b5c7b5d91cd975c7271a4737db45f97a420853fa192f211a787c01b528433fc4efca7206c9e1768f6fb6183921a821f2d1a5fa51144420f9293289b6020894aa807cb86817dfe0565465436075e6219c1d1537d91100e1acc5d92732e54042a6a23d07e8808497ee375c136a3101192233e799044610a139b45e0c9cf948d7a4e638998ae52d4b16d5b8f267b82c053b6a9510e9ba3408148d2f891018ac4c491004315798c08554c7452a906e97bf4897de036d8c712814a8f80e54be2591cd90a7d5243122b3d458ae2f2ecc3633f0f1047f0a2a9d4c8acf1fa11cdd10f511745c202b952ea8ac0c53a99f885bee629973c89fe4217c05dff5b0946b75b0ac1171728eefe3f115eabe8f7376dda048b2db67eb8fbfff9d5b390b2da7eff5f74d19328c5cd5260c149fb82d7dcf7bce73df0b18f7d2cf385f472c7ff962d47c1620bde77cff55abc9a9fff61faa4e52aeef9dff295d576ffffac673d0b3efbd9cfc20d37dc00ab4196d2f7cc552ebef862f8cbbffc4b8af4381fb252f18fd7fd37bde98dd158fc6f4a99e964f90892af6ebef96678ddeb5e47244e9bb8f77fcb43977bffe7deffcdd506f7fe7f71f5cca72e87ff85c7fffce5c45803b2527ee4a1e82f8eb2ce2fa60d79db5cca716eb7ede5f8407d25add395240eff73b361b9e11f4927e79d77361c7bccb1f1313dcd1fed6b29039904408423f519f23e131f34fb43f5123e501b7e47920f91017c4f464f0964ac155945c4c40093c025b40d98848084312d4216d6254a87f0e2e84ca48bcbaa3e49e2969f90234250edca48337bf6ee853aa55cb40c1a31a802f9e905334ee118f5f70f10894712198871029213115a539dd5aa5522534d4c56a0b3b38b2240eddab50fc627a6a0b3dc11bd08d840a42e8cfa34323c04e5ce3245b34252cef0f00811716ab55ab4df038f3db68bd2d721510bd3e421d10753e36194a672b94ce90b2b5315ea2b45bcaad5294521a6a1c488365ef4e281a2ed844904331e076c07c79ee29c05290c9b598b9c5d6a5311fa9020c5a907718c70bdf094e0baf10b3e3dc498ae54a33ef745e56b4462eaefeb85befe3e8a1ab56edd200c0ef4c2e6cd1b61fde000fd357d77575734ce254af587242d2478953b3b607c7c9c52fbf5f5f6c1baa81cd62d4765b15c67741ec79f2220757553242b228611110509840145e64252e3aedd7b295a151a8bc7d02e8caa86df3b3a3aa98f98eeb0582c50442e2469058d3aa53d44c17ea19e9e9e2e228261df7afafa229dc59937b09c3e4ec5b1bae84917c019679c4ee9199160a5df344bee8fe0c518474c0ad3feda1f5954b4068885a7788d927498f6434085d282cc99264e22fb0839976c166fc40e148a9fa7eed29960c7d1afc2d8a734689de0bad5ff7205d7b3e47872c43d491ee4485ccd768bf40d383617ae7d1939eac49dc7477358526e927f6487338642276189b8c3c9e689c44fca3912715dae47be56f377d817249a71d42a264151fac17a3df6cbc0442be0f10bc84febbed057d1af38fd210b131c435596c958fabcc7c74145e6e2f91582f4a2fdc71ebb0d4e3de5e445bf7758c9d7ffa590d912e4d6c2fd3f12b0787c165396db7a594ef66034948d1b179f0874c61967e42ebbda7eff23069efbdce7c28f7ffc6378e52b5fb9249870d25a901cf3c637be299ea7b4177dedca52ad4bbc0f5f6cc17bf3b9aeefd5fcfc6f391335d6eaf3bf9d3b77c2620b12f7fbfa965fd4d6c5baffc77b431c778cba74f3cdb7c47fc8b01264dbb699b6f2b8d01ff0ad1039fef8e3e97af7b9cf7d0e2ebcf0c2358b7f5df0da75fae9a7c137bef10df8f8c73f01175d741138595a41f2d5c73ffef196f7ceeefddff2d1e52411f7fe6f6e36aca6fbffd9965f683df3adcb49220b31ae2b9a8035dfecc395fcd70f7965a9189e7311dd06fd65dd4a90b5b0a6964a1cfedb1787ff6639e59413e1c2279e2fc9184a77bc258d36d9210c120113a0f8f2cc51683cf5b29e521962349f823f83514d119554449b9820c27d0c55442b2f891c43a406f59d08571a0181481786cd609006f89c246e8531d9a8d1084165298383070f434136912e29538075fa7abb29555e4c165269d29accd2fe4d4d4e51d42a7ce8dfdfdf1b3dd4eb80a3b66c800debd741b55627d24eb1d401e57257348e05226895bbba8944b67efd7a7a08d8dbdb47634165a2fad80ed61b1b1b8742a4179baf5124204184268aded308a0337a108069fc903081e56ad569b20ff7fd4251ce673d004efb8669db281a908ab833df42041a90f38f7f85ee79097985e96b5806c717c96428e52e998e1009520145aa2ad23e929878b465dac232bdccc1a83cf80217531a1e85291d3b3b606a6a8a526c8e8e8e93def1890938323a46f5b06d24508d45e70e1d3e1caf0b2c3f3e31198d5b91c82b94a233b2b53bb2034941be8a5c84f34a44adc82e247ca11d34aed1f8e331247e617f900c47d1dda235d9d5dd053dd18bb3c1f583d0d955a639e6b471bcb11d720d4b62d3c64debe1caab2e879e9e9ec8a6e6685012525af428453e94dca170c6a632ef4921fd8a8415a83ab3bc21e726547038450a83a68d8e4bf3287d2611ac7c2f5e83814a8d89fbb86e51078e5fd206a7cd949f2848c862f251d8460cb7908de6c1883e366dde4c244bdd5759eb129153fb2b286383446b73c4299e038038e2150f9e10498c325e0b5c8efd27eb0b635f07892fd4be237193a382f98ab0c59f44ee320857fabd82e0328c4f8e7ea83e0bd1b9f3ce3f1bb61db315968bac84eb3fbe50586c59a81747abe19e525e8b16e67a9726f8b0be559491b57cff4ff727fee23fbe598a315a6ebffff1e5d12db7dc422ff6ce3bef7c70b2f482f787e8339ef39ce7c0273ff9c9e885f41b72472a5beef8c7345f8b2de85f70fcdcf33fbbe0ef95c5165cdf2efa5e22e63a58ec7b146903ac2859a8fb7f7c9e70cb2d37c3fbdef77e38f7dcf3560439392b2ae275d75d07cb5d708c2fb9e4528a7a85d7bbb54608cfbb366fb8e17a22fee076d145173be2fc120892343ff8c13fcb9d1a5317f7fecf894ddcfbbff6c5bdff5b5c71f85f38590df85fd104acf9661fae05e6e052313ce722ba0d33c804cb5c1c1b75e1c4e1bf7d71f86f168cf473e59597416767671c3985129aa988261cc185db0d8c7dfde57c93ad5a9415f3c28e84244a63a74818285e41a520546d7b90900740457ed1890e4c5660021691b8986ca0085aa0c808140987a3bb30298cfae725017be22f00bb1edf05d57a635669f690deb06e7d3f74f7f4341d958405996d4f1d8a376ca7365da387da9486b03245a4334ceb55a0748b25e82cf7445b17f4f4f5524a354c67377a6484f462b4a7e8f12bf4f474c3faf583b07fffdea8bf0d493ea148481e3d9c9dae4c53df474747a9ff4c48a1883548f089be23498bd22a46df1b2a0561103462bb3daf1093d7164278fdd11061eac040ce1f929c7000f145136e941e312a8d2427ec1bf613ffe1774c31888211aa88fc4411a54a14610ac956384eb46e68cd60e4aa1a4c57ab34195d98e20f893e3c578aacd3d1297521d14d4240a69e2b15e5b8e1dc2186b0fd2d476d26f217b6871beac6731461cdf7693c63024dd41612e5886487fa23db366e5c0ffdd103dd9ede1e492e02b9667de1cf4ce1085e4ca042d20e46f6bae66957c1f6e38f83304cd6bc5e47400c0d4976121acb8a37c9d281981ea474c59a821066bf04c219cd11a92b682677094532e3170b619890ae642ad170864a328d09472049457892d63a8ebf220cc92af93a20894e1ce149fa98c181018a281793a82c3e99a3f031294a274ef1d6e437753f29b434acac9bc9a84a376198235a291cc791ebd85f2b5629fb54a191b0923261bc46888caa11b90424d1ad745bf5486a81b20dc95a1cf90a058986575c7e09f4f5f6c272919570fdc7bfe25e6c59a8344fabe19e1223c89c72ca29b098822ff48e3b6e7b6699b57cff8f7382e4f3c5147cf9bf14d13e96e3ef7f5c9fd75cf334f8f0873f04bff33bbfe388114b2818d5f5652f7b194500f98bbff80b38fffcf3db7ac9badcf1bf61c306586cc1f4833886eef99f5d4e3ef964f2018b2932eae1d2a4685e8e62ae03bc1e22496d31e59863b65184e895220b7dff7fc51597c357bef26578c73bdeb9e873d1ae2031274d10dfcb59f07ee31def78077cf293ff1f9c77deca20bccdb7b4b33671bc304de6bffccb3f47f76c1f86cb2fbf029c2c8e601ac83ffbb33f836baf7d06cc46dcfb3f273671efffda9795f0fc2fcb06877f272cab01ff2e05a113274e9c38599382516d4e3fe36438f1c41df18b767ca1ce5400e24768e4272a63231ba84f8f5fc27b493a3094408b98c28412d44ed195340d71ca408d44c03c8d26c297d2c96401c90508546a337543117f7a749c890749df12b289242b7994666f646498221e4108718ab6dce32942d8b86123a5c1a39a2107f01231b184c60389452af21792d12ad3d370f0e0214a2958a9d468cc910035395581234746a15a9d860307f6d11c8c4f4ec2a1c3c3d0173de04392d1c8f0304c4e8ec19191112257213904a3f360da3e7c5189a41c4c31480f4aa3f631aa1632df0a7e54a65e23c21146d5c26e625a434ff86a5c659f64f42520e217a77f14cc9c5b00d63c258b5384179a132149341c1d0b23ac21f1aaa354a4284f8d5a83ecc17344848afa8084278ae21399872907bbbbbb62420a4654c2948c149daa24d3fee1c3524986922f5d30452092ae301215ae9f5ab546e4b8eeee3254a3b1c2b2381e32c5609da25f51042c95e60d75e0dcd523dbd0169c3fc40592acb00ce208c95eb8a16d48b65a8fa9260707a01cedfbc5624cbac235c97851ab2cde64da3d99b251929342d8b9f37878d2132f8cda134969e6cbd02692a84c4c4652c77943f1749c1b0421c69cb9c5fa215eea4d4b84c86b5a34b83030a251c5843021a35dd51b925c67f9114a11b9d47a90d1f4a43fc3b9f315d10aa39961a431a1988e496a4d21495c39b02d47bed9ce75eb07283a599c6210949fd43a2ed4f7380a963e96a01148e33950be215491014349a0042dfa95fe89e503fdb8e62f633d6add704443ee3fa78495edabd4864142b2139a2dec67b98f54464b531b93b394ef0ed57562fbf663e1ec73ce8cd34d3ac927175c70012ca660b4bcf3cf3f0f9cd805afa1679d75165d07174b9054b17dfb71e0c42ef832fedc73cf85c5148c12e78846cd827fd9ffee77bf1bbefef5afc38b5ef462175d611105d7e3eb5ef73af8e217bf482fa3f145f46a14c41cbec45c4c41129b937441ffbbd8e9d6ce3cf32c70922e279c70c2a213d41097cb9d68b414f2e217bf08bef4a52f459f2f5e9669096fb8e1c6ccf3f81b64395e4ff05a80e906f17e03c7d6adbdf60589589ff9cca7e1f39fff3c5c71c515b43edd7ddbfc0baecddffddd57c33ffee33f2efa6f7a274e9c3871e264394b1301cbfa62791532f816ab4f6b653c9dac0e71f85ff8761cfe979f0c0c0cc0c5175f40c4051215f904c57c191fbf68d75ecca370242a7ec9cfc22ff565f41a48225241989038281a5190900774420112b6b4a82f4d84b0a49198f82023e370842dc9a092e49024c216919e82a08910156a51b03095d9f0d0902474917ec81ded07f5e00bedaeae6e4930a1314bd2c5e96d4982863cf0cb5fde09a562117a7b7ba0a3e4c3e8e818542a55e8eeea211b1f7dec718ad474646418a626c661627c8c0840c3c3c344b4424548c8eae9e982a9a90a54aa75e8e9ed879123a3740e233271a49c4d1b37c0f8d828e92d164b51fdba249c094952c1b46405950a4ff24b04b54573e62551c7602e41905224a1cf49928cafe65e12d51a44b6f17c19f5aade08695e31c215f6ad5494642a2496491255014a1d252299e126543425246961f4a2cd5b648a118c88d51bcd1912d47ababba84f3d3dbd315908a3501529125508870f0fc3fa758314a9aa03235e456d50442d216dc5711e1b1f8fc6739ac6ba18ed535ac2e8257eb150a4b6f1e545213ade8bed457507d7ada379c7285d94668f187b0a77c980c4049ba6104a200986ea34c67a8aecf5e1ca2b2f277297a22ec28cc5a7edca12cdff00983c999427dc72d594d4819ee0fa2118d555839a09e14c60919a30f13b489e9281ae54d43a8dbcd4f45d457a437548baaa1171cb4bd25485614cd80a934ec764b02cb1c1bf2bc2656fb4463855a24eaa8a89573672a2416c8ac94b3cac4c6c8ac956a00f5d4cf8023dd295efc7442e4e49c8beda57e774e22ab71328d295a7520e722a42fea4ef90cc05ad711529afa1d26d729f6352ab8a4288ebfc924b2f8aae2dfd46f7ddf5bf959c74d249e42f164bf0e1f052a4af596e9275bf8ae90017734ece3efb9c054bbdb55aeeffcf38e30c584cd9b16347f402d04560b109be187df39b6fa1d484cbf185f36a11f4d3175d7411bce10d6fa048166f79cb5b685dce26e2d542cb7ce21f09b08b29cb3d02cc520b62fce28b2f86c5944b2eb904165256faf3aa75d1efc8e317317d35fe8e3dfdf4f46bf05a7efe87fe18c9c9ef7ffffb6943d2cb7221b9e0bc3def79cfcd2c83d7f3a588c49b2638764816c26b1ea7d79deff15c4bcfff71ec301ddea73ffd69f8d0873e0caf7ca58b623a9f82d7c79b6fbe19def6b6b7925f76b2fcc5bdff5bf876dcf33f27cb551cfe17be1df3581301cbf6c264b9e5b09c8fc15bac3eb51acf95120e70aeedae24bb17dbd6e5e4e01cfee7571cfee7a7fe62b47bd59597c1e64d9be208547a14158ea412bf9857df79138a10a07f72f42ba188014c8c8a4903a0a7d70ab5c8341093aee2f2aa2f669a433d1fb62ca3a2c908265d41cc7292842b99df4c46d4210db1bd4c6040b214ca5d77ff1a6627227a51db0b1ba317759218c304343edb4cc65256c0c8f09138ed63bd8184a94e957ab093883d5bb76e85aeae2e4a8183e486fe8101a8d5aa94024f1249028a7e85b7344800eb28fa509d9e8282ef41b58629f66a30313119134f3c35be4814c2fe6394282455205189225d8924aa0d47caf10bc578cec27a231eebb813b319ad98c4134f9655f0c513a5222c1492b510a8b9f4441c7907374adb4764119f085848ccc1074e5de52e3ad7dfdf4764ab6201bff75204aa7257270c44c73105e3e0e040347e058a06d6599691b470ec710cb07d4a155990e743655399e6c9a75489d836ee63f42c99f6b093dae81fe887c168c3cff5ebd745e7bb69ae3c5f50942b9fd2c48978cb3982910d8aa426e3c2d15a38ef09e7c0d9e79e49eca0197e38e475af70923279a1b1f1da5530b7faf7d8e728825538cb85d11c354b5ac0644049040b8df292d8098a64876b02a3a32575c2986d866b2098836d28e572076106437e49f29736306c93fad4fd158fbd24aa411cd92f895625511068be12343dbe224b1106822026cbf2dae74857fab8d8ec88ed55edc5e42e8be8fe5cd7eb69e458d07d73b461f4ab6b9ef6d419badcf5bfb59c7aeaa98bf6f203b1f2aa57fd6ebcefeeffedc790f4804487c592b48864eefe3f118cbed1d394ea7961055f00ce46d6caef7f8cbef2dad7be96227f6044269c1f1759617e04a3a82201e5affeeaafe8a5e99bdffc66f249b389cab712f17fcd35d72c5ab493f3ce3bbf895ce49effcd14c43512b7176b4eb09d8b2fbe08165256faf33f24889f79e699b058822483e38e3b36f5bc7bfe278589436f7deb5be1894f7c222cb5a06f7bd2939e643da7f7f5052f78c192937230ade6d5575f0d9ffdec67e1631ffb18918616ea9e622d3effc7b144bffa8e77bc9da28a613a69bc6f7364acd9098ee775d75d47ebf5e52f7ff992a63176efffda13f7fe6f7ec55dffe7a7fe62b6ebf0dffad852ca6ac3ffb24b41d86a8097db82c82b79ec6ea7efadd875e60ba3b43a79ec98eb982fd69ce5b53b6b1ce6cbd63c636d9ba3b52e0efff97438fccf94b9e0ffe4534e828b9f7c411c71486865759215a5fb522ffef568580d154d25d048532871b424b22789b842fb41920e8cd38331c3835fe42b2366a6434b3a491191a4be4092b74492e6506e49d49b50d5910430d008585c56f6fdbf6eff59330b64c620a61cf624f167c78ee3a3cfc4564e771893357409434a5f373c32224952425034a4de9e321c3e7c988a74953be1d863b6d3f895bbba8878552e636abd9ea8b84ffdaeaa3478bb76ed8ada2e11c904094414dd2a128ce2548fe66f626202ba7b6484aee94a85facd44ac244d5e9810ac3c393e41d048f8564246441271a09ed9af71014cc202b00dac247bf832b29522d3d1fc2ae255d090a9071b2a42169a8229fc62fd51bf28fa544789a264312910a3f920b10a89501881aab7b717faa24f4afb8684338a6855226256676707bdecc2d48518890b05d7f1c60deb297d23927a30aa151e5b37384851e5d6ad1ba0d48283eb0669bc91d8852fd27c15c1cb97b929a92fd4a6b06c3946cf182dfab779e306b8ecb2a710e66cc2c4242fa30d332a16a592e37952d831459237a30d312760f6ab82ea0bcd12b43788238db1708a4aa145b7935f34ff87d3e9732a4ba0e8784c029d6d7abc42a183487649442f2dc51f485f44d1a3848849564dc42ae55303e5bf38b51f451454c426b62c5e2b61929e9575aaaec6be8ea359f99acfe67160bf8c7b0db5867572966dae1a6198a450443fd59091c5906828499041ecabd91e6cfbaaab2e839d2726841577fdcf2fe893162be5d2f1c79f00471f7d54bcefeeffed822f1b31e5c96208464eb8ecb2cbace7dcfd7f5276dbb66d70e59557c26208e2f1dc7367979267adfdfec79777981aef739ffb1c11b15c44acd90b8ee5dbdef636f8f8c73f4ee42b2622adb4888573c53f46a45aacf46aa79f7e5a535beef99f5d900c81f72a8b21af7ce52bd7049973aecfff30d2d26291272ebae8e2dc513ad7fafd3faedd57bffad5f0918f7c84a2442e55ea3cbcb74492741a96f4be62d977bdeb0f96c456bcbee1b5ee2ffff22f897885e4e3b5906e70299fffa3dfc074d29ff9cc67a231ff385c7ffd0de024bfe0f5f0031ff8007cf0831fa4fb15be475b6bf7ffaddad0cbb8f77fcdb294f85f4871cfff165e1cfe57beac35fcfbd1cdf0bb5b753aed7c5e40cfb770bbcb9199b91063359bfe2ee6dc64f579a9d6481e99ed78a67d6f47571ee7bb5863e7f03f7fe2f0df7c7ca5e01f49374840f9ead7bf4529e8e8a53e6faa5ca022a72061808fc79feaa53f1220842293844cb652c428f9925e9221982c80ed20f98423d8f89a5ff0d47761d82a793aa2890bc2e9c5907c1568c42e45556822630068f32284e68f82988085e9db9ef39c6783ef99f3ab48624c1ad252bcd1e15050cab4bd7bf7c3d7a3b1acd725ebcb8b490aa16a475bd3427ef6f5f7c269a79f2a237f4183884148aec2683b48bc78ecb1c760c3c60d4438c26857386ee3e363b069f366181d1b8f1eaa15a152adc1e6cd9be8f8d4e4145986738b5198b83d4caf373c344c0f087aa23947dd48dc424212eaac37024a4128a3db44f3520f685c1b8d7a4c1c914321a3e0a88ecf0de3f1785ace69eb8ce796082605698bca3c0732fa55110266c5a8a9e188588d7a43da4feb4fae3b24e360bf89e4151d6f04f8b0b4339a8b3e1a035c939e22a5e1cb068c6885e357999e26521ca69a449d988eaeab5c26c215e208d313160b2528953a881ce6f905153148ae1dcf9351af9815e879bc9adb1f39b92443223e49418217a646ec837ffbd72fc2f4740d64ae3c23ae96c242a0ef3649983e47613237668d507d49d211dacd560a9a0f089e6fa2dac18dcf7a269cb4f3048a662520899ec729f63895a94e0024f2208471543b22e4693e2b8ea40622f6135992b80a8e3685292f01befa1fff09f7ddf7209d273b4024c42921bf33018afd1d1f936b3950732f7d16f941487c2e47a9435de40b8de18befa1d8b785499f43634e987488e22b5f8463c2e798146b7b8881eb9fc9649e76ef16287d4cc0e2b69068f83fdffd36faeb655dd76c652d5eff4f39e514f8f77fff7715d570e104ff4af7da6baf6d59ceddff4b12d66db7dd165ddbf7c24209be18e3e836adec71f7ff3235e4edb7df0e434343b0502253f63c2f7a21f834ebf9d570ffdf4ed9bcf8c7882ce79d771e9127f0e52992fcc7c6c6e8d349bae09a7ec2139e40bef9cffeeccfe0d24b2fa597e1fcd27c2de21f3178f4d15be18b5ffc222ca4e038ffaffff5bf680ef2ca5ac53ffe710a9260bff18d6fc484fe851024bfbefded6f5f300246deeb7fbbf5164ab2f08f38c179f9c10f7e207f5f2c90e09cbce73def9e31278b7dfd6f556731a41dfce378e1bd1d5e13cf3aeb6ca856a7e91e1fff286ea105af1fb7dcf2e6f83e26cf18edd871020c0f0fc31d77dc018b2198aeed99cf7c26bcf7bdef25a2d8f6eddb693d2fa4ac26fccf468f29e843f0b7ce75d75d0b37de7823add3e9e90af9f885fe3dbad204ef71f18f40def39ef710b1528ffabad6efffb374adc4dfffedd65b28598bf7fffa673b751643dceffff6db75f89fbdac26fc13010b66d910bf5869b7de6cdbcb7b3eef04cdc5412ca47331f5ce773b8b65f742d45facf536d7ba4b25796d9e8fb972f877f89f4fbd4b8d7f8cce73d75dbf86c71fdf2d5fe2874c78487471c41c3dda8b50dfe58bfc84d0c4a4017d3d126e3035184822127084157aff2f74c3e56728538f61d9a6b459fca9a538a4e241a08a84a4338e0024424dbd24684896822443495e44a8b80c3ef4f577c34d373d8bc84fa8901f66c64133e341512484d0074e2a864498919151f8c217be02d315268e04f409b1cf10aa3d552bd273ca293be1ec73ce269bf1c11cbea8eaec28c18183876172723cea7f0895ca34e9c187214822e9ec2cc3d8f838cd5db9dc0d1363e3d0dbdb47842a4cb5578c1e06e043132c5f2a15233d53f48000fb830fb6d08689f1098aaa8563d7a8cb39c1285e6143928f786e0b44d80893b9113c0cb23ff6a5d6c64dbe418c8b674b245b53c4227d3d00c404177963cf91d104c8a9937df0fd84048344c36234064cd49344ab226d6807a62f4442178e1b96e3cf6249461fc3712c46c790ac572c46e73b8a92b8a6ecd22346f942c4a9352501d183986c346bf2953640440ec27ecbe8606120a239ed86ef7df7fbf0f8637b7074e4daf670fd6174a98051a0cea935a9369400537a6ac74286643c27224e57caadf38f2a22440aa1da906b863f3da194801a2763e148fd21a566bce9376f80e34f383eaad360be58b496031969cef39b7ec415685f8d37468d1232ea55a8118570c37a728e208e8a952588799f7d8a32a25607f8cabf7f3df297f70111db50575448845aca56a1225c29d262420255642af687a03c12471604d54c984401c3a86b4245b7e231a29933a2ff814848ab81fa8c230e2ab216b7cd691039a29667dcd348bfe8c5edf12c11614c27c9f2392128ade7e5573c199efbdc67cddbbdce5abcfee3835d8cc271ebadb7929f5a08c11741f8e038edafe1ddfd7fb3f8ea5af8fdef7f7fc15e3823f10ea30e60c4455ddcfdbf5df0851dbebcfcf18f7fbc607f1d882f55fee00ffe803099252bfdfe7f3eeb9a82442c8c1070d6596745f7a503b06fdfde4579e9bc1204fdca962d5be0dc73cfa575862fe79ffffce7937f36495769b216f08f69c39108b067cf1e5808c1b17eee739f0737dc703dcc46d622fe376cd80077df7d373cfcf0c3b0108273f2c217be90a2e1e49185bcfe9bf597e3f33f8ac01c5d13efb8e3e70b4614477f75f3cd37533abd8588c4b716eefff1d90d923dd1c763baf172b91c5d13f72dd835115335ffdeeffd1ebce0052f9c716f9926f45b399adfd34e3b0d1e7df45178e081076021044969b8665ffffa3744ebeaf7e9da77dc71c7c16c64ade35fff3e1fd77f9c17bcffc534904f7bdad32862f2bdf7de43eb027f032d24c973390be209496affe37fbc1ddef8c637c049279de4eeff978138fcafdefbff85d2bf5cf43afccf5d1cfe5716fe674dc05a4a590a60cca6cd76ea2c54d9f9968568dba673b938bfbc609e8f36cceff3a573b589c3bfc3ff420846f7999c9c80db6fffb97ce99b728341c7383295b61f9f03264231250992f25c57889883114788114959260300eb0e13e20f9306204e93a774aa885ac2b4d33694a196c64b9d0fb587099de50eb8fefa6ba307cc83aa78a8a21471a721f94e140d45e4082599627aba015ffad2bfc3f0d084ea77a80f20348fa7acd83fd007e79c7d163d9c1b1f9f8c1e6ed429751e92808a058fd2dae15fa2f5f7f511e907c957f24628b2b7a30c43c38789188404b74254af52998412a60a23b255916c0b14e10d1f6c57ab557a8082e42d4c2f8891a09078d55029daf01c9291d44045e3eb2b425b422ad1c7c0be64e7be8e85f94f344761a3b46d914dbef065eabb868abc2619416a4dc8d490724da20e8f1e22150a323295a7d21b9a29e93c4fc41bb5ebc9ba9e1635883aee2576e28e30ec8ecb259d9a5f11720ce40214b4757674c27d0f3e00ff75fb1d10f3059a2265b12109ae789586f111ada4c5668963893121f4284afada48a2dc49329a228331210b8cf4774212b0fca8ce4d37dd00271cbf3d3ac4a44da983c654819de70c9bc5e86db8447d451664df82c43be088572a6da1c784bf167391100383b86c1078f095af7c1deebafb7ed96fc1b435e5e7b48853bc5643cd87b130094a560de31f364d91ad345f48042c45208cf7e5004bbb1a92a81647b70245c052e575fd61dc3f88ed0a82e6e87c58ae41293fa109f37af4ab9023e1459febd70fc22b5ff12238f1a49deefa3f079d780c5fcadf7ffffdb4cdb760ba893ffcc33fa45409796c73f7ff72fd1f7becb1f0b39ffd0c1e7ffc71986fe9ebeb2742dc196764a79f74f7ff4959bc7e3fe9494f82fffaafff82471e7904e65b10279846efecb3cf9ed1f67cc95af9fd8f04367ce97cd5555745dbd5f40706f8e2f9f0e1430b1a4167b90a469ab8f8e28be119cf78067ce8431f8297bef4a570e2892712a9252bddda5ac53fae9f134e38017ef4a31f2d48248e9b6eba895ea666112d1dfe9b05f18b2f9f3132d97c47b6c37bd8dffccddf8c5e72ff0ff2f3edc87219bf8590567dc3e70498561aa3602d04a107d323e37d4a2bf295bbff6f2de86bb66fdf4ed7c44b2eb994fc1aaef583070fc27c0946f37be31bdf481115313a773b82fd451bcf3fff7cb2e9de7bef85f912267721e11853b75d7ae925f49b673e487d6b19ff0b21f88c10c95848a447821c9206f1de18efdb705b2b647a1c074c87f9aa57bd8aae4b380e738dd0b656eeff5bb5617e9f2f9dab4ddcefffb5f1fc6f3988c3fff293d5847fffcd6f7ef3bbe7a2a01d6905b4e538b069e7f3d8ba1a1cb0393ff3f157b60b6dff6239cbac63f3ddc67cd6cd8b39877f87ffb5847facd7d5d509bffcc55db067cfbea6e855ba84332bd207a7280cd4cbfe388216a5d84a5eecb3de580f13b230f2928a201468a9b7b80cdbc8a405e499241161984c91e84d0858296b58d92eb4f222ee4f183d14bb1c4e38feb838ba8cd049254257a29fc3685e1865a700dfbef507f0f0c38f49eb3413388d9f601b85242f556b7538f5d49360fdfa017aa881a4112462f5747741c1f7e2f1c1f47bf8e01ba359616a3c94aeae1e7a005e6fd4e921c1d4d40474948af2619648a2f194cb9d513b357ac18351b2f013eb6154208ccc44e9da22fb9998846dd6eb353a1744ba31bd9bb4d753441ef93d7dc9cdeec62d9e3321b401579bfa40224f0324d94ace5848c41ab9467c6804759996b0e0c7042a8e7c4665d4d830e92f8e48a56dcdfd901b1351e23242235ae13e656e346d5e48cc87da3087920e48e90dfd688d54e07bdffd218c8d4daa658db88a13dd41f3629692960e31bd0b49c42768a2f56804a22054c421690391d86285326da91a4a90b0956bfd59cfbe1eb64718e43851cd06a90856d47d995a50ce471289cbe734a72249790a8a4c1913e872b874c9f162029688f0e9c711b068cd41d04423e3b5c151ad62df675e3f306216133f85d0c651fa095f4b691847b9d24c662295a77c2cad698dc8c5291143fd583cea4d4319b72f0c1fca7e4a8fa4a563c3576444acf384f3cf8117fed673a207fd3deefa9f436796a01f3ff5d453a3971ef7c1ae5df347f8419f8f2f63f0417a8c819cb2d6effff1ba7bf6d9e7c09d77fe6a5e234ce09cfce11ffe01a5fd68f765f35c64b5dcff23b1e7ce3bef84fdfbf7c37c09be787ccb5bde4224006ed7fdfe9f1ffce3cb3c4cdd72f5d557d1cb5d7c99b76bd72e5afbab354521464d407204aed5d7bce635f0a637bd8948574f7dea5369ad2de6758d65a5e17febd6adf489844bfc0392f9105c734822fac427febfe8b7d73a48b3cbe1dfae7bf3e6cd44f2f8e94f7f1afdee9c82f9921d3b76c0bbdffd6ed23f17fbb2cab47bfd9f8b2ce6f33f9c8faeae2e228bcfa73f95113adf45a9275b89bbff6fad53974d9b36c6e464bc26e2fde5e4e4e4ace70ffdda99679e097ffcc77f0c4f7ffad3e9de75b6826461249b6cdfbe9d4858b325c0e2351009c6f8db03536de3f5ef8a2baea0df3a2ccbf9fe7f2eb298f8cf2bb3b9fee36f158c50f694a73c85eee1303ae1c68d9be838ae0b7c6e335fd7e6a516ec0b12acf00f6f5ef4a217512adc17bce0058485561171d3c4ddffb7dfc67cd675f8b7b7dfcef9a5baff6fb7ec5cc5ddffb7d6edf09f4f1cfe678a7ff3cd37bf1bda90d90e926da06d805e8a49c82bf10bc705d49ff7fc6201673e742de59cb61ab3bcb6a5addff9b26da588c3bfc3ff52e8988fb6f3dc007477f7c0d8d838dcf1b3ff86465dfe55baed9653a8cdd3750a2441404c16e20d342216966e846133bd434f7ba53610095920601d2ac596d0ce09950e8ba2e084e18c145a6904ac847c11c676818ec5e8eb939f7241f4b2f54cb288223f09b68d8d8c8dd50858211141fc42297af8f9cbe845c17fab083acd63a79a880f6084acea74052eb8f07c4a6757af4f11f1aa54f2a137faa18ff67695bbe8c515dae2ab747928987210bf6368798c5e35383840ed95bbcab48f0fb76ad10391a94a85ca60d9209a28fc8e638365f0e14965aa222360d1bc63b8ac2026c471de398aec131d4752931c328e7426c0ceb56a7fed0bf38b68de84367a714a483c1a84330805940a0fff095eebf445d5124de540cda2be8148a26ee99b5e4a2f2bed1333ed5e60913649f215074ff2bc0294a279fdf14f6e87471e793c4e75a7e88ad2de304939c82683643059da98d96ea88892742d83e6a84dcd1d17b1021a4315558da3939963c46bfca69bae871d276c8f8ed4e3ba71e42b5e098aac257d0dc4f305f19c43ec2f904848d1af7c4fa630ccc3be829904ac7a43c097bef455b8fbeefb541b81460655d1a9545428698b88fd58935ee5d378d2843c28eb44631b689103637faaf93f2293457d8f53a40a455035eed1b0554a17a95246c6fe5318842bed98c7fe93d3bc8a64bc63f2a222d662fd72e4675efab217c005179cd736b147b735cff9b572fd477f8f694a1e78e0c17989f0830f8df12f77f12f7839d56a1e71f7ff52d0e6f5ebd7470fe02fa00813434343305741f2c92db7dc42a438bea62f4759cef7ff18390149148f3df618bdb89ceb83524c2ff2ce77be935ebacc5556cafd7fbb7ae6da2fbc662349000949989e102310617401f47918c9f5c89191154dc6c27beaa38f3e9a5e50e28bf597bce425f0bad7bd0e5efffad75324095cb326deddefffd6e7312512be00c6d477232323305741b2c3273ef189683e36a7b63917590bf847a238bea4fef5af7f3de76b22fa054cc7f9477ff44771e4c1e5222be5f91f926f700cf13a76fbedb7cf39420dfab24b2fbd9422f59d71c619b010e2eeffe5bcf5f7f751645abc1e5e78e1138988383c3c4cb8ca7b5f833ef215af780511185117ea9d2bfef15a8d783cedb4d3d54f7901070f1ec8551fd7215ef39074f5dad7be166ebcf146d8be7d3b11bb5692ac14fc2f84d8aefff87b127fbf5c7cf145b45e91e887d753244af7f4f412216b3e49b98b25780d4292e9f5d75f0f2f7ef18be93733a66044626bbb444677ffdf5ae74a1187ffe5f9fbbf1d3ded88bbff4fd7e3f03fbb7a0eff9ace3d7bf684f3c16a5c6e629bdcf92c3f5fc2edb66a7fa9ec5b4c69672c50f294b5e94fdb5f4eb218eb571fc7d5260eff2b4f960bfeefb9e77e78db5bff307ac07d6ffcd29f5ec66bed866176fee326bd4234a510e473c2b04def93de0e930c4c5b29f28bd28f75306a54539a2fadbc1ebd855366c584083193cc80048d5bdefc5af89f7ff0568a96238910490a3488530a4a2b3c8a7884e70208305d5a5884bffddb7f8557ffdecd10d68db1428205d9a1348890081ec20be0baeb9f0ed75e7b0d943b6444998e528908515ddd5d7194aa2aa56ee982e96a8dc8577dbd6568049866ad00e313a370ccd66d9264d5a8d15fa2e15fdd37a2eff8000d7b3a5d9da6fee343061cb38ece0ee296e060e2716c0fcf215105c96449541dcab317d5a953ba42398748cac2d392b021d3bee1902a96cc3cb08fd00698113848ce83271439449b6bdad7da95d1d4a2b2f81012a029f29a4e8263f24e93d8ccb71d33494139893d6d4b98752c88db0d034104acf1f129f8f3bff828fcd99ffe6f98ae54912604a052100a90e42b11ce542b2c4d082f9c71544665f2a139f255b3614c4c62bc49628f888bb37b88dbc67d08e8a1f1dfffc367e08acb9f12d5a9491d815c0b12bff23bf699d3160441d06461c1f363a5444cd25247721d91638dcae0707599e232b27d6aca8357bcf20df085fff735d9d3b01ee90f2551138c1f52eccf380aa0465223522913ad3c931628fbc311d702cdef728ad6509d07a1457283e61f795c4f196afd2115cf8d6a8bd14bed2bdf1f5f6f4046bdc23ae83f3c75ec8cd34f85bff8c89fc0f1c76f87bce2aeff89648d054687f9e8473f0ab7de7aebacd3df3de52997c0effddeab89e4c0c45a77ff3ffbfbff071f7c905edc7feb5bdf820307f2bd88d2055f6a22f9e4c31ffe307d66a51d6b57d6eafdff912347e0739ffb1c7cec631fa3efed0ae2022332e05fba63b48fc514f7fb5f0a5e53907885f387d88a9ed151741d4c81842ff46683b58596fefe7ed8b87123bd50c617de4f7ce213e1820b2ea07b6e3c87f732f3915a29afac05fcdf75d75df0bef7bd0fbef39defc06c04897ecf7ef64d9402eb98635a47f45968590df8c7884b1ffce007e197bffce5acfc2fbecc7fd6b39e05bffffbbf4fd8c98b19f7fcaf59f4f1f8dad7be069ffad4a7a3e73977cd6a4e903cf3c217be90a215a12f5b083bddfd7ff65820f90aaf7f3ffbd91d70fbed3fa5df033c9748ae43220cce0d92a3f08f03f0da837f28a0e3673ef18fd7e74aa512fd1ef92e7cfdeb5fa36b3292c450b84d8e9a851ba61ac43f04cc4bb872cfffe7264b7dfdc77bb84a651ac6c646a3e7c9f7d01f107deb5bdfa47b395cafb89e970bb91ed72b466643c215debb21c91049a698ee1831359fbfcbb2c4ddff27e2f03f3759abbfff57b238fc27e2f03f375931f86702d65c16675ad92c1dadcea1ccc5212d35d8f4f6d26c99cdb865b587d2aade523bfaa558e8791dfb7cd79f8fbeb6b38e6733b7697d73f89f9b38fc2f8dfeac36f3b63d3d5d8d5e607d0afeea937f4de9ea5807d7e717ee487a41ba01e95665386d9d4e16109044a90914d92054e9b2981043290ab576b80cb78d2f301a58175ff8ab7a9cf290fa8475a273dc1eed53ba3ca0946744b350d19be2b45c96befbdc7654e79a6baf827ffea7bfa1cc6932c5191e976dcc246029628bd750c49022fceaae07a397dd4f83a9896abc3e650d8ede252363856c67a4f3945377c21bdff8bb8001a6464727a3879f47531d2481f5f70fc0a38fee8a1e1674c2e0c0808c7a55eea2871ce5ae6e28164a14990a8952a5521106fa7ba15a9da6efd86f4a572880d223aa21a271ef881e8c4d4e4c426f5f0f04f5a85c4789c692d70a46516ad4eb1429cb27fd20c753c85491446a1109418f89586af5c15c25175e82b0654ba197c3168d44b51cc5ee2f134e4fa00840b8a668cd8502bef69fdf85df79c56ba307b8c3d17c79b49664795ad514b52e8fcc2460514ba0e0c7a566d68b4bcac8563c579e22f4e1ff6484398d38199dc2f5faf7fff0d7f0d4cb9f1295a84a7fe229bc87e924b752a108b5469dda41bf118449842c4a3d48844220c2a2487304869804acc9498097beec75f0952f7f43f650348880250756e23a30e72a4c4856b1cf5451b288e8e8491bf91ca50db5d8427e56f9a920c2252852594c745351ae8075283b3cfd1aa95d0b7c2dada11c23651393bb009aaee57177b47e6098fe57fece4be1b5af7d053db074d7ff85d18f04acbffbbbbfa3976a79051f22bfe0052f84dffddd57d10bce76c5ddff67cb6db7dd4673f2c52f7e11f20abefcc70805bffddbbf3de3a594bbff9f1bfef1c50ecec9473ffab1e8f347b9752036303ac30d37dc40649a85909570ff3f5ff5e713ff982efb91471ea517bd988ef58e3bee885ee6ed859ffffc8e798942d78ee03ac1e85518dd0a8993dbb76fa7c808f8020f3f39358dfbfdbff0f84722c23ffdd33fc1a73ffde9dce412bc17bcfaeaab29b2054697d45fb0badfff73af8f78fcb77ffb7f743dbce38e9f411e41cc609438bc26e2cb6f9e13f7fcafbd36d3cee31f622151f1cffffccf89b89847f0bef139cf790e3cef79cf2362423b045277ffbff0fa919085d74524b420d96a707030b3cd85c63ffa6214c42eae15b4692ddcffaf04fc2f87eb3fae555c2348a27ff8e18769fdde71c7cf298ada7ca74a6d25f8fb0befd7906c859f78cdc1a871e8f3f059c642c95ab8fe9bf5e6220effedb599767e39e0df3ce7aeff8b230effe9e71dfe1746f2e25fecdebd7b51acca3b000b3150ed2e903c36e4a9bb90939ec729b4eb3866d3ef2c50b72bf33d5eb3bd902dc60568a92f848b2d0efff32b0eff0ba38febfce217bf8237befe6db06bf79e193704f87f33ce0d939e986c1584cd519ff05853241c79302643c569068524713554ba2cb68788148a58a01309e2fe09114770e11e13218b495742457151442ffaaef74324e42149fe6ac0c9a79d08b7fdf0ebf4404912bd02224a485da1d67b45c0c2f35e5de92fc0f048152ebfec6970cfaf1f34483d22695224a48952870f1b370dc2cb7ffbc5b0f38413a056ab50b42924a260c4a9aece2e4a1b8821a7ba30dd6064676f6f6fd4cf7af480b51e3d5418806aad11bdd0ed8142f4000cc95895ca5454a70cf5e8016c776f37d46bf5e87c2f8c8f4f44757ba2877793d0d3d34de388842e7c0882fdc507b69ca28ae6cec7f16f00a753146afce4840b95c20fe2f4678b4dc0a208582d8ae52160e96b76398adde726042c19810949317eb456316d9f078f3cba175ef6b257c34f7ef2b368ade0fcf83467525748d1a6a48eecbe9b042ca16a13d98b224d35d4d1743d024266ee287f2162bb3d5fa54f449c47f8eae82cc1e7fffe3370c5654f89e6b74a91d6780c74bf1e2adc4b92644091e2e81323b881f43b486ec2f52ba3c849e295af885879c424601d19adc38b5efc6af8fad76e259d22eabbe7252902b97fcae0e453f9379498648ae3a095175aeac21012925332888a8085e3148671442a6a02208ee617c473243fcd2880ecaf8548a264f95a4a44aec7be59f7bdba1de8abb76d3d1a3ef297ef8773cf3dcb5dff17f8fa8f0fb0f125da37bff94df8f9cf7f0efbf7efa7bf3866c1758d2f3431e507a683c088579836214ff40277ff3f3b91a49f1fd38b841ffff836228ae0b13a11971b14cd03e764fbf6ed144d025f68e24b00f3afabddfd7f3ec9b38e1027f7de7b2f7cef7bdfa3882cf8f2474fc58463cf29f0307d0a46fac0f9c97ad1ec7effcf4de60bff388f88abc9c94922dea03f1c1a1aa6080bfbf6ed25fce1f5717c7c9c70687bb9877571fe51702de858c4ef18b1033f715d601aa8134f3c91cae33e5e4b3b3a3ae97e3c4f742bf7fb7f7e455f47188905e71b497948bc946b61a8e99a882457f4b7886f4c7779f1c5175b89afeef7fffcd8c01172befbddefc177bef36d78e08107e885bb2e882d7cf98d78baf6da6be96538cfc94abbfeb792e5827fc40992f8317de74f7ef213f281ec1bf9be117172ce39e7c0339ef10cc28b2d6a917bfe975ec7e1bfb5fed576ffdf4adcf57fa6c8285915f23ff8bc117d134678c3e31313e3743dc7fb3cbed743c17b7a5df879250bfa2ffd5e8c235be17ddb8e1d3be1a4934e54e9117be81eae0bff70357abe6afed158de7e3bfcb7270efff3536e3eda5e2df7ffeefa3f7ffa1cfe17461cfe53f4b443c0ca03f6d9c8420036cf40cc7602e6d3e96409ea44992f27325f362e373dadf4668de342037e312e2aed5e68e7b3ed3ce7e6a2773eec71f8cf3e3e1bbb563bfef1c5e1bbdef547f06ffff225a853da2f2df515d7950a62dd78bea14801281c75265469ace2682fb2021dab0732d252814811b23e2749d3537661fa388e88c5915930920d4666a28858a1247db13da44ba5c7e2b4649e2263710a403e2e8c885ad44618c096a337c2b7bff9c5e821e4718a44116612b0283a962ffbd308fca86f4578f9cb5e0d5ff8d77f97d1b88cf908284a11487b0446b06940a9b300af78c58be0aa2b2f8789f131387070287a71de4d11afc6c627e0d8638e85f1e8e1848c26d680bede7e22521d3e7c985eb21f3a3c0ca79cbc93c853f55a8d08159d5de5a8bc80b1b1f148571f0c4565b66cd9440f5fb10eda43734564158f8830539353748ec782e628d28934975abd4684164a3d18534304450a038dc42344bc4a165ce4746463292d9a50d33eb49685bbc2e468db8ae184e34324c090c9360d8a5a363a5e833f7aef9fc2273efe29a8d6a2398682ae51128b4034cd9d4dac11b0547439c5bc54939eadc7e3e2d211b0f6a6a874418485cece127ceef39f81a75ef61408894cd6a0123867f116cac48b412388499fc5824ffd96fecaa3758a58a3e857b8be0b92808624c5d450787abf696d874d04aca1a16978e18b5e05dffed60f95cf08227de84b95ff13224e8b89ff973e23a161a22fe4a87f589e49a75edc68125d0e547ddc90541afb38430fa70f6c70da40900f39c378dc454c786d18110743650768f70c4cd8c263212444aea67b2dd5cfdf7ee58be1cd6f7e3df99df91277fd6f2df800faf1c777459f1560dc2121607070208e76e5eeff17aeedb473987243123f2ab4cf0400dcdad13b1ff6b8fb7f29bb76ed86234746e27d7c59c3f3321ffadb95e57cff3f5fed2f25fe398de164742f8b2ff4546975bf14d2398c2a8b826ba1abab1cd7b5a59e6907ff73edd35cebad65fce3ef1a4cd38bbf6179be31a2dd7c45b770f8cfdf3e7fc717e7789f228fc9db4cc41ca67e9c6d6425f3d872bafecf97def9b027addcd8d8183d3790a464a1a216ad23e229fe61573bedbae77f4ba7a795deb57afd9f6f71d7fff4f30b897fbc4f43a2163ecbc034977a33783fcf7f5884e57b7bfb8814cfb6e869381dfe1dfee7bbed3ce7e6a2773eec71bfffb38fcfc62e87ff7ced3bfccf4def7cd8b39cf15fc83a692acd6a208f416946b6323c4d579664d9cdfad2da9dcd40e6ad9367a2b2eccb533f0d8c796cb439af76f4e4b968b41aff56f55a1dcb63ef6c6dc8237931a3db91475ae167212f24adf43bfc3bfc73f93c65963bfef125ee0b5ff81cb8fda777440f4e7753394e1f887565e0231529852ff4aaaefee29ecb859a9d812274e131241ea19a404582f194eeb89e6a0f5303d64349b262a2817cc0ae95e5a8598a64c073c11c8b50b529a3595181b82f44746834e2e85c68d7f0e121f8c59d77c3b1c71e0b48aee2768522aa8471a74542b2080511608408a1186de79f7f367cfb9bdf85919131aac011b94060241e357658c10fa0de88fa5a6dc02f7e71179c7efa19d183041f7a7aca14fd6ae4c828f4450f45f71fd8073dd183ebde9e1e7a808d0f50f7eddf0703d1438862b104679c711a4c462f9b26c62788103530b83e7a6831021b36ac23a2169abb69d306a8442fa788c0d608a0237a29318164acce324c56a7e8afc530ed20ce098e313e90c5728d5a83a24845232853a60548fea84b321c4640525c1c24c4f88b44bc9a93ccdac425a46999842131b300129c8250fec520ce4d4f57275c76f9a5f0377ff3b7503b22ffda39b93987180f616a93614cb48b232ac5a42f89df38921368d850e5110b414cee0a55f42da9b7a1ec409b1b6192ca14148662c20f2451ec50983c29fba22240a9f48bb856f13ca6cb444613fb0cc4110409810bbce83b1227d95acd445d429566917c0cfdf361a2324d693b71ec90dc4851e0a4239106855a0c38c407ab159258e5b16f54fd0bb97991a47a255215fb4df6d3867f14c68f2cae17e844582de29607cd5dd57d24285b40f939fd071c0f74c07629e217cac9a79c44d78a3ce42b77fd9fdfeb3ffae69d3b7764d673f7ffb393b9dcff9f78e24e77ff6f91a5c4ffb66d5b69cbd376da3eebcfaa9fa7cc72bfff9f8b2c17fc23816a362957d3da6b07ffeef7bf5d1603fff8fbe594534e99553f1cfee72eb6b58744d7d34f3f0dd2ecc823eef95ff3b12cc9837f24596511adda919570fd5f8df7ffeefaeff0dfca9e957efdc76b0747e1c3b4cfb3c1715e1b1dfee72e0eff0eff69e2aeff0eff363bf288c37ff3b12c5929f8f7b23ac240d6cbd8cadb8e71237afdbcfad274a5d9a7eb69475f9e3aa6bd59e7cd72b673ed2c36db38a5d537dbca2a97a76d16d319cfb6dd349da6ae766ccbd2d54abfbe76f473599f596b3f6b2da5ad77ee532bbdb6b9b7ad35db672b71f877f8b7b5cdb296f1bf63c776b80ca3cfa8f231b10a2471008c79627216a7f90b38f215db1eca97f705244360d42991a4c022128f2ad70843032b82a270b10d5406d30b920d017d4f22c34866484cf252367a2ab5564c68d062378590103ad81eac5badd6e0c1071e855a3da0146b64930adf832424fc87040e21070842225f21f9ab00be2850c4a0334e3b098ede7a14a5795396108183ca1281292492484891b5645f763dbe070e1c380895e9003acb5d942e7074f4080c0d0f416f7737a5253c78f0001c1e3a4c916c7aba7ba05028d2f9a1c38728ad60474711ca515d8c928591acc646c7a0516fc0a143438a4806502a15a15aab519b3ea5900ba1b3a393f66bb5aab4d6f7289280ec37ce891713dcd0e682579084aca80e46234342881f4a020df68d6ff4b26ef8e643a849213237616e1622559863538d656f0b243205a4d9078b5f52eb13bf6174b9534ede01dbb66ea53506b44153a725d927d4368e54a556ad00a90d8977b85671d9e25ccb6521eb93ae688d288291a78e4320e8bbdc12d41131295a4f9220a4a255098ef8a40c8b4b7b0ada6113f94a36a9fd000b257ec9bf206dab11508a44220c216153c863d24109e0e1c3f54d7e487860e7d779846db423103e8c46981c1e3e42767a42620131c051fa08236a03454ed349519c4e95c64df9348ed0c7eb8ca35f01fb45a59bc6408db58ea8865e578d01d79773a8eeb75419dd9fc7bd8ceac46906f5fb16b6958fa9e358feeaabaf881e8c1ee5aeffabecfa6f7ea6f5dff699d6aebbff77f7ff59e2f09fdfb62c5dadf43bfc43cb720eff0eff0eff0eff0eff0eff69751dfe1dfe1dfe1dfed3eadbc4e13ff9eef0eff06fd671f8cf675b96ae56fa1dfea1653987ff85c7bf671e3095e0f9ac456e02c3b6c04c03f4f2690b30ad13691d316dc81a0cd3b62c1bb2facf3ab2f4b402984d5f5a1fd374d8ec486b274b67abf66ca0e2e3e63ae0725936ebc7d2c6df06de341d66bd567dd7fbd3ced865f57d361789b43ae6bcb6aa63d6d3c536de79e6cce1dfe1df3cbf56f08f7f497cdd75bf01ebd60dea8ae34fa14556c1a39c0a2b3e86654542be0ab91f5a3d52a5d963b751d607bd4f4a37d6e67aa4bb89b421fbe651babe20ae17065a9a30d14cc349f665b4975ffcf29794fa8fad94d50434b1b7749343a1ca0a8ab673e2893b60c7ceed4de48b644e1342858cb2236d3a78e830ecdebd8748519e578052b140d1ae38a523469bf2fd02115746c746a1dc558689c9713a76e0e0c1a87c1186474629c5001143286523a66404e8e9e986a1a12198ae5429605041a532449b28c2598091aeead051ea24520a92771a4860511d96e90803499c11326d2112c23c9e0b6d30d2bd9a938516117f123261dbd147c329a79c8cb3a570c25852d832ebe312f774df238b1276654ebfa6605c72490b0e08175b605b0349b3123cc2240229f844cb8a82c3711b04610d6fdc078f49910a97815a9f8415752cc6bb6aca830cdfd972e50a22391e191d05dd1184aa73ecefa81dc415287f68f493fd81a7704dfe5091b184b28bbadb682403a0ceb17edd87c6d70580d8162aaaa5328c53b00a1a14aacf6303daf086aa0d3da52c9f97a6c888643b761c0fbff11b5740a1e0bbebbfbbff77f7ffc2ddff9bf51dfeed36ebc71cfe671e73f877f8cf12877f87ffac7aba38fc3bfcb7d2e9f0eff0eff00f33f43afc379f77f877f8d7cb3bfc3bfc3bfc3bfcdbda71f8cfc6bf676b30adb0ae306b20ccc96d358959c275d31663dabe3921b689b7d53341cec7f473698b5c175bffcd71b4f535ed9c4d7f9a2edbe2cb2b594e2dad3df3589e7eb402a66d7d71993c0e29ad9c6d7e6de367b6957561d4cfb51aeb769c4fde0bb0b99fe5d0f97bde7134db71f877f85f2bf8c7cf934e3e112eb8e05c28954ab21cdbcd1beb112289a8127df78d345694020c23de50dabb86244759c68cd3fcb1b04e4f45cd8a6d04496e88d302e2863a84469f0865b49c86460cc3636c37b618b2edea5c981423b9fffe0729855fbdd650adb6162c45a427df830d1b36c013ce3f8f884eb25f92a4e451c41d2f8e462507c4a78691dcb17bf73e287594a05a9da40856fd7d7dd0ddd505e5684316891fe99e9c9c84c1fe01181a3a4c84132cb379d366a8d6aad0d9d9013dbdbd91ae29081a754a29393a3a42e9d9b05d3ccf240a246110b92b3201235f61bb48cac2f48358a6542a40ad5e5383a5d655c8c43ba122fdf86413461ba2fe8701cccee339997789e6a9a3b304975cfa6422d4e96e2d6c6212eac735bf20d4f214213068f083f850946f1300e2c855844cd56c10138466288f6d107692166af185c2aa761cf18dc440cf4f7431314b2bd3081b84335c856863c8a9079948c90eccf49b90ef5a3d31360647468e500b92b818c691e1c8efe0a688533426a85311a8e27e60514e9b2a64743e4ff3cd448ed288a3bc795a0441d95c18934dd987799a1f6cbaa629326aa895439dec0b40f3f194c651f9d7864a3f8bf5385a21123e9ff29427c2093bb65bafb9eefaefeeffb3ec34d74456599bb8fb7f70f7ffe0f06f6bcbe11f526d74f877f8cfa3d7e1dfe1df6cc7e1dfe1dfe1dfe1dfe1dfe13fed98c37fba38fcb7270effe9fad37439fc3bfcdb7439fc2f3ffc7b7a03598acc3269a04f1b28bd9cd94973b0ccce9b9dcdb3d85b39bb343127306d21d9c6c43639adec30fbdbaa2f690ecb2c676b338f23ced2690362bb178bb436cd76b21c4196a3d1cbdad64996f3c9d29166a7f9d9ee3acd3387696ba45da796b6161cfe61465987fff4b6b9dc5ac07f6f6f0fdc78c333e09863b6c6f5885ca0884d71d42b21890148ae02f382ab480444c6e01b14aca2483f4854e008554442c0736c2fa7dde2faa8d33647aa2d4ae3a5ec895380a9fd50d91b4282a940112080cbb2ed4432f061cf9ebdf0c8c38fc67a66e0db32b57adfbbbbcb70e113cf839ede2e08858a2613aa7464d050e56507253d44a625fce94ffe0b464646a0bf7f0391a6884426e4f8562a5350add6a92ea611dcbc694bd44e174c4e4dc19ebd7be93b12e66ad56928143c225f61794c538844995a4dd66d0475181f1fa7e353539344b6e82c9729f562bd5187ea74958857144d083805a580e94a85ce13c1c493f35e8fcac9b51110b945403c254e965a84c4dd85173e017afb7a200c83e4a698ced36c35d7e13529144947824aadfb5046c722925dd034d77c5eb66b854772f32dbca67d5d88d0c4b056a7a9494590aa3764242f2633fa715427884956fc89c4402443e2f924d5a8177753a7808516daa098d18b10f6eddb4f38529e8cdaa0705d6a3c991ccafed1eab3958f8afd8590c4c5a631527e29d47c5f23f6632a2da410b16fd523edc9f1511633918be73cd4fea24ad7014c769564d9f81a0249fa41b67dfbf66df01b573f153a3a3ad42177fdb789bbff77f7ff7a397db3d9e5eeff679631bf3bfc3bfc3bfc3bfca795b3b5e9f0eff06f6bc7e11f669475f84f6f9bcb39fc3bfc9b3a1cfe1dfe4d71f877f8d7cb39fc37d771f877f837eb64d99156b79538fcb796c5c6bf6736661b6873006ccacdf269836376d0765eef9cdee9bc13961760a6d8f4b7b3686d7db38d552bc09adff531c933f6b6f96c55d714db38b6e36cb3e6bf9523cf5a1b59e7cdb2369064d9a6d7d53fd3406496b3f5cd3c6e1b9f564e3e6d3d9b4e2ccbe9d9da31bf3bfc3bfcebed3bfccbcff3ce3f07aebcf23222f2082d421545b45204013c12260d34dba0a5bf6a341a4dba417d729ac03a47ac1292c4003a0181eb29fd149145eb8f9ee28bc9024ce6e272b2acd421208ccb219948b0ed8223d178307468047e75d7af951619e1498f0264f23584f60fedc3f139f38cd3290da14fe6e951719858117d253b44d47f542c607c6202eefce5af28ca15a6f79a88f68f8c1e21c2147e9fae56e26839386e9393538a705580e9e9691a678c648565a7a6a688288351736a51bd42b100954a8522070d0c0c40252a5f281669dc51e7c4e40414a3fd8ece0e8a9884242c99fe508e3fb6e1e35af0858c66a6d619935be45c51a7405f12ad51e7647ec4b8ced1d20a61ebd147c1ce9d3b682e900c48e7b98668a619c5d855dae239d5da407c08e169b86fc6a790a0d395c678976dcbd0554dbe80bf675d3f197faa6c230c8890259ad264eabe3e215ba220190b949ea61f1814d60dec225409226c023cf6e86391cf0894b9a13249233cf1b543f99350f54968e9ff40f3fd34c60afb3cde7a342cc17dd1ea529fb4b115ac1f12dfe86bc451f3fa8a65e21483da7811b9b611c463166a7e36f9910570f5d54f85934eded93c37e0aefffa679a8d6936f3b1b4ba59e7cdb2f375fd37ed4d2b67eb9b79dcddffbbfb7f877f877f877f877f877f877f9b7e877f877f877f7bddacf36659877f486dd3e17fa638fc4353fb0eff0eff0eff0effbafe3cf5f4cf341bd36ce6636975b3ce9b651dfe21b54d87ff99b214f8f74c0536c3f32ef8bce56d0bdc3688baa1e622d0edcd3bd8e6a0a53928bdac6dd1da2639cdf1644d94bedfca09b502ae29a68da6eddc77738cd3da4fd36f4ad6224cdb677dfaa7cda6d9886d7db40bceac7936416b73b67add3ce3636b3b6d1d990e3b4b87293667ebf0df5cd6e13f5bbf29ab11ff83830370dd33af893efb898040c40015ed2a940aa55e244ea9f45421dbaf08074446f2fda61484dc47d0d2087afa8d87a50ff171ad7c6c03cf2d931c903cc063182664122c17d465f41a8feb0034458f91e9b68048113fbfe3bf61ba5ae3dafa4042d6b81301058268dcfae09c73cea6483dd0b4de802209616a3769075694fda94c55e0aebbee8143870e13b96a7aba0605bf04bbf6eca3764bc51274747442bd11c0e8d8284c552a44aa5ab76e000e1c380818e568726292521fa22d8d7a83082fa8abbfbf8fa26561dbf8d9a8d7288a0db689642b4c7588e907a72bd36a2c3dea0ba627c4efc5529152145204231aa7007870312d5910705ab6861671488063602d9288f87f24343f611d7a7bbbe0f4d34f514b50addd502e638c381568cb59a8a58ac78210e23a824f84d0844fac2ffd6a12098a23ca2512260d629d46a0d64fd87c1e20261f5114264522e2e850f2b847eb376804898f249b425a97428b82254d0f9bd29b36fd7853690ad3c8572113acc84ee92feebdf7bed8c751b4aea6b14b7c98d0c608b46b52d253394e3161ca97dfa55e492ca3f1507e157d9a507e947d31ceaf2444fac9e4a93ecb714ba266f12853ff415b259aef1546fd785fcd3dea5bbf6e1d3cfd1957436f4f4f531977fd77f7ffeefedfddffa7b5a9ebd1f71dfe1dfeb3da76f877f837f539fcb716877f877f5dafb9eff0eff09f47876e93edd3e1dfe13fad0d531cfe1dfe1dfe1dfef57d877f87ffacb61dfe5727fe3d6843b206a99dba3683cccef171fdb3951e2e6b0e6c9abda683d2ebd9dad6eba51db3b561abd3cae17219d3565d5f2b3bb2dac9a32f6dac5bcd419a534fab67da613b3e5b5bf2d8a9efdb9c927e2ecfda4cbba0e67514a63eb39db4b2792ec8b6353e1b7b1cfeedc71cfe571ffef1d84927ed84a73ded2a8a8c84fbbe48884b4d76b22e5024262db520609968c3684afacb7e2ac329b2343f23543d9118439bbe46e3b659bf2ad73497784e8866d203f7537d7a1c3d8beb52fb82ccfac52fee8491e15198b578213ce5c91741577719a4a9da0d8cfa94e9bf44920e2c6af8eebbef8643878600492d32259880bede6ee8eeee26c2d491b1518a9075e8d021287776506ac503070ea9ee4ab2c954659a8e57a6abd0a8d7e3e840c8656928020c46ca42f2158d918af015b546734da90503691f45c10a1a44e66ad425a18353bb61194e2927c959dc479d6433fbb5ea646e4269faa2f573e699a743b9ab938ee93420c6859c2641a448d07d01e34755a12222f9119b6c865e9b8f465dda36b30000a74984c46d00af9f5045cdd2294484195091f338329f7ecdd7f6b97c1cb1abc56d80e983ebb53aecdab52b4ebd489b2754142b5cef416c03c4c7c2f8b36953fad93fb2bd7a64c1640a445334c0f83a03d2874a52a94ae31a9f9351b0846c28f639ba8f0cb87de57774bf145f1394fdb821a9f3a9575e1a5d1376c85494e0aeffb6f3eefedfde97d9d8e7eeff5bebe1b2eefedfe13f8f38fc3bfc3bfc3bfcebfb0effd96dd8f4b55bd714877f877f87ff6cfb1cfe5bebe1b20eff0eff79c4e1dfe1dfe1dfe15fdf77f8cf6ec3a6afddbaa6ac65fc7bed2c607341998b2cad6c56393e9f552e6b106d657960f5f236c79465731e49732e5936b6b3f8b31ca2ed7b569fda9967d391d8749ae76c73d8ae53b781a695e4714c69fb7a1ff475935636adcf7a19f37b56f956b6a6f5cd562ecdc1db1c4a3b0ecd6c47ffeef0eff06fb36135e21fd3e03dfba6ebe1e8a3b75099462823ca70e950e903a1fd6541c8c422200200466141121097c7321cf905b43a744e4b27083c1f30f3468bf5c791b0a81f8a44c0a911554ac350eb33459051ed8356572a95f525a942c0fe7d07e0ee7b7e8d8d40fb22c910173cf109b07dfb71915e4e41e8a976654a33348302fcd018c83e6244ab5ffef257d0dbdb079d9d1d5099aec0860d1b280560a95494631555da7af4d12a6d581d3a3b4ab06deb56989c9ca041efeded866a751ad60d0e46e31fc260f4b96fdf7ed8b869034c4c4e12f9ead0c1c3d46c4f775754b646a4aa7a50275b30ba0f462c43e296beb6318d21101106887845a9ccd45817bc02651ff455f42ce07a6dfa1727f32344aa13924077ea69a741474799cfc46578691304709e0263be424988a4e9f414d671ee717d6b1b283c818a1465e3dc09e3df8c42a80684b225b1d2d37d978ac6e5fbcdc42a491694e4c046d0506430d1742dc4f54a650878d052b04eecb7a22a2323c370f8f0b0f41d61b36e8949393eb16f52e4af9854a56dba4f02f53d642ca931c7efa82be0e88342a6fef45414c298e8051047d3e2685788f940f93f8c92e5ab361b0a8f643ba719d4a264f1487b8a8817fbdca8dc19679e0acf7ffeb389a0a98f513c37eefadf54cea6dbd6a6bbff4fd7d7aa9c6dacf5b2eefedfddffe715877f7b59877f877f9b5de677877f877f877f87ffac3a0eff33db74f877f877f84fb7c7e11facb66489c3bfc3bfc3bfc3bf6ddfe1dfe13fcde63c321ff8f76fbef9e677eb8bbf95f27626ac5dc9a3bbd52465e99dabededd6b78dab7ecef63deb58de3617abcc42d4cdabb7d57ccfb70df3b586747d791c72162ee74b1cfef389c3fffcd93057bd4b81ff8d1bd6d30bfadb6ebb1d6a186d4548fa041282b80c89ba0188d366814cef87fb4802613203968fc9018a38c0e2ab482f743157c799c040916484243679525142cee01b0d4d0770fa3bbe31e02d6c263ef84211863c1e3ff9393d5d817583fdf094a75c0ca56291c8454004907874207de02449aabfaf0f0eec3f083ffcc18fa3aa721ca87d90b650c42aec8d08552a3241d1a7a62a53b0fdf8e360a07f008a0599be114927bb76efa51484b55a1dc6c647a37345d8b6ed98a8ecf170df7df741b9dc0dc3c323d0d989a4aa2a943a3a885cd5ddd50d131313d0d55586eeee2e98189f80c17503d4cfb1d171b2ab542ad178d4b05ef4bd4011cb7c4a4bc86906b1ef4870c114701ce98ce651a54143329864eb80a4d990df82b5938a70c9fad83cbe6a196ba93d3df8c637be09fbf71da6399504394944322024eb7b726d8672f21836715a3e2a6f6c34d742eec93484aa9c48bf310f184f4265028c3e71bd3fe739cf86e38ed90a8270d15069433d6a1dd71fea261fa2186474def780994bbe5a83d47f8c02e6f992381ac6d42c6839a2d4e7062de72010f0cb5fdd0b9ffbdc3fc2f8f8a4f2310d6a0c099d4c58223ca8ca422424273de5a9fe6348f783f1f55df950f6796c3397f1741fa9d23dc644d44838fd68a8f944be36082ded2b28dcb24d7c3e996cd92eb6d7d1518277bce31678d2132f98d5fd9b7ecef63deb581e71d7ff85bbfeeb9ff3a1cfddffcfb4234f1977ff9fdde662955988ba79f53afccf9f38fce71387fff9b361ae7a1dfee74f1cfef389c3fffcd93057bd0efff3270efff9c4e17ffe6c98ab5e87fff91387ff7ce2f03f7f36cc55afc3fffc89c37f3e5908fcd3fbdb34f61c8b8dd9672ae2c6f2389c3cede4ed2c978b5f9c581692c97ecba3cf763c0f8b4f97560c50b6d756a65d365e3bd28a45c8654c9bb2a49543c903823427a0cf719a7dbab45acfb6b6d28e99ede565a5b6922c1bf5fedacab462e29abaf2ccb5c37fb33edb7187ff74590bf84762c3a5975d0ca79c7a9222e508ac1493073c754c276398fa8904a045b7e274840d4530601216eb2451248420d0d2d9e9a40269609cbe4fae9fa0c96ef9e993bd1033c4912c844403f9491a14098b36221e04d4caf7be7f1b3cf8e023d1b7029130a83fbe8056fc0d3c8ddc9042f4bfebae7b3a1c75f4c6a8ed1aa5e993b622f1c12752168f988020fe76e8e021b8fffe87e1c8e8187579746c0c0e1d1e82e9ea344c4c8e47f320fb8664ade1e12178ecf1dd50eaec26b2557f7f3f94cb9d946a70f4c828f885221c38b09fa267211165ba52a1d474554c4f1895e9edef219b702ea6a6a628cd21af238cbee571c4226caf56a5b28562118246033a3a3b6494b3868c3a84fa6979507d155907a368e1282397476d7922102d3bd13b90b62d92ccc0bad03e855acf20d737cef5c60d83b0e384ed2a6a925044aa68bd79013442dcf7646a405a85820261514425da571195245d4aa51004da02b55154390075de93f5e373d13e9eb76c607c4fd000f137e93f147681234a49c21513b3f058bddea0b5ea2b9211f6531019d48bd5098ed80532dba2e785d64d56c7549b21a523add64278f8a1c7607c7c8a34909f5136a304ec9b84b48fd3f6513b428b042792887fd2c6e4af783c2df520622b508453264535d47ea8f450394ee1aaf9c546d8fc574bec1b0365137d27e2a93c16a8b695c226261ef9b1e8dc19679e064fbae8024972d3c45dffddfd7f3be2eeffb3db71f7ff0eff0eff0effadc4e1bf7d71f877f8cf6b0b9fb77d67fd7a3b0eff8938fccfaceff09fe873f84f6fc7e1dfe1dfe1dfe1bf9538fcb72f0eff0eff796de1f3b6efac5f6fc7e13f913cf8f7d22ae73552ef9cdea0a9b31d876276569f64534f2bbd69cec69c2c1bc8b38eebe76cd26ae19be3659ecbdb8ed99ededf2ca7d76e7f5a81a3d5623341901790690b3fcd59b73aa6eb4eb3314dd2c653b731af036a353759cea49533d0f59af392a6b71d71f86f3e671387ffd589ffe38e3b16aebbf66ae8eaea8afb1c30794aa5cea2345949a5e413cba8c8597a191deff13150c40fdd4eb6cf58b758a6a1caa22e8a8cc5640bd4198671a233ea23e9114ddc9f50e91220233551641d95a20c895bf7dc730fdcf1f39f130184227b45c782ba8cc09325521546a80961e7ce13e0c94fb9984853a00862dc07ea1f127742a592883c02a62a15f8c98f7f1ad9127d9f9aa608567dbdbdd0d9d1419f131353b065f366e88dbea3fcea577742b9a304b55a95ca63d4aa75ebd61131e6d0a143d0d3db17d9efc3de7dfbd470aa348398ae2d90442e1c3f9c5f245321110bc7b5a3d4416590588522531e36647d35f73876481811a190d189a8df72ae30ea19b7e7647145fa10fce2417777194e3af944281555d43afc5fc8e8a01d555648c8061cdd4c6147282c4a30598996b391d8f784f27fba4544e252eb8bf6757226c8149844c4e2a84eaa679c5e94ebf058585a5736a4adcd5012cda262d3d3d370efbdf742a552953e2f9c3902f11e8d53b2853c8690f8324f45a99211f01202979c2eaf3915abd56a68f6b120ffaa44f7a3f13551bf46a9637ac42e224faae3badfe5b487fdfdbdf0ec675f077d7dbdf1b93471d77f77ff6fabdb4a17dbe1eeffddfd7f9e76ccf61cfeb38fe9bad36c4c1387ff99e51dfed3fbe0f0eff06fd66da58bed70f877f8cfd38ed99ec37ff6315d779a8d69e2f03fb3bcc37f7a1f1cfe1dfecdbaad74b11d0eff0eff79da31db73f8cf3ea6eb4eb3314d1cfe679677f84fef431efcc711b0b294da06b3d5a46581316d11a4e96cb5e8ccc566eab2392573b1d8ca70b9acf6cc365a4d649aae34b12d92b4b1349d9cf969d36deb6b3b736bb325cfdc67ad212e93b6465a39a0560e266bada50113c5369ea6f3cdeb68d274e9e7d3da31e749dfb71d33dbcd72620eff0eff0effe9f82f160b70f56f3c1576ee3c1e0fd08624018cca12f016262ffc81c714208ee4821bd7912ffde9a4244ea9b4856407487205f05a5529b7cc31a448558a6041ad795e12c9250ce3c82ed8965065380a0c134ca81c11c9e477a16ca4de07021ab500bef1f56f43ad5a931a54d7c2eca502dc792c5beeec80673cfd3754a4296cabd174e326e7548d1d45d992ca1f7f6c17ecdab50bea516348a0aa37022897cb546e60a09708542848ae39ffbc73616464082626c6a3fed660f79ebd5099ae5234acc181c1e8f82445e42a777652f4312458154b05e88c6c9b9c9c24220b1eab56a7c9668c8235355591ddc0f18f3ea7a6a7a1502c51da431c2f5f8d3545bec274949e5091b264e7314da19c3370b24442c4c2687d23f9ef94534e8cd6511745be229c301f506105e7ca130afbb4794a03a875cf739bff47456bfb204e5589a00ac244bb24544abc17540ac1204c0859cc0263d21f45bc9361ae540a4591605a64fd68082d5629bb4249861a1919835ffffa7e2214fa5e41faa140fd903084a34c017f46ea111f143d8a31af45c7f2380d60934d96fb114dbfbe8f3ad1a7d699104be94a7d4916d57c0c1e433b884449c448e9c329aaa1969a30543eb1a1ec3ffbec33e1f2cb2f8d7dabbbfebbfb7fdd965676e9e2eeffddfd7fd671db79877f877fbd8ec37fb36e877f877fb3bdb47d87ff99b6d8caa7e970f877f84fb3c5e1bfb538fc3bfc671db79d77f877f8d7eb38fc37eb76f877f837db4bdb77f89f698bad7c9a0e87fff9c1bf3502963958699dca526c8ade415ba76c0bc39456ce48b7b7dd01495be0699392e624d2dac9026f9633b0d54d0350d698a4d96af6d56653d6b8b70285a9cf5cb4a63d2640f4b14eebbfe904f54f9b4dba5edb5c9a7664e94c03619643d1ed31c7092c7db0392ffd9cd92fbd6c1e71f877f877f8cf877fdcb66ddb0acf7ef63361dd86f57830261a602a2b4e4128d46743235de18b7b5f457ce1e394b6d0f7286d1947a921fb1441cad30814a08805f1786059695892794dd94351b19070a522bc98631893bc947e4f91bba401aadf20626299277cf8d59d77c1c38f3c8ab549b75ff0200f0725248254038a451f9efce427c139e79d0998d64cc49177401246e2be08157d0b4ff83055a9c26db7fd1734ea011c3c3444364d4f57a94ea15880b1f131383c3404fbf6ed8703fbf741a9a313366cd840e4a981fe7e4a1788a907fd028e7d83c606d3b44d4c4e424f4f378c8e8ec358b475963b63120c770b2368611a432467c9310a681e0a7e81c6a04a64ac423c96587e7aba42040fdc471208a7864ba205e5183427f32684279a578985934e3a1136ac5f47eb8fc87b842990389535e4ff83b039725cac50456f131ecca7242b037d4338c37e94461058eac97fd24fa9147bb2c75aaa4285efb6969e76ad02201ff0f8ae3df0d0830fa9e87c2a252a401c31aaa97608719a564ff9308e16c8a531329dafb021fd9d97b4aa95d3ed88fd5d8a8ff7f46b5d288958348790f8588e5648e7953fa6f157360a6513fbcd81c17e78eef39e059b366e70d77f70f7ffb63e98bad2e655b7c71c270077ff9fc7ceacb5eeeeff1dfe1dfe1dfe1dfea1a91f0eff0eff6976a4ed3bfc3bfc3bfc3bfc3bfc3bfcdbda70f877f8cf230eff0eff6679877f877ffdfc5ac4bfa7efe8c6eb0b346b20b2ced926d2f63dcdd82cb0a7d9970570fd98ad5c9abd6619739cb224cbc19a92062afd5cda799b2d3667d6ca56ae67fb9e66b33926669badf4a539a1ac79d7ebda1c3740fa854bb737cf5cdad64c9ae8bad29cb359d61c0b9bd3b78d6b3b0e37cd61e9e71dfe1dfeb99ec3ffcc71e14f24515d75f51570e5959741b1584cceb34db270d237fc6ee034ee031ea7f462469f790bc39834008a9c1032114bd58fedd6cad231ee9b2a9f90ae9890a1d943442c19fd4546ac0a41aa4072914f7af7ef3b083ffef14fa156af522f911095470446c8f1d1da066cd9bc1eaebbeee9d0dddb0d4886e27efb94aa0f1b0e541f3c6a571e1370f7ddbf86bb7e757734f6c568cc317a950793135330343c02070f1e825ab54a24a95dbb775394b2caf474d4a7024c4c4e1059ab56ab41656a8a485385029eaf4031fa2c968a9476b0abab1cd59fa2b6f13c92a6886c15b55f8fea943a3a681f493948ecaad6aa4440c128580d225d4d136f86c856d13f247d20c9ab69ed044994b1206844c529ec5253ba390893359717b76b5974ffa36fb1d0d8e19a0e41f2af4238fae8cdb0f598cda0b8892089547c2b2c2424438d24a47f123e92547f724d04a0a7d4d41a6fdeb35cbbe3b9d5c85c32f29c884943aa079a5649ae620bf193538e62398a92a56c6cbaefd0c74530eed12edbf5495ae00979be11485d77dd7537ecd9bb0ff8a703a73ea4f2a1f90354eb33f785235e61c4a908673efb7c2ea3a2f109355e9caa552755e9fe8c7c9e36f64d7e4edb3c8ee6a7da0b345fcee3c6d1afe2714412a5ea23fafa8b9e74219165ddf51f66d8b296aeff5992e6b79b70689435c742ef4fda3cdbdac8735feeeeffddfd7f96ad5ccfe17fe6b8b41a3fb3df0eff0eff0eff0effbadd0eff0eff69e2f0eff0eff0eff0eff0eff0eff0eff06fb6ab9773f887a63a0eff0effb632a62c37fc7b599db62dec3c1368abaf97d1dbc972302640f4f36627cd73a60ef39cedd3d6275d976e8b0926b6c3763c4da7b9886c20d375ebdfb3263ecd6198b699cec056c79c43bd9ccde999eda5d9668ea9d9769a7332cbe9c75bcda7a9db564f9f13538fb91e6c6dd9f685713169671d9ae3658a6d6ef4be98fdd1d7a86957da58d8c6cc6687c3bfc3bfd95e9a6d2b1dff9b376f84e73def468aa44391adf8e5bdea13a7fdc31a682d47a031f107fa0609f183cf7b9a5e6a9bfba0362e8f9164785490300438463c8642c4515d7c4ef3d5c0e3d41295d1894132e599a7f55910e5636a7212beff831f524a3e0f2340d583b85c9a606daf2062d244a95484a75e71099c78e20991cd489c90242519d9076dc2685105f0a010ed7a6a13307c78046ebbed2732a294e7935db57a0d262726a1bfaf178aa50ee8e9e981a38f3a0aa62bd3d48789f131e88d8e4d4f4f11510a656c6c3cea4785085748b0d8bb673f0c0eac8383870ed31c22a16e62728ac8536cff14a62df450df24ed3728aa15901d98c60e47b1542c294215900e4e798644b146a822a4617a4299178ef4635f4cd484e69130fb06cf498a68d025f8c47b01f4f7f7c1f1db8f8b89431270e6b823252ba495efa94d685af4d2442e8c7110a82d3142fa2bf3da101a9b69bec46c50af4b42919ee6d2f3133fca84314e27dad432c4243149ae34d697e0f1b1adb1b0c91ac479b55a2322e4d8e898f24b5adf2420e2fed2a662dc59af673c5aeab84e90a2be86329d27628dc6017d9a8a1a28f428599ab2d8c72a3d7a194eb94a110a35129850243094380a962aef293fbe6deb5678fef39e055ddd5deefaefaeffd6fb58b38cbeaf974f6bd75697cb9b6da6b567de07bbfb7f77ff6fb6edf0eff0eff0eff0cffbadea38fcdbdb71f8076b3f1dfe1dfe6dfacdfa0eff0effb6764d5d7a3fc0d2675bbf6d3acc73b64f5b9f745d0eff0effbaedb6b61cfe1dfe1dfe67ea37eb3bfc3bfcdbda3575e9fd004b9f6dfdb6e930cfd93e6d7dd2752d04fe3dbd73691dcdd3903941594ea515b04c9b6ce5f4c9d60729cd1e9bfde604e7b1dfd4a3b76d2e405b39f3589a934c035b9ec935cb9a20326d30250fd06cc0c8d2635b43696bc1a6b3d57c66ade356f6a7ada730e56264736c661bfa679a3d696bacd5d89a20ce9a2fd36e5b7b0eff0eff66bf1cfeb3f18f72eeb967c3b36f7a06918a90f4d4502ff943a900c28624f1701417243f798a5c80e7b02c927e906880248946a3ded42696d55363a10e2419e071499252a9bdcc3e824c6d489174c29062d508a52b498928a33509ee6b20a3e8505b2aba0ea56753e70b8adcf0a31ffd181e7cf041226a154ba5a6e83336410d0d1a63548421a202d8b1e338b8fe99d71209aad481a4a4209957351fa8179b979b2480dd79e7afe1e73fff45344e01203faa566b40a9d801535348b042824895548c8f4fc03446c48a8e8f8e8d43a1508cc6be00870e1f8672b90cc5a207954a151e7f7c170c0c0c50ca408c6a15e304d32b22712a6a47f882eae0f8f40df421170c8e8c8e12010d4b5788ec25c7a65eabd33ce23f246c157c9fc6b056add178d6833a8d3b9249f09caf74d01a4c26cfc9bc8ad03e719483683e8bb0fdf8edd1fa2d1014c2509d6aaa2624a148db9063e5899036d0aa7004298ce8067a34a9388da62cc9f5087e197e9e8a7bf2b8c46c4db62624b6c380f54942a54fa1bd20392f20c638f9a1507527852c6946c09a19112b4496191c1e1e85db7ef4535ad3643b78aa2f21f53d8eccc71bc86854d246d9d7a64856eca7957fa4d9e1087e9affa42855e87f54398a57c6e326a4af03e51f93b1e6c85caa1d245451ba573fd643442c2675a9851093b44299c2b5a3a3042f7bf9f3e0ec73ce0273aadcf5dfddffdb6c6a35eeba3ef3d36c23ad0fba8d59f365da6d6b2f6ddcdcfdbfbbff4fb3c1e1dfe1dfe1dfe13f4d87a9c7e1dfe1dfe1dfe15f3fe7f0eff0afeb77f877f877f877f8376db29573f807eb795b5987ff6cfb1dfe1dfe970aff9ecd78bd41bde1b432e600d9cab65a20590b592fa77fcfb227ab3d5d97a927ad0dd3a199eda439b734dda64eb34c16604dbde6a2c9aa67932cc76d7332e67ca501552f9b66bbb96f5bd4a6beac79b08d8bad7c963de679bd4cabf6b2d640569b7ad9b4b593b6565bd96f1befb4b56db6e7f0eff0eff09f8eff673ef33a38f5d4939334591a394ad780c402226921a980cb791809a90ef55a8da229f92a4a535c07db0ab59b1dd52613a9f07c609bdba8181209900411a72d9c81e750e96ac8c839a1ac87e9f342154dc7f3923e07d214d8f5f86eb8e38e5f1009a311d45be24c2a00455e91df311ad5d39e76156cd9b2995203ca7468b29f8a5b32f36627902909fff33fbf0da3a3a344aa2a950ad0d555a288539d9d65e8eeeea671debc650b747674424f7717744465706ca7ab1558bf6e90ca0c0f8f521d8c68837dc434867dbd3d94aa70686818fafafb28a52092b0a62b5528140b30cea90cab35181c1ca0ef48f8ea2c97a9cda94a252a5704afe02b22985c6ba8bfd451221b1ab53aa52bc43540118342352f4d5397633c9de416b9f655a42b25180dea94534e22d25da0483e3348470a7bcd9b4764c04095154a17b724648ec3383d9f4419fb2b598fa32f419c785234d996b41fc8c87ad19af2bd823241ae1449f0f388a44811a290c0a8d61bb5a7eca744869e20a21fd68c5399dac62914aa0ddbfa13f4ef91471e83bd7bf74a1b1afab54a1036d9f731612d601fa5fc159556045450e7386a55ecc7f87a03109f0f545a4250c7286a96aa2fabccbc4e917f54fdc5635ea110a7382437a4f9c24027cd2a3c125936da3fff09e7c2d5575fe5aeffeefa3fc33e5b7beefe3fbdcd34b1f52dad3dfd7b963dadee4bdcfd3fcca86fdae4f03fd34687ff99656ced38fc3bfceb651dfe1dfed3c4e1df6e579acda65e877f87ff2cfb1dfeed6ddaf438fc3bfc9be7f5320eff33cbd8da71f877f8d7cb3afc3bfca7c95ac6bf6733dc9c105349ab45624ada02495bbcb6c56e7312ba0eb35d7d804d9b6d3ad3c4e6404c9bb326d1a6cfd6ae6df165012bab0dddee3c923576fab1b431cfbab0a4813b6ddf5cd06917a234679756ce74beba5d66fbad9c4a16606dc7b2f4dbf4da7061b337af93cb1a47f3bcc3ff4c3b1dfe1dfef5effab1134ed80ebffddb2f82bebebe24028bb29b22b30899c68a2eb622216881d2a147c5c223ac59703bda5aa7a8599aedb4a936759b98fc85915ef05ca0a26dc5e382ec8926428adca88c3202891bd815493641f2934724228cf2f4d5affe271c1e1a890a15a9628811708840053304095d7e544e40212a837d0fa23e0770fa6927c1f39e7f1345821234646800348db5fce7cb483b6438c0c30f3d0a5ffeca5729fa55b15882c9a90aa5121c1d1d837a2380f1890908823a0c0f0fc1e46485c855c56201babbfba3ef4768aca72ad3444a41024e65ba0a9b366d86838786659239e1d3318c6c8503516fd48924d78544ab483f91b1c6c729ea5947a983a267a1ad14ed0a8955d110fa4846c1ba51bda0413341f386042d22c450c4b390a288052a4a1291e098e5462d8b6472724aa8fd5b1922e661cbeeb7c451a895f70883e79f771e0c0cf6510a494f14283a94078cc150a5f74c369c1641a905f5f90815f132d14ff31a32f147e246b78aa633546b5bd8fb246fcb65ea51c4bc242572843aa98ddba0fd90fbaa790fe56b4099469a3d02b66e3ec87059d8b9501592dfa9fff1bf30c2440dfee33fbe06232323aa3d2481196b95552a3fe5e9f712caffc5adabe854e40b9900a58d2e99cd6909d57726b60ad537fcf4950f8cfdbcf2b9b16e4d6fa008b0424d846e9b34c9235f422312e9da72d46678f9cb7f0b366fdee4aeffeefa0feefedfddff9bfd72f877f837bf3bfc434bbb1cfe67ea32f71dfe1dfe1dfe1dfecdfafa672bbbf388c3bfc3bfc3bf5d1cfe93630eff0eff0eff30c366877f877ffdbbc33fb4b4cbe15f4a1c01cb6c503f9e05fa2cc3d216b85ec6a6475f70ba0edb224cebace9006c1394363169baccc14bebafe9444cb12dfc2c20a72d94b4bee9e7f4fa3680e7d19555c776ccd6efac36b324cb01da1c906d9eb39c9859ce94acf9d1dbccea87be965bcdb73976b339671b175b9f6ce71dfe1dfed3daca7b6c2de1bf50f0e1a28b9f084f7cd23914fd88cfd3c651a8f0983a1ea8345dc063c46d1aeb9dc903be46ce62d28250e7591fd583243517dbc764052479e96b29363f0c63b2169145bc849c418420906591a4426d451b1297300de02f7e7157b48f7a3d365ff6c1337d0dc83281dc0245ed2814053ce7a61be18cb34f8dbad500151e47a6490b9918015456eac01d8c2e15c09d77de05bb76ed269b905051add5888075e0c07e2265dd7fdf03947a706c7c8222f54c4c4cc0e1a161a845dfc7c62669ce0e1f3e0c434347a2cfe1a87e3db2a714b5e7536ac1a9c929e8e9ed064c2758eeec2432560dc9549132b40b491ab57a2dea7f83a2797994f2d14bc61dd31746e569dc25ad8bea860d19a9a8c144101c2fec5b1ce12720d2968cd2232318d993c6cd14b9a620ded270bcac84487773dbc240b4e87772526823ba6edd206cdcb00e743e55d0741d88b300c64582581f6b16848d204c7c8a2734e24fec07c3a49e50d1b8429011aa42a53890e32140c4a431368723d121e94a4f23c8a5711d71cabd261fe2490218f585c84c0204dba1c68a085622215a255bf47f64130a49d6443c7df39bdf89d30f7a20d728288226e03c307fd397a94f433506a0d9d3507e0474bfaaec271f8c442ba9311e774f11b10285f750f379ec5763ffcef5422df296face64d790af7520a382c5a95c95cd58bf582ac2e5573c05ce3fff1c8a84c7b6baeb7feb63eefe7f661fdcfd7f735beefedfe1dfe17f661b0eff0eff0efff67a0eff0eff0effcded3afccf6c5b17877f877f877f877f87ff997db29d77f877f84f6b2bef3187ff997d70f86f6ecbe15f7e7afa813c95f348da22d30dd527d90632dd06bd4e3b8b2c8f33485b6c69cec1e634cd89b1b597656f2bc7682e3ed31e68d137d3ceb479b5e932416eab9fe6d474bd79db6c3596593a6d20c8027a964ef30296e668b31c8089a5acb6f3cc6d56d9ac7a690e30ebbcc3bfc3bfc37f3efcaf5b3700bff55bcf876d5b8f96c7b00c8f892c44df29a558749c4901a493d364b13e2e8ff5434938a0945b4983b14d4cb0c22381969290d36771fa2fa0729e5607c942783c494f289b4b0862316143404c6800954eedd0a121f8d297be4c6490401e06117a719f8d5155fd41a3038a3224a33e05b073e77678e94b5f085ec1a37db2cf93d40b4996118a1c2662120692510eee3b00dffffe0f616878147a7bfbc9de524791d2192261aa168dd9e0403f6cdeb41e6a75eca70f8d7a15366e580f13939370cc31c750b42c8c8cb5e5a8a3e081fbef23ddbb1e7f14bababaa052adc27405495de3442843bd684bb95ca6d48418d9ac3a5d836ab546642c8a103459213256b5364df6f80515ad48f01860474215b12850fd04daa7f1a02523c9321024f33d3b0fecc42e340914a5acb3b303ce3ef76c48028d0963fd366338e4750ccd9b302262f119de93d1b79a6de01a8d80db6efe94780e29e29ae9aed04e5f24643f5486eb27307d1791b69aaf31adee1b4c916909e538fcec67bf80bbefba07981c19aa7ec9352e88878576902fd1c70e6d62d2a9660747b4a2a8712acd209155c3640cd91f73df0418f3a4f5c733ae19b1efc67344846cea581ce5aaa91d4abd2a29a23b771c0fcf79ce8dd0d7d76b8c89bbfebbeb3fb4d4e7eeffddfdbfde9ec3bfc3bfc3bfc3bfcd0687ffe67d87ffd6bab3c4e1dfe1dfb44fb7d3e11f5aea74f877f877f877f8d7bf3bfce713877f877f53afc3bfc3bfc3ff4c1b3c5b036621dba4a519c8c7d38cb70d5416f06d75d3c097a6c3b43f6dc1d94063d363eb732b27d30af8faa73999694ed3369ea6ceacbe9bc7d3168a392e7c3c0b68adda4b7368ba0ebd7db3be699f5ed7fc4cb345b7b5d538d9f49bfdd66dd2d78f69af2922e3a29665836e7b1ae6747de6d867e9e672b64f539fad1d87ffec3e9ae7f54f87ff9587ffb3cf3e13aebfe11931f9492704789c7610eb89843465b643df7153e7a99e8a6aa30ac80f245b29e2161393e27e8409694168fd41e2127e93e91031b296df441d2102848aa62328d99a6c8b23d3848a5c824c0b244d7cf39bb7c2ae5dbb541b2acd5aea12a7f03e20a3fac8f6f17ba9e4c16f5c75059c75e6a9e017b044438d4728092894fb8df612e5d1f95a2d80bbeeba070e1c380c9e5fa2c348842a954ab06bf76e38eed86dd1f70eb2b35810d0d7db0deb376c80a1a1619a9ff1f149e8eaea869e9e5e183a3c04bdbdbdd0d1d9015bb66ca368599842cdf74554a60c23c3a36a0c4338726494ac28466d21790b0957b56a15aad186a909312a1646086bd465442f24d0e0779acb8247fd6934022a8382c42eec2f45d8a1790b28cd23461f8a43f1a4bb0d27b31049180a2998dce9a79d42a92359e2147ffaf51b204e0c482b33347f4cb1bf08685debe724e63ccda7040a4b5c537d2340e8be55e2d6f30524e913a57df5683d3514812f5484484a9f18fb0aa9d9f70ad45bb4414695838cb5c4bd9463932c3e1c071f2a951a7cf56bdf88d66b9d7babd2f8c9efa0d268fa2a0220933e758228e181d3a0f218aaceb21f129e67d0db129363ffe6794d56839a13e031e63902e37e05ecf72f4d5713750cc7f199d73f034e3c71a7bbfebbebff8c3afc39e3daadd9a4df3f9af69a629bdfb479d5db32effdb3ea987d6cd53f2e67fb34f5d9da6935affab1ac7b775b5d77ffdf7c2e4ba7c3bfc3bf5e36ad3d533797b37d9afa6ced38fc67f7d13caf7f3afc3bfc3bfc3bfceb7639fc37d737edd3eb9a9f0eff0efffaa7c37fb3edba2e877f877fbd8eedd3d4676bc7e13fbb8fe679fdd3e1dfe1dfe17f6de0dfb32d3ebda2cd19a4eda7359606ec561dc97208b6baad9c9159366da2d3f64dc7a0dbdcae5de622b04dbed94e9ad81c90adbd2c3bccb1601bf44d6f2bad9db4456e2ef4b4faa65d364767da9ca52bcd699863adb7a7b7653ad334876dea4cb3c336af3687a0db6b5b7359cec8d676162e1dfed3cb3afc3bfce7c13f464a79e94b5f00e79c73a62412849204a047a6c1ff872ae5956d36291d96226b610a3bc07a7a592162a245a091b058bfa7080c145586e75bb33f5e036c1b47dfe2b150840e8a9e43a1ad30fd98ca2ba6220021a103530a3efef82ef8f467fe062ad31550c90fc9dc60c65484325a8e87fd09125e11457d0ae0986d5be0452f7e2e6cdebc91da0ba00e49001d1e6ba92750f56b8d000e1d3c0cfffaaf5f82871f7e94e820d81724546150cf8e5289fa3a3e5585f5ebd7c3de7dfb61746c82c84f781c5307d66b55989898a408637d7d03b077cf9e48478dc854a58e12d4aa35d2592a158988353e3e0ee5722711aa707cc6c6c6233b6a508fea978a25228035280a191371409ba3c8beba2491f92ac217a61a445216cd5920ed8f832cc94955f39a8d9334e1296b7b5b0d82b0b1ad43ee643438b81676eedc11cd7541cd5748d90185b6e674b7c204464b53722d03c464abc4b708852968220049df20e2d482d8900089534e89a71452ea4ac40da5b0d42689dae11486dcbf50a24bead5ae49221981cc39568b2676999412d183871e7a147ef8fd1fa9685f8ad02519638a8825eda2b4a56118fb374ee948a429a13ec3993f8803dd9fa3ef526332639c551d8e0898f82d8935f62d9ef2bfe4fb40fac31052ae59ba2da18c1e78ce3967c1f39ef72c8a92e6aeffeefa9fe7fa6fea4cb3c336afe65899f6dad69c6dad648d81ad5c565ff4fee7dd4fb3c7868134ac98f36babd70eced8265b59db3a74f7ffd06483c3bfc37fdefd347b1cfe1dfe1dfe1dfecd63adec74f877f877f877f86fa52beb38db642bebf0eff0eff0eff09f55d6e1dfe1dfe13fdf7e9a3d0effe9b8f3f200c936f0a631590e42373e0dd07a99345bb84c96adb673e6609b8bc406a276dab79db3012ecd19a4e94e1b2fdb5ca4d9ac8fa94dbfadbdbce7d374d9d64b2b9b5aad217da16789cdc1daf6f3b66993342763d3adaf399bd34d9bbbacf36936a4399aac3170f877f837dbcb7b3e4dd75ac5ffc0403fbce635af806d5b8fa2631409cb4b52f3314980eac3cc1b292ca7930b9ad28aa98dcae277a517cb70942c1a1f246781a64bb795e74f88847c35c30eb5cf24b230898c25eb51ebe0810f5ffad257e1aebbefa33ab48198316e2135102aa3d53884614c12c1547d4fbfe64a78fa339e0a859247c7b8ad58171985c4268e8ae541a321e0a1071e866f7deb3b303e5ea186b0d8e68debe0a1871f85912347200cea30347418aad53a8c0c0f45fb358a5c75f8d021225e1d191982c9a949225d3522bd95e91a11e9f6eddd0f1de54e989aaa1001ab8a29083d5f8eb3a7c85ebddd645fa1508cea36607262123a8aa5a8ab72a690608511b90a053f4e0f89a31628861a91dcd0e600a83e8eb59c7b3956f8596fd489d482b68236ff7984d6d92cfead0669ea8990eb0cc0bc5606b069cb16d8b86143b27e55ea478997e62876a1d225d344ca0de2f52ef765dd664b9411f1a6e3bd41ed09a557e10e24c6682dd05a61ff21b1ebf92a35a8d28525309a1d28e21593ba845aabb11942c3a5d0ae8bb42a91f419d328412d403a836912bffd9defc1aec7f7d0be2409aad542fe46ea9104b5c4cf807edfc3a3a145ed23df08caaf0741628fe7355db3a54d523cfd7e43e9a694ab9cce5323a47aaabc9007926ba9d2c5a42f503a58ef89271e0f6f78e3ab9a520fbaebbffd7c9a2e77ff0fd67b4edd36fdb8adddacf36936b8fb7f77ff6f6bdfe1dfe19ff7cd720eff0eff667b79cfa7e972f877f8b7d9ebf0eff09f6693c3bf5d97c3bfc3bfc3bfc37f1e1b1cfe1dfe6ded3bfc3bfcf3be59cee17f69f1ef7121f3a46dc1d81c46da02b31996e6006c0bd36c43b721aba3363db601b7e9b1e9b539a1b43ed9fa63137d21da74d8262a0d10698bc75ce0a61e2e636bcf666fd6c2b49d33179e09baac856c6bc336f7661b79e6cae648741d369bd3d68bcdc1da6cb095b5d9dd6a5c6c38d53fcd36cd3a36a79da6d7e1dfe13fcd26b3acc37fa2fb494fba006e7cd67544be21c25520d373e179bcd81698600009210b0549093ea71354fb644f185294256e93095a608c35dbde94b250b54ded6be9bf64341d55449dc73a1e113942220fa9910019b5aa11132e880c847d89ca3efac863f08d6f7c1b90a20598d2503085439b0b15a207532f86712a3541fbb2a3011c73ec3678d9cb5e0cfd7d3daa3634470ce2e857619df44b5697178d4b08b7fdf07678ecb13da4af5eaf52b42b19d9aa0eb55a15c6c6c7a9bfdddddd70d496a360ba5a854aa5021d1d9d50eae88046bd06b5ea34f4f5f64275ba02a363e33436d3d355a846f5918435313e09a56291521096cb65d8bfff2014fc0214fd22d5c73129968a945210eba0c5180d0bc7ab5eab5334332c8fd175703c1a419de6be1ab54b690791c0e373fab83026d80924e0e01444faf5083f4eda1339ac387a6a2de2ffbc100607fb61c3c6f520535c26d1db242442391388552fbfbfb2b72fd4ac8620c95aec97121f244940aa2d550fd715af0f4a95186a69fc2215788c884fe4260248886140e42d9184beb219a52d26a13095f45ffa370f864746e13f238c8f4f4c480296908430c43fa703445c72743a2fb289899fb8de4dbf19285fc47d8efdae98f9e34c1922b1006aac548430bd0cf9564518e5f602456c437b84b2c35311b81a0a4f420d0da74aece828c16ffdd6f3e0fcf3cf71d77fc35e77fd77f7ff663beefedfe13fad8ec33fccb0cde1dfe15f6fcbe1bfb538fc4393bd0eff0eff0efff6361cfe61465f1cfe1dfe6d7ab88c70f86fd261b35938fc3bfca7b4e7f0eff0afeb76f877f86f07ff5e96d2b4414f0363da39d3a83460e88b38af63492b93b5504d2766b3453fde0a5c36bbcc494f731c59e75987be713d73c1dbbedb7467890da0fa62b181cc74547a3d9b1303b05f1cb2dab67db7cd915e4f6fc7eca3cd065b99b47dd3165b1fccef667d9bf3b6b597e6f4ccfae68522af73d48f39fc3bfc3bfccf0dff1d9d1d70e38dd7c279e79e05c54221b10f28991f11731a8a4ca3a722c4321c29a969536909799f22c7185b936db40915ad491124b09c668b24748838da0cd078aa1488c25351781ad1b140f232f47147a298ef4b724854fe0bffef8bb07bf75e99e68bca2591819aac0a3db94121ea87aff6f1b8248d9d7bfe39f0fce7ff26747577293bc92c65af8fc9dd54da36153d2baa1f343c38323a0efff00fff04f7ddf730d46a7598aad460d3c60d30343404939393d0ddd545e4288c2af4d8e38f527bbd7d03f0d0430f52e42bbf50a2285c232343d0d3d30d13e3e3b065f3664a3188e4a9e96a2dd23341842eecd554a453a638048a5085518c300211cef574751a64aa355f46b0c228581d1d1425ab5eaf45e72b3181a6e015a0233a87e5902816d2f801f1d5e45a02192d4b48528b9cd7b0292a9a93f645505a4d89819e68ad1d75d416e01482a6c861d6fcc5ac863d04498e52580d652a4a698b424b18434c1e178c202f5e67b8e991ee50e45a00855f2f215f8a04816216943d01526f355affb77ef707f0a31ffe041af550e768291255835129ad5591ad80fc9d24211271308418e71c11d07a0d8c3bdf7cedd0a3020a4d07d7c7f30dcd4f523919aa8b52b1221195498c780ed3851614290b541b78ec094f380fae7be63544b28c6d72d77f77fd6fe3fa6fce5d5a7d5d47dabda8ae5f3fc7e5cdb6dcfdbfdd46b3efb6f59a35170eff0eff5976d9fae0f06f6fc3e1dfe1dfe1dfe1dfd68ec3ff4c5b1cfe1dfe4d5b1cfe1dfe1dfe1dfe1dfe21b58cc33f58756789c33f58eb39fc37b7eff00fa97d6917ff9eeda0de40d642351bd41b4e038badae2e798cce7230ba3d3660eb136c2ee4b4894eb3df9cc8d0e244ccf2360766da649356ce5aff6eda64da60b3cbd68eaecb1c57fe9eb7bfb639c8eb206c76e87ad2ec319da36dcccd364c1b6cdf6d8eafd51ad38f9b7db6d96e1ecb5a2759636cf621cd9938fc3bfc9bede8ba1cfef3e3ffe8a3b75014aceedeee38f9595c476f50b3c116d90a254ed7a5956312176fb15d9a4da1d20fdcae8a3a43fdf67d3acc362131a1d190d1b21a48aac07a6a8bfba66f48636920a901e091871f85ef7ef77b44d8926227b3f0f879a12fd3f4a9547da0e2d078228497bcf4c570dae9a78024ac808a6aa38a700fe37a1c35cc87a1a111b8fffefb616a6a1a4a25246800f4f7f74367672711e27c4acd86c4ab41228e7546c73acbe5e8b38b8e23416a70701dd4eb0d28168b502c755034b2dede1e989898804d9b361219ca2f1460e4c818744575911053abd7296a0ecac4e424d9532a9660ba52816241a62c9c9e9ea63631751c460df2fd027d62a42c24781119ae50a0e34862e3a847218ea7978c1dce7bbd11a8c83d029ccc46128ce1fac2a8641b370e668f67182ad2dc6cc75cf01297f0213262a28de858846f8853f861394e8be733a909940f54a908c340bb89f7e41ac2358691a8688df9deac4d0ea52381438787e1cb5ffe2a4c8e4fceec15f9065ff937f41932b214467b9336f9a483befb5e1c452c64521648fc06ca2fc565d1dfa8e8547c2cf614ea3c6180fd6d1826e3a3f6491ffb4bd42f9d49e2f3b57b144f45d7da72d466f8addffa4d2261f25827fd74d77f77fd77f7ff36491b933cf8b11d6fb5b6b36c37ed61fbddfd3f58cbdb8e39fc3bfceb3a1cfe1dfe1dfe9bbf3bfc03b41a13877f7b79877f877fb3cf36dbcd630eff0effba9eacef0effd0a4cbe1dfe15f3feff00fa9c71cfe1dfe751d0eff0b8b7fcfb638d20c343f6d46989f3690a589deae3e00590b33cd469b6e9b3de600e66dcfa6d3d69eed9869afb968b3f4d8f6d3c6a315c0db0546962de6424e73aababd696bc61c5773819bfd35c192b57ed29c95ee98b24064139b9dfc99869fb471687591d0eb9bed9b6d84291707f37c5adb0effe9edd974dadab31d73f85fddf84732c4b5d73d0d9ef1f4ab89ace36b242a4ad2c73600c4a9eaf8bccf115eccf1c2efaa1d997a2c4959c8ba623b54aa2d2c8f91afa40e45d452edc4fe85cb534568fe543a058f33121fa2b248b4c0883bf5a001539315f8ec67ff1e1e7ae4318d4016d0bf90d20f465b18b513446d3498cc258884250221bfd3bf069c7eea4e78eb5b7f1fb66edb10e9af9326dfe79483beda04f7569137044c4f55e12b5ff94fb8f3cebbe1a82d47c3f4748dd2c94d45c72bd1f789c8c6e1e161181f1f8bec2fc0de3d7bc02f14617c721ababa7a6074741446c726a36d8c4859c3c38789343236364111b4c6c627280a15ce555f5f2f148a0598888e6104ab23515d2497a03e8ccc8551affc820fd56a8dfa8af67bbe20b215926390a0123402226a2529da54b42beaaf4a3517952d14647f0395764dc4232ce70275c9f909e379a3a5a5cfa3becd9308b5b6789b5d6428c8d958d07a6b214cc0018aa356902b28a843319ac7638ed9463a8497d6099110b4508d081519c83ea432a295c295f6290d01857f90732664c24e4abbe929dfa7302cd381ca947f4cb0927815b4be6218905ee5ef028831cd96275908438d7ed6bc1f888088609c1095da8edafdd6b76f85ef7ceb3b84f9503a18a957a8154751a704c4a4483c2e9a2354e127a762a5b5aace0990e4284e0518a874a9eca3a83faaae506d72798a06081097112221a8c6de81af0f7c5e4504643f8f78664192e6739e73235c7ec5a5eefa0feefa3fd7ebbf6d2ed28e9be3607ecf3317babde6b864f5bf55db59f6ebed65cda50d37699267fef43675bd36db4cdd367b74bb6ce395d69e4da7ad3ddb31877f877f87ff99e2f0eff0af7f3afc3bfc3bfc3bfc3bfccf6ccfe1dfe15fd7e5f0eff06fb6e1f0dfacdb668fc33fb46c53d79b56cee1dfe15fd7eff09f0fff9e69545ae76de0cd5a1ce680674d64ab01d417a3b9d8f536d33ad9aaadb4bea5b567dbb789a9c3d69ede562bb0d90092e55cb2e6cc9c1b9b64cd89d936eb318f9be39e55c75c7ffa795b3f74a769f6c70455da7ad16dd4ebdbf67567933567697af4b66d0e294c71e4e677db9898eda6e96eb55e1cfe1dfef5b66d7af898c37f3afebbbacaf09297be102eb8f03c594fd56fa8ef14c9a521a34de13ea796e3b45daa41a95f28d294225621010af802cea4026dac7cf519c7ed213d1e112670bf113462c24fa8cec7f304dafa50c4114e3b4804864046a7125e3c3870e79dbf82af7ce56b8041b41a4aafcca597eea3805b536908058d4900173fe909f09ce73e0bba7b3a229b90c41428a21246cef2487fa0c8646807f6230804d4a603f8d6b77e00fb0f0ec1d86415ea75806a3d84871e7a0c26272b70e4c8383cfac823949af0b15dfb89bc74f8f061d8bd7b77d4b84f24ab5aad01e3139330347c0426c627a1d48111b4ba60ba320d232347a0dcd94904ab6aad46e41d8c6e85042da1885718152c267ae0383764fa356c0ba58891898447e90c6915a8e10954c42fee974cd5e643b55225bd425b7b5ac85239bc4c625164379a479a6f5828fe9522eb245bb8a00cacb98b5a6280692b434c7f1948e22266e5dcba752b00d8ae39c998c5df115301ebb2b7c5742da1d674bcbe43d621a18b3a897825b81ec4443ac2319ef7215ee3a18a4c87a4464f2784d17af3d5a78a3c05724e9a7c134f16dbaef6433688c95898da331a9fca5415fefeefff190e1e1aa275e911d94d56e4948d72ad493f81d4524908d5d2645a7c32f93c48d211e2318c2c07c67d47a0cab0b90d4542f4391d27fb458ea2a5c63014496ad6d82fe2712d0d61a0e6923175c185e7c3b39ffd4c28958a4db6baebbfbbfeebfbed5cff6d7af4b64d1bf89cd99e798ecfdbe6443f9fa6bbd57a094377ffeff00f997af898c3bfc37fdefe39fc3bfc3bfc3bfcdb6c71f877f877f877f8b7b5a3b7e5f0eff0aff7c3e13fbd5d877f877f877f877fb3bec3fff2c2bfa71f34176e5683fa62b4d5371b4e1b58bdae69b86987ad9c699bad3f59e0cb02936d4ccc7ed96cd0fb6c7300a603b52d48d3e63407a5eb34eba7e90180d4b9b139401b50cdb9d6cfdb24cf7c64398e3c0ea595034febabe960b3c6c4b6d6f4766ce76d4016194e47b7cb668b2e36c7638e8fb91e6ce3e1f0eff06fdb77f89f3dfe4f3cf10478e31b5f0dc71d77ec8ce0529c7250f031d48575f133da7c454e68a8e83114f5259064246eb7a1885af1b802c411629afac46d2bd2039280649abb4023337933d7a450765984ca04147287a241fddf7ffa3778ecb1c7997e024cdeca23d254ec9b80c1c15ef89d57be14aebcea72c0e057dc5fd967110f118024abc98c6e9218f3e0038fc0a73ff5598a1086d1a9b0d8fa7503d1be07476dd904db8e3936d21946f3b18d74f5747713e90af52199aa566fc0e1c30760d3a64d44bec2b48287870ec3d43446caea8ece0d1181647c7c9cc679726292c87018050ceb63fac2c9a84e656a8abea3ad32a520c4f385fba5924c5b5899aec8d85f48d08aca62542c4a3518c80863b49e807d4e324f1c4d882221c9a1a0f10ec33021ef39c9144119fa42d8b6f52899ba2f30d7b9a0e84e7834490f8ae3af7c8911516a2e42d347cb5ba83d60669b4a259af8098f998f71259051a8d421612a6ecb106a81fafbeffff175f8fef77e20a3750915b58a17b2f251788cc64688e6a8537c9e71cb3e05125255a0f98f50e9b25d0f05b711844dc429d02263615d5ff3bf1ce18f6c02e9f7d84e3a1e9316051c7ffc71e4a3316dacbbfebbebff7c5efff53a667dd32eb36ffa5a336dd1c59c83acb9d2d79dd92fdb5ab7b563b3d1563f6bdc4d9de6b8a5d9612b67da96d6ef2c1db672a60e7ddfe1dfe1df6cdbe1dfe1df363e0eff33c7d2e1dfe1dfb4d5e1dfe1dfe17fa65d365b7471f877f8e73a0eff0eff0eff0eff0eff0eff0eff2b03ff1e583a9c67606c03900600db77b393b685662e98ac4566b3c9048e6d516501d9b4d3b4d776dcb4dd563f0d8c667fcd3ea5eda7d9afb7d36a1e4c7b745b6c2036c722cb66b3ef7abd345b743d69c0e0f369f3653a982c7d7c4effd4cf65b59505f0b47269fbe67a4db3c56ccf764e2f93b5261cfe1dfe4d7b1cfe9bcf65b59585ffd34e3b059e79fd3594ba0e490080ed639d68f31531008f792a9d20d765e215e0459a530d2adb3d2d8dd78cb956c42daea3f78da3c5c8b45e02e47faacf54569b7b3e2312624fc063a9caab0e63a3145deadffef5df88802463f4787aa99682c496504572dab66d2bbcfce52f81134fd911b5dba01613bb3cd5df40460cc2e83c5e01304210da7aef3df7c22f7ff92ba83782687c7de8edefa7e395ca24a539eceded255297e717a1542ac8796934a0bf7f001af51ad95eafd50089284894421d830383b07fff7e227551c4318aba1542b9ab4c249ca9a929e88bf44e4d55a0abbb9bc689520d06411ca18cc6af11502a3919814c9ec3783cc5a89d46bd41d1aed0166c1ffb57ec28d1f0e27887926b26a36705da1a0b65e4ac900b02af517092251276d0ddd30da542d15a20c1396381ffa7ce85f3437623925d1c812a24a25308728d41986017124a914c435af0b574a512b11c554af520b70d52b5c4ed9e3d07e01ffee95fa1568d704011aec2846e4638f4623267a88850446a42b2a1a68efd59937f543ae2324212ace2b4854258af839eef293b54d43f550ff85aa2f486aa4c32731097114a7fa8488a7d7d7d70fd0dcf201fedaeffeefabf10d7ffb47269fbe6fd6a9a2d667b61e8eeffcd32667f6df366d3e1f0eff06f9ecb6acbe1dfe1dfe1dfe1dfb4cdd69756fb0effcdc71dfe1dfe1dfe67da62b6e7f03fd326877f877f877f877f7d5f17877fb0ea75f877f837bf9be5785fff74f86f1ffffe9bdffce6779b1d34c5ecb0ad9c3999692033273e4dd21684ad4cda311bd8b2da6ae5fcb2c6c7ec535a1fcd45659e4b5b24697accef369d6975cc459e673eb2ecb58da3cdb6acbe98ce35ed98cd96b4b65a8d4fd69c7099345d665ff563e618eb7db161296bcdd8cada6ce1366ce55acd61ab35e4f0eff09f65af70f86f3a869fbeefc1b66ddb60f79ebd70fffd0f32e339ee1713a5388d96c7bad91695ce8e4527f570aabb385517d6c1fadc1f3ca6a2cc0845be6291a9babcd806ce8f968cc5cc348842b5218c71f1233dd56a0d1e7dec71d871e24e3871e74e0095124f7e8a98e8651f60b42750440c590d53c3f5f4f4c06db7fd182a53d354488e7328092b4c00217b64258aea15e979e49147a0bbbb078e3a7a0bf519d3048257804e4c1718d46162721ad6af1f84a1a1611aae6aad01a562013cbf004522e344baa2729351b972b94c11ad50476767274c552ab079d3163874e8208d099a84653c4f925f903c85a906b14f7ef4298748507a38245f459d94290ca3769054c2eb042368e13e7ec773d5e92a148a45a8d76b50f08b541f095c48dee27490413c1e3822819a4b210790e6d28376258fbfb44c5ffc7fe360ae9ad9452c980ca13d1110cf83fc7fa0e844028e1c99847ff9972fd05cdb9a11715d75f3ac517b245612bdaa86a557499dd824ad501842b34f20ec86e43b6efacd1b61fbf66340120dbd84fc88be45f8aa6f1ecd3bae0bbdf15c73c9fd0b657f1a8107fff26f5f86cffddddf476bb20a32329e07ec5ab82b02123fe72bff13b717aaff697e08bfcbe87bca7f2a8564b3c240a0d20c365defb10ceb6792aad2a3a71e145c8eaf6900f178b15e6e1f3d1b62f9861b9e0e2f7ec9f361ddba81d87e77fd87967ad2ec3575a7d9b696aeffeefedfddffdbca3afcdbfbe2f0eff06fb669eb572b3bccb6b2c4b66ed3f4a5d5170effe0f0eff0eff0eff0eff0eff0eff0eff09f669bc3bfc37f567b669bb67eb5b2c36c2b4b6ceb364d5f5a7de1f00f0eff0eff0effede1dfbff9e69bdf6d0e9a0da8ad0cd0cb98e04b7320792627abfdbcfb26086c656dc7cd09111920b7e9b1812dcdf166e9ca3ade4a5ab599d597acb130eba42dde34076d6bd356d674be697399e6f0d2ec6835ce69ebd5563e4b5f9664e9b45d1c4d1cd870a69f4fd369d671f8b7eb70f877f84fd361eaca9a9fdede1e4a7175fb4fef80234746137dea7ca8b71d4ad28147640a1147cb12ea3b121688a82404458712f26453442afcce690899a4047cc147f20ef545efa768b207cb866c19af5ddcbc66420f9e21b288aa38363646dba5975f02dd181d0aa35471f4ae0cb24da83ac943889f85820f3b76ec80e1e111b8ebce5f43ad5ed5e61824e58a860a09228ab8a1c847939353303955819d3b4f80cd9b3741a311404747090e1d3840e903b1dcc18343502c16680c0f1d3a0cdddd5d307a6484da3f7468184a9d653a8f11bdaad569287775433db201c950488a428217a613e4393972e44854a613827ad4566707f5bb5299a2ce603ac27aad4eba3dcfa7f28d40ee63e4ab825f00bf50a0685b18390bbfd352886c433216452c8bf4c9f6bc987087d1b0688e4112e8384525a8dc75a136ee42186b351e73916b0db72bf974e5286310b0d27c50b62dacca533842a29e24f14d4d55e18b5ff877387cf8c84cffa5e83df47f1a1fdd77841aad50ee870a7b22b55f22c60ba5370c55642768f64d129a0161e0a69b6e84138e3f0e7c350e9c9a5428c21feaa1358038e076c3661b8467ccadd08e85925486380aa2861f7974177ce0031f827befbd3f5a5505d527a145da829884665eaf131f298f798a1005ec8bd46484ec5fd4b9501d675f151a693467b4a18857ec2bb3d61afb4e3dfa1fa6243df7dcb3e04d6f7a0d1c73ecd6dcd73c77fd77d77f5bf9d9face2c9deefedfddffb76a238f38fc3bfc9b3acd3a0eff761d0eff0eff693a4c5d0eff0eff69fd73f8cfd7a6c37ff371877f87ff56c75b89c3bfc3bfa9d3ace3f06fd7e1f0eff09fa6c3d4e5f0eff09fd6bf2cfc53042c4891345099c6d82636abbc6d92cc4e9a83feffb3f726f09625459d70e4b9f7ed4bed6b57f5def44ab376cb2a8282e3c2288bb882806c8a0a022338cb378cdf7c7e7e330e23e3a838332ea3a2a283a3b2098a288a0e5b37bd2fd07b77555755d7f25ebdfddd7bf23b119991276fbecc73cebdefbdeaeaea88e6f5bd274f66446464fccfb9fdf36f44d54135498cd4fd54b2c5ee876b5363b1fda5f6915a1ffa50f73db5b798fe98af75fb89e9ac8a5b4c57ea615cf7520af794f2b74eaaf2a4cad7980f7e2c520fffd43986369a9ea93f9ec2654a5f4aaaee0bfe05ffe167958e981d7fecc98a7ffcdcb36717553ebaf1869b60955adc19d1b6a215af47028de671a5d610a9689e57054b7b3a1c49027d004bc2024392ca58af360407b3bfcc564c52865461ab65e19f6d60083de410fb87427eda18a115d4dfed683876ec31b8e0c28370f5d5575235abac6755e25c8800963923d88610dd40f2d115575d09f7dc771fdc75c75d86f2a2ccfe90c8642ae0684b126bd13a247db55a43307b6a86ee61a51b6cd1d76ab76079791e161797616c740ce64e9f86f1b131d8b2755b111f5c87b106d8bf6f9f25b7b460b5d385f189718af9c8f0309c3a750a46c7c6617565857462652cd4bb525c633b33ac028664abd9d373b4a7e191616acbc6e42913f79c628eed06db6d53d90ad7631b42d4b9b2daa131dc3be51258e29532442e9737b6421912c2e83b55406a53db3aca1347b4d3aee5245757432e8fc9cdfe31d4549ae1b3c11ca561ddc266b4fb62f2a6c8b9e5950efcc99ffc193c7af8d8da759857ca28209cf4b8abddbf95adf64485e4f4daaa63da6e03cd33014bf127ea0d9f35caacc2ca725801eba28bce2fbee7e087b44ded075b8e78a8c0fa1709d79ab350de98b6cf84c2b9b9f9c52216ff1bfe57118fe5a515aa084718c7fbfc7c237d99257099bd722b408e19ddb3e42bed8d83d766d03a163adaf39d33d455f8b3c42cde629bc886e030d613f3c21ee206f5b4b80da836d5baf6ecdd0d3ff1e33f06cf7ddef58e90ca7192f77fda1f79ff0ff6fef7d7c56c343d537f5c7effd7efb1ea1c439f62f3c3fbe1dad49812fc477509fe05fffe9ad4fc546cc239b1f575fe87f705ff827fc17ffa9ee05ff0dfe46c05ff827f7fbc6a7e0c1ffefc546cc239b1f575fe87f705ff827fc17ffa9ee05ff0dfe46c05ff827f7fbc6a7e0c1ffefc546cc239b1f575fe87f71f6ffc675af7f6e5ac72de1f8b7dc636108eb32d96d8fcd886c2b9311f521b8efdc57c0975a46213b359b59f70efa9c48ded276533fc1e4bfa704f7e52840f8dd4f9c7fc4a9d47cc660a90a14fe1fc98eed479857a537ea4e6f967928a459837e15a7f4ddd39faeb52f353b917c620e5736a6ef829f817fc0bfe371fff482278cd6b5e01affebeef71441abc9b615b3a28c905b92547f5e49d170f223b28e5da092ae83d0f6dbed01cf20befdbea2f784d6dba8a3f24eb50151eac0a43e420ed4813f8e7b82ad6be1933df73eb0b1323b07213b530cb86e1f4ec22fcf7dff86db8e38ebbc9bbaeee403d8906f5b78a69468fd90c92273a70debe9df0def7fd0c3cfd594f83b1b1514b2a433245d792c7d0256c35d6856e1192e1a1612237753a5df8dce7fe0e6eb8e1466a65b875cb344c8e8f43ab88fdc993c7e1c0c18330bd751b91e134b488f074f0c07970f2d42918191981f610924fbac5fd0e11ab904ad6c52a56c556562d790a0956dd4e4eb15c595d86c5c545af25a4b67f40d5af4ce5a28cdaca8125bd75f28eab64b4b2bc4ce429dc239e1356dec2135e595da1ea46a615a5b6e76fe35ee48ec91b6d7d33c43522c0d0391a5f5a963c44e4ac1c2395c3935e3450f5b3919151d8ba75abd75ad0138ab332bc2664e721d14d9b78fbed1d2d2da9087fa2e5a3a60691766e1d16ac69d4cf3634b82a5c983f2d6a55090cfee2ac959bd7af68dbce13f770c38d37c1effffe1fc1fce905aaaa96770d310b54ab2472523e99968454518a9f71fcac216266d94a902ae0e19f6d35c8cf37d2a5542f3dd33eb732afe560664956fcfcf549a5ae05a19de30b1352e9eca024a3e2bfbefffb5f09dff6d2175b93f2fe97f7bffcfe4f9dbf2fb15cf03f63e719cba1541ec7d6fb9fa13fb1b88567e55fc7ce51f02ff817fc0bfec37d0afe05ff31bf787eb856f02ff84fed47f02ff84fd94ccd17fc0bfe4311fc0bfe05ffb066ade05ff02ff817fcfbf178a2e03f4b812475c890084eea41128ec78253b52ee6471888d878b8d60f80af2b653f9548315f9aec377cf8f9128b612c49527b8fe98a2557ea1c9bf8e3cf8dc5354cc2546c623ec7fc48cd493d7062b91593aa792940f2bdf03305cc543efafb08bf87671ed351e5b7ff59a5d7d7efdf17fc0bfeabfcf1e70afe07c3ff962dd3f0e637bf1e9efef4a77aad00ed4b5e59e2815dcb15ab780e5741723ee33c224e80ab86c5242b401f80abed6877afac7c65fd37caacbf74519221a892121852169f2512b55077f1d729be1b020613c7cab8dd71c75df0677ffe31985f58a40a4eba51a52565ec6b1f63c50f9416c055575e0a6fffc9b7c115575e825e783e6786cb84cdcd900056dc5b5dedd278ae72f209095837dcf03578e8a14385cf00e363c3303a3a02ddce322ccecfc1f2d202ecdfb71bb6efd80e478e1d83f9f905e86085328ab9a6f685d87e706e6101864646a1ab6d8bc385799a373636020b8b0b543c6c7a6a0a5657966174649808605845c89c95a9e6852d03b1aa1612ba868787b1cb1cb520c4394c4ca1aa3df68c913e82f1552d059de515d01d43fe2956d1d9e8c2b7d54227914b5a5c292b23b210dacfb539432612a12e6d2b86756d3e3cf944db3fa03c6ab7db30313e5ed136d0ce56de6ac2b806d3fe32b3b8d425a602a1b3e45cd5d0cc4b3ae0b5e79365ca110f7bda0ef62dbdef14acdaf6877ff42770e79d77d92a6a766fe0bd27f9af74d254cd03ce573039a54a8228f07332782ee6baf73f4694dbb3766bb47d8e615e77ad3e266eb10e94768480e5de37763d7e47cc3dfb59cf80ef7fcd2b1cd151defff2fef7ef6de6fbdfdf47f83d3cf3988e2abffdcf2abdbe7eff7ee853a82b753ea91c89d989c5be6e5dcc0f5faae21dcb531f4b827fc1bfe05ff01fb32ff817fc87ebc24fc1bfe0dfb723f85f7b2df887e83ec2ef827fc1bfe05ff05fa5a3ca6fffb34aafafdfbf2ff817fc57f9e3cf15fc0bfeeb72246647f0bf36b6ad77bffbddef0f1786924a98f0c1e01b6c02b298fed475ea6112db5cb897d0cfd4c3229c13fb1eda49cd89f913da0ee3e64b9828a1bfa1edd4fe530fef3a40a5ce29653776bfcea7945f299bb187785ddc63f6526088ed2f66b76a7f31bda1fed4dad859c572b62a06a9fdc6e6c4f22f8c65953f827fc17ff85997e329bb55d7e712fea7a626e1c28bce879b6fbe0d8e3f76dc8d876db64c05235db6fad3b63d2080ab1c9379b6d05a2caefc6322f35a06ba5674605432294783dd2768228ba8ac9c07d62657c1f2f55075201cc69539928372b8ebcebbe0e24b2e822bafbcdcc407ab702199c82c0457fa467967cefe2bedc8674832c10a5b4fb9ec62b8e28acbe11fbef00f70ead48c3d875671af6dd76bf217ab1ad94bdac7c2fc3cb52fbce8d28b60dbd66dd06e0f0356b6caf30e8c8c0cc1d8d804ccce9e8493278e13416a7979016666178acf4518191e3215b28a3359ed2cc3eaca2a114b464746a86d20123a904c85842eac728531c6168228e833c75341d94212ab6575f32e3988ed0bbbab5d22880c17b6569657681e561e1a1a6ec3eccc1c4c4e4c10098c88376d537908495b481c6262095520b3b9c3ede84c753373ae180f43aac9a96d9d396e26eb415945cd7f56e4f60c4c286daed96befec52fc9f6ccd14b5762ef542acf90b9728556b7bed22fb2fbf05a1c2ea632dc8a1059ffef45fc31db7dd4554414c4dbfdb20552ed3da5674b23150861ca73cf54a9b6b8c3fc50b3fb5a9d0a49d69551678b3d5ad5cee138ecda668d72a27ec7fcff77e175c72f145600aa779e4ab2102682d01cbe0449b73c636869921e1ad2caf16f984f732585acee1431ffa1df86fbff19bb0badc75fb70b1d2651e1bd2932a8959199f74297e7b54f681e2006bdfab3ccf3d6ba1acac67f4e4f40ce0e71bcec9fd67abf78ca6b5f63ad765fef2d835d75c093ffbde77c2532ebfd42d97f73facd1e1db0fd7c8fb5f7eff8776423db13389f916d39fba96dfffbdfe86b653fb17fc0bfe05ff82ffba98a66216ae49d98dddaff329e557caa6e03fbe4ef02ff84fed5ff02ff817fc0bfeeb629a8a59b826653776bfcea7945f299b82fff83ac1bfe03fb57fc1bfe05ff07ff6e23f8b2540b8415f9a24632c506120aa0e9ae7c59cafd2e7fb16fa1b3b942ae0843afcf9a19d58fcc258a5f61ada08f7ea8fa7f4a5ee579d6d68273627fc0b25d49ff22b3527151b7f6eec3c5239193bcbd44b26fc9e3a43df862f2950d5cd4de94ec53216577f7fb13955b854150f5fc1bfe03f1c17fc43747c3df87fea53af8637bdf9b5b06ddbd692478273f80f75e0679679958a4c8519ba17f9cc22780628c907aeb212706c73439642c21211a30a2d3866bf6b28495d6e3fb6452193c388f043440cd49b531b40cb9d8053274fc3077ff943f0d02347a8c51f928e88ac91a9debd2a650960b9db91ca5bc55edb859d365065ab627878b80dcf7dce33e15fffabf7c2c103fba0856b73edaa7011f1451bf295a904c655c100e61796e07f7ff4cfe1c8d16370faf41c55b1c236850bf30bf0c8a187e0c1070fc1f2f2121c397218a6a7b7c1eccc2c0cb733da4bb7bb0ce3e323b0b8300f1313e3440ac11684adf630cc5822d8dcdc3c2c2dafc0ece979b27be4c831daebea4a872a5fa17f8bcbcbe4e3dcdc1c8c0c0d931fb837bc872d0fb1d521c6080952863c0530323a0ccb2bcb14ebac95153e2c9af3c91491b2a84251318e6d0af14890948555b14c4e29fa44425a8e042e30a4a0bc6b5a15b66c4b3843983395b7702d00f4b4b724dbda6499b62d0c95774f7355ad40f2e04f0fd21f2f21cafba7b1f8e689d8a55d55a6c989b1e23c7ba773a529ae2aa76d1f40b6ab3d55f467fdf28d69603c9b3fcd77189f0acfdfcca3027268c79b0f60da6c9ae7538f2a33a6837d4544590a5df124a1f3c23f22f00db55da5ab1b6ebc197eeb37ff272c2d758c9f7cae164f59e68c5aa299f2b41b129855ed764fedffc2e72c9479c39359935fd9ca3d73b4a940c7f3b86a203fc3583a968858e6a43957d487043a6cd5b963c7f6e299fb3aaa3ee8db90f7bfbcffcff4fb3fa63b15cb585cfdfdc5e6a462e17f574a7eff0bfe05ff827fc17fcc7fc1bfe03fb4e18be05ff02ff817fc87dfebe6a6740bfe05ff82ffb5f110fc0bfe533e09fea1c7aee05ff0efef293657f07f76e13f4b256d2a11538642c5a1637ef05207114b8c30717c7b75000efd09edc4f61b1e707878a9bdc60e2c4cf63aa0c624963c311d4dc113ea0e932ba533a5c35f9b3a4bdf5e683be68f3f37954f29df62310ad7fa9f55eb5339119e756c3c96effebcf02116bbf6f5c4f65c95174dc6530f20c1bfe03f363f3626f8ef1fff58ede8252ff96678e52bbf9baa286948efc954bdc90ca1812bcb58820c567961625528788fffc81ffcb36b919043a344882aff688f18036e3bc8e7ca640ad481df910891795569987545edc7c0127f72b8f9a6dbe043bff1db444802b09597c0e8ad152e1ba4f9870a407bb80ddff7eaef81f7fdcbf7c0fe037b8a5ba6929472f1538efc61e2a2681f486c7ae8a187e10f3efc4770f2e449387e7c96da962d2d2fc1e4f838ecdfb7074647468bcf7de4db539e72118c4f4e126566616181d662e5b213274fc0fcfc3c2c2d2e16315c85b1f131585a5aa6ca55ad96396b2476edddbb97485923a3a370f2d429b39d221ecbc55c3cfbc5a525182fd622e96b796585d6a03f6363668c620a390c6195ab8e395ffc24020a988a60ddbc039d6e876c522b3a55e62292ba88449529aa10065c830aab2ee119689303b96b49a9897cd5d586a453b626d4eedfda956e829280c339014f34d1960c652a5e4d4f4f53f5b2de1960f7aaa99c97f26ff87fa1661c5346b7ca12b15139fd6145aadeb58ace37532dfbbcc9607474cce21ed76594279807795e62a399201634f9d5b695d4d0d6fd0f3e0cfff5bffe063cf8e023a5affcbc53f5fa39e788ecd4ca7ac65afc4c81f23946388592a0c5f3739b77dc269543dbe2678b1543b8d4cecfccea66d2a00dbfd3893239310e3ff2daef836ffdd66f81a1a1b6bcffbd79f2fe4fafdfacf77f981bfebcf0ecc23c8deda96ecf55b1889d492abee19e63d7a9f38ac5a32a67fc18f9bef9f6623914fa94c28ce05ff05fb75ef01f17c1ffdabd0afe05ff827fc13f7f17fc0bfe05ff827fc17fafbdd076cc1f7faee05ff0effbe6db13fc0bfe435f04ff6bef57c542f0fff8e23ff31d0827a70e22e664cc117f7ee8782c8963008ce94ddd0b37ecfbef7fc60ea5ea815017c4d0af586caac662c9590582aa075b789efe9c58dcab401cd31b03580a0875b953673316eb1878c2bdf8d755b10a7587d729ff620f47ff3a66335c13ee29bc8e3d6c62388badf73fab247c0009fe05ff757a05ff1b837f247dbce635af84673cf36944fec8bba612115782e13fe7177fd7869c90dbefd1fd787e29f0720fff506f2b736d0cfd3fb2817f4c82e0789b8038c20e1174ec38eb219b962c91dbb938e7931ffb147ce2139f34dc2cd52ed6a26f2de85f34b5886bb7157cd777be14defad6d7c38103bba1d52e6c416eab126968615bc2962124655edcb1c2d0fdf73d007ff9977f5dc47a0546c72688dcb2b2ba0a6363a3f48924a74e6785da0822410449547bf7ee8389f171e8ac766dcbc1e562ed289c2eee61052dac20353e314611c416868b8b0b140bac68853120dd2b2bb0da59051b65db26d17cc7f68578f658e98cda1a0e0d335d0a969696681e12ad700e56b8c23898968448c669538c5190c4c52d09812b1261b52aaaa0042e774c6c3056a6b59b4d18aaa4450419db4e92a83a5ad9b3ec9a9cb0fd0a73aa8e64720294cb9e2794f8b8999a9a2262538f980d9a7678b9ff1c2dff12ec2ad3aa2f37f18b3c15ed5920b1ce10b10062cf2a559cf790210c81c10c11edb0321c559d6a8621778ec68ac90b9d512bcce5e555f8f8c73e01ffe79fbe5ce68ef7bc5176a3f1470c07c1ccd599f74cd796506573ce8453af89053d5bb2cc3de7e8d98415ab5a2d47aa0a9f61263286c04573587fcf9ec191b0902cfabce77f13bcea55df0b2323c3f2fe97f73fc4e44cbfffc33d85d7febe52eb52ebfdcf2af16318cbf3540efabec4e29eb293ba0e7fbff3775f62384badf77d93dfffd06343f02ff8f7fd12fcc77df2c704ff82ff94ff827fc17fe897e0bf5c13d329f817fc0bfe05ff827fc17fb847c1bfe03ff63df441f02ff84fad7f22e13f8b25462a59c2c0874e541d7ab82e7508e13d3fa829f084123bc4d4dad89cf0b0c264ac3b983009fc4ffe1e4b8cd0afaa440af7eb833f7c1084871f7b3887b6ab7c8bcd0de785be57ed3976b6b11745d5032f955f2940d6edd9b713ae89f916ea8fed23dc7bb8a7d84336f66008f32e1607df9fd44387d7fb7a5263e1b5e05ff00f20f81f14ff28975e7631bce31d6f83a75d7b3591b054e94c6fc7b4c8b9921ebe46bd769dbb46c693613da151336e490bc0fbf2fffcf3096c86a7cf042dae84957bf121e2057eb715b2eebfff01f8d55ffb1f70f3cdb7ba5661ddaea65666ccfcd2aee1615accfe8a7feb2eecdcb9157eec4daf8777bfe71d70e185e7015612ca32434a1a1a6a11d162647884c858e83d568c424162d557befc55f8c427fe0a0e1f3e0add3c83b9f925585c5e220213fabebcbc0c539313d02e746145aaa5c585e26c149c9a99213d68e3f8f1e3707a7696c84cababab800497c78a3124496dddba0d8e1e3902dbb76f8353274fc0caca6ae1d3308ca32e2454655825ab5be86c9b4a4060e287c78224a0552280adc24a67950857b816ab6661ec90244364acbc6b7c29fc5a593164312ad4c404b8ae695dd7a24a601d133fa50d2908dc0f3fd3b69133807ae19973a00cd225c9ce1c2ed876938ac8625d47c62bcfaeea04d59a848366a26bfe1aaa888ee31e5506d3d3531e19cdf779ed7f3878ddf7a242c3a44b5758f733defc9bab9851fb49ab627acb244c4e4e15e7a82817ccf9e2d9645e1bc00a516cab385ec2190eb408fe2b9d1cbef08f5f825ffbf5df84a38f3e4aba31c7c03e27dc72f0f867f4cce9554ff1d16612b5b13481b2eb74498682f2b9013c27f24ef4abf6515c6c753ebac7ad315b2d3797ab6c717b425ecf634f7dea55f0d33ffd16d8bf7f2f444324ef7f79ff83fcfe8fe54ba82f16e7704f29a98a6fb8363ce3586caa2476c675fb8ff928f817fc876329df42fdb17d847b17fc0bfe05ff827fc1bfe05ff02ff817fcc39a7b827fc1bfe05ff02ff817fc0bfed7da14fcaf952712fe5bef7ad7bbdeef6fd29f1453ec8fa53e639b49dde7ef3160a50e2be66fca8758e0537b8a3dc8aa001bfa51e56fccbf985fbeae5422faf753b18a25440cb4319f63318c9db5ef636c3cb636f550898dc57cf13f53f3ab1eeca1ce303ea9f9557bac5a9ff227158394ddd843349513b107746cbfa9353c1efa24f887a8cfe15e63fec6fc8bf9153b97d03fff7e2a56e11e431faa621c8ba112fc6f0afef7eddb03070eec839bbe760b9c3a35db53bd857cb37a5c95181c57ca559ed2acd77e661c9b3c77ad037b74d9fbdc024c9b00983de098b505817d0863cdeb73437cea720b30ad7bda8da1865333a7e0d8b1e370ddf5d7c196e9691b03ebbd02cbc34ae3c2040cffb46bc738323c04575d79055c79f55570cbad37c38993a74cebc32e10c12bb7d59f90d4446d1e734d2497ceea321c3a74085656bb70fe05e7c3f4f4d6c2a7adb0bab25ccccd60ebd62df0d863c7e1e8d1a3b47e66f6348c8c8cc38e9d3b6165791926262661eef41c6cdfb10d66f173db766a713836364ed5b1706b48909a9f5f20d20c92b4f03ec66f646494c8571313133057dc47921355b22ae661f5abb1d1513a1fb483ad03918c83e37c8ea807f78215a8b0ed215648c28a5b23a3234404cb5a86a0d3e1efda7e27e29df123d7e57969aaf0d432e7af6c7ee399628b4acca95699c3384efc378c235528ea4d8bcc554082c6a21a4cd64d59569586ccbffc1cd38064c016fddd71fb9df0d9bffe3cc514f7c3980a5312f3014b493146cde6836789b2e38c73ac5765c2e7fe38e779358e11be6d25372cc6b57bf77678f39b5f0f2f7de94b60bc38df56dbab5ad734c67c46a8d772c20ccf4ec14d37dd0eeffbb97f0b77dd791755a503de0ffb6e0f96c97d40d5bc0c860c3f2f3731d2f6f999298763bff29cea3902d553798fed6496c4e99e5d10b428cc4dc53565c9697e6540f79cf1aef9f979f145e7c3cffddc3be0e94f7f5af43d0220ef7f79ffcbefff54ce843ec9ef7f88fa1cee35e66fccbf985fb17309fdf3efa76215ee31f4a12ac6b1182ac1bfe05ff01ff539dc6bccdf987f31bf62e712fae7df4fc52adc63e843558c633154827fc1bfe03fea73b8d798bf31ff627ec5ce25f4cfbf9f8a55b8c7d087aa18c762a804ff827fc17fd4e770af317f63fec5fc8a9d4be89f7f3f15ab708fa10f55318ec55009fe05ff82ffa8cfe15e63fec6fc8bf9153b97d03fff7e2a56e11e431faa621c8ba112fc0bfe05ff519fc3bdc6fc8df987d27af7bbdffdfe2a634d94f3fd5812d6813dd4e37f0f1d8f6d24d4edeb482564cc6e4a4f6acf3100c674a67c4aed3106b4506f6a6eea7b4a47cc175f77d3bd5681a76e2ce65f2a8f7cbf9bce8f3d8c620f921890c27b550f8154bef9767ddd557e57f919da4fed23f56008f554c5258c63681740f0af94e03f9c1f8b576a2ce65f2a8f7cbf9bce3fdbf18f84817dfbf612c1e1b6dbee84f9f9f95e9281fda43fa380c6b8f20b587d4c6ce0f67f58fd05ef30418a7cb2956d32af5520ae61bdec53ee93b7780ff65ab10f6613a53e24601159a36477e739b7aa53f0c0030fc2f2ca327cf38b5e084343234408a16a3f34c1c610d2a298b6611c284c6baa0c74e0bcbd70f14517c3bdf7de07478f3c06dd5cdb567d190c0d1b6247b7d3311a94d96bbbdd82871f7c98f4eedab59bda032e2ccc93ded37373303b376fab680dc3f6ed3b617464c8b53d7becf8633033738a2a5c2d2c2cd03eb0552156cbc2fd9c9a3d0dd3531344d21a2dc6902035353969aa5775ba44aec22a5643ed16919fb0da95a9ac65e2beb8b804a3e36340d5993498392b2bd48a0eaf57a8e296395f0ceee8e8282c2f2d11310b095686d03544f6d069226c51f5ac0ec545a9ccb6a5d38e3483dfd1073c5f6c6f88641f9a67738d4847967083be2bdbc212f84f776d7bbc9a430ccf54f531793d621d53ec5ca6a975241e5eae5b70f7dddf80cffcd5e760a9887de91bb8fcf6c708335a3b0cc56ce9604d666d730e1b4c98fc3764454579a4880497c365975d083ffef637c3ebdff0ba2277c66dae23c6f13ccc5fe435b5460ca9ce4c2c61a6e0d86333f0bef7fd1bf8a77ffa52913360aad1d96741e9b77d46d87367fc73152ab0cf2ede3357b2235bf64f7941c05cf249a3ee9ada5ee6ae621e1d8fd5cbcf11cdf1d6a6e215e2999f9df48c81f259c6cfbabd7bf7c01bdef823f01ddff1b21e3297bcffe5fd2fbfffd7e6654c5f18c7d02e44e2dc743ff2fb5ff01ff3b10a17bedf4de70bfe05ff82ffb80ec1bfe0bf2a2e611c43bb00827fa504ffe1fc58bc526331ff5279e4fbdd74bee05ff02ff88feb10fc0bfeabe212c631b40b20f8574af01fce8fc52b3516f32f9547bedf4de70bfe05ff6733fe898055b7a8cec1d886c3f5a9248805387650314085f6431d295f527a52416cb26780f4c328f431f5704b8dc5fc6872b831b0a7f69f4ad2985fa958c4fc0cefd59dbfef7753bd3100c400e3af897daf8a41ea05538597d43e62fbacf223a527b636fc1ec34f4c527a05ff82ffd85e63b188f919deab3b7fdfefa67a9fe8f84782c0654fb984884137dc7833acaeacf6f8c38404feaeed5a2647b46c4b2ec0f9b6028c134b50207dc6287db62ca1812bcfb4da6d4362b0fac9c7c07f65be18dfd16fab8f49147ccf91b53c5d7927873befb81bc626c6e0ca2baf247293d1e2ad836ae1ea3bc01e2843a6bae8a20be0ba673f131696166061710ef6ecdd092b2b4b4442cab1dd61b16ca8d81fc668a83504482ec196830f3df41091c29ef5ac67c0d4d4344c4c8e11116964a84dd5a832dbb2717864041e7af810b94924a64c5185abb1b151aa3cb56fef1e78f4d1234486c2289d3e7d9a5ac79d9e9d21a21692ea5aad362c2e2ec2f8f838f98cf691488504296c53887a96969689f885e4200c3d7e67a24eb7d385a1a121da43bbd576b9b0b0b00843c343a6f2502ba30a55a62a91a9a6d459ed98fb5687b959e402b5296c113988da0eda33c8f30edd5796dcc2cdeef87ed6ce28a69cbf4c1c7379ada06c635823a967c9868b49dcd2272230758b3db6888075df3d0fc0273ff9d7b058c492854851aadc0512fb98dc633093595c6aca0bfe8bbd2974f0a71039ca5425538e6cd4815dbb77c00b5ef01cf8c007ff23bce4db5e0c931313c599173947505d35042347726a165f6df76ece3d83471e79147ef1fffd007cf4a37f51e44346d5af54e0b5b255e6fce7179da8c59fca7adf1b1c63d4639b243a3214e74e995f963c68d7729e71acc3e7dd9af70ff9559237b5d587b8a44a59c5bc5dbb76c0dbdffe66f89eeffd6ec2a81f0fdfefd4b3dbff1e7beff4c457deffd179a97b4ac9fb3fb6cf2a3fe4f77ffaac7d3ba10ea5e4f77f189bd4fe05ff82ff985ec1bfe03fb6d7582c627e86f7eacedff7bba95ec13f447df66d54d9acd22bf817fcc7f61a8b45cccff05eddf9fb7e37d52bf887a8cfbe8d2a9b557a05ff82ffd85e63b188f919deab3b7fdfefa67a05ff10f5d9b75165b34aafe05ff01fdb6b2c16313fc37b75e7effbdd54afe01fa23efb36aa6c56e915fc6f2efe5d05ac9803a183b144af3aa014a0aa029cda5ccc27ff7e4a67ec5042892541381ef3b16aacea21105b1bd35325a1cfa19d94be5872c6cebace9f589c20e10f4a550ec41239662f652366a7c9fc58cea4ee55d96c828354aeaa04e8637e54c527dc6b6c5dec6111b31bf32d5cdf74dfbc2e8c45ec7bb80fc17f5a62e7e8db5182ffdaf96713feb1c2d1fe7dfbe0c61b6f86c3878f9484265d56dbd1b6fa8ce2b510103e3cffb8520d55d2f2fd661fac1eb07a72f6cffbf4f7188b0deb421bb89ed628e5aa23f9edc1986871f8d147e1aaabae800bce3fe0ee53fb3a5d4f29317b56442a51aa6b74535f3c4544a7cb2ebb0c8647c7e0f4e9b9c21f43365a5a5c8691b121e874b01a97a9c8852df6ba7987dab19d3871024646c6e0e28b2f806e6705e6e7e728322bab2b448aa21682dd2eecdab51d4e9e3a05c3434370bc58b375db0e5b510a60797919868747607969992a6161a52b6c91888429f401f79de7a6dd5f6e5bf82dcc2fc0c4e4387456bb54e16a6a6ab2274f88e0537c6245aef1f1319a333232625a1216e3abab1d473ac1712477a10f39953402226365346f1586da86a0857e20798b5bc87132a14d7b743d8418ae684495b22ce12553c51e546ec82e487e29f684f15196b0644b6af5e462d5799e0921bc1013c9da6302963204ac071f7c183ef1b14f9bb690de1a124e7528b16ac85986e406bc67fba7eb7ca1d0b4288e86aa8603396cdbb615def8633f5afcbd1eae79ead594679942921c252c557b23bfad63da62a16ee3a6cd2412f172582dcee94f3ffa17f0dbbff361aad08674aecc5644539ee7ae1a15bf0fb5c53de54556b685e4f353a52d0d25d1cc0b5fcff3c03d2b2d0994620a260f914c85ed31b98d213da7ecf385520befb5b2de773b13e36c25ad6ffbd617c11bdef043b06dfb5679ff47d6c9fbffec7bff87fa627e54c527dcabfcfeefc5b312fcafb92ff817fcc7ecc67c0bd737dd37af0b6311fb1eee43f09f96d839fa7694e0bf76bee05ff01fee5f09fe7bf457d96f9237827fc1bfe05ff02ff817fcc7f624f84feba992d839fa7694e0bf76bee05ff01fee5f9d61fcf710b0c20d55012315ecaaef2907439b550956a5d3d71d7bd0a47c8bf9d3646decf0fdebd046d53e6209a822004d2553d5032c965855a0083f53099a5a1b3bc798ed14e862f663608c7d4f8d55c5c6ff9e028a2f2136aac01fb39d3a575f7768bfeaa155956390d8672c5f04ff82ffaa5c15fc43cffe361affd3d353f08ca73f156ebffd2e3874f88899e3dbf063e0e79aadfc829673db7250d99670aad5b2dfe3676708184095935a59db11b1aca7e67fd60ffcd3aaf7478db276c937af2d1b11746c2cf003e7a16fc78e1e855b6fbb1daebae66ad8b77fbf2561d86a5ae56a707c19e5dc28f3cbd2c7ccad0c5ac51e90b8b173d70e78f6b39f05bb8bcfd9d959aa2e85648d8909437442c2125e539529c08a501a969717e1be7bef2592ca962d53c5194cc3830f1da1ca58a3c5bc95d51c4e9d3a05a7666660757505969697e0da6baea5b67fd85a103d383d374f31dfb96b27cccccc929f43c3a348bd2262d26a4713912bd7b96d9ba6616a6a8aaa60b55ba65de0e2d23220a1072b832d17be22390c2b6ab5da43b40e4963a80bc7b0425a7ba84deb86a8c5a05987add93222b0748918857142f215b658ecea2e8c0c0dc3dcc202917b5a44c4b2442f246bd90a5154dd2933e3a81f095be61cb5cb3b24e4645c858baa3869b0ac389b2780acbc9ef38bfde1b29a296bf2a0d15f549c4670f44385f98eed311f868f7fec93707a76b1671ee72de628167fca21b7db546e9b58458baa8e2965da383a074c2cdac579606e62cbc9966ad3bae19121db764fc3f0e830bce45b5f08fffa5fbf177ef4753f04179c7f5e7116486c2b7246e5aeb523e552b051c2b5c520b527a496929a8e81ba821ab0505daae5a50efce5a7ff167efedffd0738f4c85130e4333b5779a44ba56cfbd39c5a2dba67963d0baec0d5a20a6096bce7d5e36a6559490cd5a6652093a7982045cf113b46e4526572c7e832e2c85ad6275c87b90d76bfa4cbcec3169b64bbb8ffb297bd04defbde7716cf96bd365de5fd1fea92f73f24f584b189ad91dfffcd70157e0ff72bbfff05ffa19f827fc17fd53eaa74faba9b6238e58fe05ff09fd225f817fc876716ee2366a36a1f75984ded8d45f00f6b74c5ae53e3a1ad2a5d827fc17f7866e13e6236aaf65187d9d4de5804ffb04657ec3a351edaaad225f817fc876716ee2366a36a1f75984ded8d45f00f6b74c5ae53e3a1ad2a5d827fc17f7866e13e6236623ebb1684e18dd8f7f030c2cdc636153adccf2652891bf323f43f04424c570c40299ba9b5a97da4e657c52c5c174addb9c40014d315fa1ece0d75d5d94b9d69ccff3ae0fbe3a9b8540123b5a7705e2c97fcf1d43e5271520900579d6dec611bb317db7f5dcec4f23edc73f8a2a9b219fa5b97cba9bc5082ff64ccc275a134c5a33fa604ffd179b15cf2c753fb48c5496d10feb76edb0ae79f7f10be7ef737e0f863277af784fa023d44b6cabc8a303de40cd3ae0b093c489d306b4c051b6ae70786e440041247b9e26a33cab41ad3da912980edb3cf768cbfafc1097dcf3c528a193c79f2043cf4f023f08c675c0b3b76ecb4f30c67c72d344aa124d528e8a946449f56b766bf8a09ba0307f6ef81cb2fbb183add1c46c7c66165b5035353d8cead45f1dabd7b2774bb58412a2312d6c2e2223cf8c08330363e0efbf6ee2572d2f66d5be813b91d7b8af958d50a493448d06ab5149c38790a8687876064641426a726617979058e9f384ead0bb1a216ee71d7ae5d448ac29683274f9d84d1622eb60c1c1e1e85ceea2a2c2e2ed17af47b7e7e91ec2139abdd1ea67d22010a893a0b0b0bd4ba10c95f745e60aaa251dbc8621fab850dacb68515b85aed16c53fef740d91ca1254da965c871143221aeaa62a43d842b09bbb7325e28db2d5b2865a86a84604179be3b60291f62bac8121ede05fb79b53a52f931239e505117cd6d087604d6bbed81fe774d37fd2929539a50db98a2a9215b973f8d011f8d85f7c0a6666e6285fed2cbb67b08d32732237997b39fd9bade596f484f3a9b21664b68253460424e41821e98a7376642c83a1e2fa29458ebefef53f0cef78e74fc273ae7f567186680cd7e7d052e58eb07a97c194fdd3a6d11f93b394ca81bd2ce162bee1e8ea6a17fefe0b5f82fff0fffd32dc79c7378ae196bd9383e13fd9e7957d8ee077aeba670365f2c2affc0525f695cd2305def3d3ae5559992b3dcf0b7ef673b5358f50c53e685d12b2943f177a9fdff887644b6ce3faf33fff2fe1820b0fba530f9fe5f2fe97f7bfefa7fcfe6f6633f4b72e97537991ca15f9fd0f6bd685d2148ffe9812fc47e7c572c91f4fed23152725f8aff43116d3186e623921f85f3b5e85477f4c09fea3f362b9e48fa7f6918a9312fc57fa188b690c37b19c10fcaf1dafc2a33fa604ffd179b15cf2c753fb48c54909fe2b7d8cc534869b584e08fed78e57e1d11f5382ffe8bc582ef9e3a97da4e2a404ff953ec6621ac34d2c2704ff6bc7abf0e88f29c17f745e2c97fcf1d43e52715282ff4a1f63318de1269613e732fe5bef79cf7bde1f0b48b821ff33e544ec5e0a28b1cdc7f454e94ff918267f2ad163a049e90fd75525a6ff19023436ee4b2a11abf652b7f7d41e9afa1b9e61682fd419ee23159fd85ec335550ff9aa7da76214d39b7a2185b6627ea6723816cf50775d7c9a622ae6436cefe1752aaea9f54dfcaef39545f02ff817fcc7ededd9b30bf6eed903dff8c63df0d863c75d4b2e6af5d7bb29434cd0da9112f0bb214fe077e05a3f607b8a95fee25ab29b95be679effb68291623b783f8cad3706fe1e7a226f6c980a5dc5f716dacfe1c1071f2cf676029ef6b4a7c1f66ddb8c2ab03a8877a51cefca1a0b0836cafbe3214dad07b19de3ce9d3be1a94fbd1a2ebcf002aa1a35313141e3f3f30bb0b4b4488425edd9c3167e0f3d7c880839175e78218c8f8d50f5a94e67952a606dd9b2c554a22ace627e6111766cdf0a278e9f80a3c78ec1d1a34761726a8a885248924282d3e8e81855b8425ba74f9f86bcab89604595b0ba5dd2353939058b0b73744e53931314cb2c6bc3d2e202919e26262761d556e2423215929b903886242bacf6b358f881159646c7c64c0bc291211a5f5d5ea539a8b7dbe9daef9ada11a22e14d35eb145d1eb14b6f0bb3d4643b26a2311a6655b0d9a73a37f98ec07e61c711f481ae34a66e81f12b46c01289bdb2d883c7e1b891a74e15a4df6537b971951968e1c7d0c3efeb14fc18913b30186cb65ca92ac5c5b4660929806263f0111cd32aa8ac5642524eba1aeceea4a714ec3b06bcf76b8ecb24be0077ee0d5f0d33ff353f0dddff5cf60efee9da68255114bcc0f2c7fb676df6bf35d29ed79913bbc124e086f195570fb9bbffd02fc5fffe6e7e1e69b6e0360bcd39a8ed92387c456a3eab14c7aa0c43f3f3ffd8872bb5430ad4c7de1674de63f9b01dcf3ac34a35c053df74eb16b9894955b5dc0cf2f302445bc7fd55597c33bdff9e3f0cc673edd3e96e4fdefeb8beda1a9bff2fe97dfffa19e94df75beb2c8ef7fc1bfe05ff02ff817fc2bc17f343e827fc1bfe05ff01f8b49180bc1bfe03fb5f7d41e9afa2bf817fc877a527ed7f9ca22f817fc0bfe05ff4f76fcb7ab0cc69c8e6d36e6542c70b124490529653b363735e68fa7eec7f695ba4ee96cba6e105d753ea6ee5725610aac3c5e058a2636792cf5100ee7fa7a703c046d2cc153895d2755fef912cb9baa9856e59e7f1ddb6f2a7f52b9913ab798a47ca87ba0857362e3827fc17fcc268f09fe07c33f567379c10b9e43c49dffe7fff94f70fcf809d71a30d4cf1af98e6997a640dbfe63ca1832157c14b84a41d8d20e0be910550be75b1de40f5530d246070e2209876d72fbbf54bc7549062b630065751c8c8102aa0cf5c94f7c0ab66fdf06fff27def815d3bb781c6b6789a892cb6b199ce019ac2a95887adf9b8a2d3e4e4185cfbd4abe0e0c18370fb1d77c1d76eba05c6c6c6606969091e7cf021d8b2659a2a52cdcfcdc3e2d222ccceccc2e73fff058ad72bbee73b01094663a32385be5598993949adfbb025df96e929aa2ab463c70ec85a58796ab1d8cf2a55a3daba6d272c2f2f11b96a7e6e9676b1657a1ae6e6174d55a9f610114630ee4b4b0b74bdb4b40cd3d35b6065ee341170b01217c52eef42a7d08995b6e60a3f2727260abb1d22732d759761bcd80bcad2e212ad5b59592dfc352d0887b361aab2857bc17f90c48715b896575688088604346a31d86ed39e906896b50c518a0855c53f2b2bcbe67edbb42bd4b6bd25915e8ab954454dd93cb46df898fc87e4202aa0a6ccb9343fc433201607b4d9e2244c0c5a6bb09c192e14919bb46594e19e99643454ac41f29acfedc236845d2252b588f0373d3d599ce71811dfb615b9fe9297bc085ef8c2e7c335d75c0953c53dc3a132a4352c644575e95a2d43abaa78af93395a9c83019cc115f969abdb21c6fee10b5f847fffef7f116ebff5ae429f6ddd474764aa58216151e976ef3b88ab56d1b320b3ed124d152c82afcd5f6e13d8b5be32510af8baf88ef1a1b560aba569afa29e0999adaa66c880b9ff7bc2ee935b18ba3f4f0fc679d7ae1df0d6b7be119e5f3c3391a8e8c74ddefff2fe8ff97a36bfff43fdf2fbbf7a4fa9317f5cf02ff80ff724f8eff52314c1bfe07f109b3c26f817fc37b11df353f0dfccc7d47dc17fb5af827fc17f28827fc17fe89fe01f92e3827fc17fcc268f09fe05ff4d6cc7fc7cb2e0bfed2faa4a3afece73fd7529c7aa82da2f707c89ada94ae65872d581a7ea61914aae3a3d7571e3efe1fdd89c54b2f1daaa73899d515552fb9f757bea27e153bec7f6da14a0e1c33b3637069c26df43e9e7bcfdeb54ee35c9337f6e6c4e2c97fcf1943dc17fda86e05ff01f93cdc4ffc8e808bcecdbbf151e7af861f8f55fff6d6a2fc704187716ba2431e43912414ceb33338ef38c4e1b5943bcb2b6a84a91ad62641d2172067568cb4d45244515a5cc3de0988121dd58879d7df03166f740edef88bc65c838c66fb02d0f81f6f4e10fff216cdfbe1ddef0fad7c28103fb815a166a4d8431c8cc3e68dfce26544b6e8825d40a2e6b5105a2eddb26e19bae7b3a5c7cd141aa36f4f02387a9d5e3cca9595859598123478ec2f8f818dc7bdffd70f2c429f8c23ffc13b5207cdef390d0a161dbd6ad44d0426214ce3f7e72061e7bec31181b1922b2dcc183e713f163efdebd446c5263a364ffc4c993303d354515b390208573d13724518da2bd7bef03acd085b1bee7de7b891086a4a7567b846285ad0bb15d2112f0b08a178615c955d8786e72721c66664e933eacee85c4a0b9b919dac7d808b638ec10b1080930d862710aab69753a54956ba83d64497adab4a7b43145e25a6ba85d845d13098b2b9111fdce9e236500aded1a020c92f5bab96b7399d141dbff88a27c04efe0fa7fd66f8ad0d6cc3ff8bfa1a1e162df867408b6b65566ab4199968066e75ab70de98c72cb542b6bd93de1feb1452592ae3007a6a62760d7ae9db0adc8b35dbb77c125175f04cf7bee73e059d75d0b5313a366bd254fa119853426223e5a4ca69e3bcadf03d87694a6a5a2392b0c3f62bb0537df720bfcf2077f0d6eb9e976b0902bec6aa700559956a50697f4ecc973a3839c523dc44fa244d95683a53ff67d821aedf389ff233a2bf24f792d06f32cb32d2bbde73d9484336a91696d96cf71f35c22821ac74495ffa18e9879fbdbdf543c2b5fecaab8f5ba27ef7f79ff3ff1deff55fef3dcd89c582ef9e3297ba9b393dfff827fc17f5c04ff82ffd49c981ec17f5a04ff82ffa622f817fc0bfe05ffe198e05ff02ff817fcc7c64311fc0bfe05ff82ff2a7db1b9febd8dc0bf3a74e890ae52c0f7fc45550f831000a9fb55b652f363f7ea74572550d5432775ddaf347998c41e707571f2d7fa6b9afada746e93077f4a673f36509a3cc0eb1e12fddae2ef311ba9f5b1f9313d4dd6d7e57b13bd4df23835d6f49eef87e0bfb908fe05ff55ebebf27d617e017ef1177f19fee2639fa24a4db96d4768c81756f2924465f4592207352153545687ab1aa129ae6493652dfa8ef7b28c63695411c9064c151cd7568c7d55ca16c6caa16791510eaa65f422398708583866ef2b4bca52ca10765076eedc06af7cf52be0ddeffa69d8b76f0fa9212a89cac90f6a9187fee5fea623b1cdcc1a171cf2c7c60a0926c5dfc2f20a1c79f418dc71e737e01bf7dc0b9d4e0e8f3cfc08f958fc16a2567e37df7c2b55447ae9b77f1bbce4c52f80c585f9c2af7d30323c0433b33370f4c8093870600fed7165b503b3b373303535053bb76fa3168448765a585c866d5ba7898485ed0b67666660f7ae5de41292a076efd9032bcbcbd4b6105b0b6275a4d9d959d8b36777a13783e5e5452288eddfb7d784a9b0b55cf88ebae7e6e7617c7cdcb42244024d11d3999953e483d9a8a27d20698bfb3812a1283704abbc5356153367a3a06d5b32e6b6cd215612c3b3c61686940f852dc569862d0dbb5d97a744ba2192963952a41675b0aa9325f7a47131f873b51fa1f4d46c2b075355cdb41fece60abef18d7be18daf7f2bdc78c35de4139e6b96992a5e86b884d5c38660654513a9add55630353d0aa74ecec3f0701b46464660647408f6eedd0d13e36370dec103b06be70e6a258ac4ab8b2eba002eb8f07c181b1f26529d42029732e442fc3054487431b3ad1d0d76fd772987cb3df30ac774a689b8457ea2166df27db5a3e0fefb1f84b7bffdddf0d5afdc08cb4b2b765fda863cb7f6c1e2c91297f8f90150b634655215982a6754610f7adfa1f63f20dc5973cb54ff19abed1e79cc7fcef9a42d9ec7ba5dee28d5338e82f9fe933ff926f8e11f790de141defff2feefd7167f8fd948ad3f93efff267a9be4716aace93ddf0ff9fddf5c04ff82ffaaf582ff6a3f04ff82ff980dc17f5a673f634deff97e08fe9b8be05ff05fb55ef05fed87e05ff01fb321f84febec67ace93ddf0fc17f7311fc0bfeabd60bfeabfd38d7f19f2460553d14f8ba6a5d4c4fcc912a5d559bf2d7f5b3f926f752635536aa1e1c750f93aab1a66b9b3c88c3b8f3fc3add557a0649ce260fcf26c0eb27ae4d7c627b556355e34d6c543dc4aa72a9e999d4cdadfb11109bc77b15fca76d08fe9b8be0bf3ffc3ffae851f8e02fff3a7cfce37f09f3f30bae2a8db23e70e518f48ada85591287cabcbd508b3843ba024beec0ea37868ca55d752caa8ca50dd9c2b45e337bcd2cf182db81e51ee9ab476c3b31f0e7d8f5bc96091b3c86dca0f1893178cbdbde043ffeb6b7c0de3dbb8bfd60251e5b7dc8b6b35375042c6ac3d63b41595bec6557636bbd2e11a78e3c7a040e177fb7dc7a3b2c176333a7668af8ce17d7b751fb36ac54f58aeffd0e6a65b865cb566817b1c16a53f877e2c409d8b57327cc9c9e85f1b171383d3757cc5f8591e161221e218109afb11ad2f8d828b51ac438672d05c35871a9d5a23f245fcd2f2c503b433c0fac648671192bd6ac2c2f51bb403cc791912122d210d1c412ebb0a21556c25ab5d5aef00fdb5622f10aedaf2caf40b738472405618b4465c95adc7a90ce054cfe2099ab83ed09b18257318f2a6d614e2139afb887799177bab4b685958d9068a5336a5d887be4b326d21de58969cd97a9963b88b568e9ffd93188e8dcb7635beb216e0afb9de2debdf73d086f7bcb4fc297bf7873918723b0b860084b00457ca726a87a19c206db4e62552b8c259e253e062eb9f4623870707f3196c38103e715f347e1f2cb9e02fbf6ed85fdfbf7c274b17e6474186c7f4fb36b6cb1a9cafd2b979f8a620f54f5294874de821db6d3a845658b08528a7898dd8e86fff3a51be1431ffa1ff0f13fff24745699d2949be78132a42acbcb3435cd141334c110ac14931e6dbb450feb996d27e8aaf171bb42b0cf6a30b877b8cbca968d2dbb16852a5d5972a6ff0c51c69902cbcacd316d2dcd7392d74f4e4ec06b5ef30a78fbdbdf0cdbb76f85e8b96b79ffa77c6591f7ff5a7b556355e34d6c0cf2fe0fe7f7a33b35af9fdfedbcd758def4a327b6df2a5d557bf5d755cd13fcc77d6511fcafb557355635dec486e05ff05f352ef82fe7b07f827fc17fb8077f5dd53cc17fdc5716c1ff5a7b556355e34d6c08fe05ff55e382ff720efb27f817fc877bf0d755cd13fcc77d6511fcafb557355635dec486e05ff0df33ce04aca65207f441411975ae2689fc6bf6a91f7dfd02bb9f75fd3c38fd35fe3e362221abf65d0504beee27d959ea12b24a479dafa9354decf50b8e50fa7de155d9ef37c7fa91aa1747d5c3b25fdd82fff43ac1bfe0bfca7e3f3986dfb14ddeaffffa6fc19ffee9c7606e6e9ec6337b1f3f9130e1880a608833e41b58ee86b22d09b9229532640ffc8e242cb285e31993a7ba38d9365f4323969855fcb53c5205c4f6a03591b0f2c2278a93b2d5702c5183ee598209b574431b4a531521ac84f573effb1770e10507a80a51375f313ea00ef4af92800544dc608f707e860bb4b61414439a41e2125682c24a539dd555aa6075ecb15370f8f011f8c637ee8163c78f93af8b8bcb70e4e851d8bf6f175c7bed5530313e4e95b190bcf46831be656a8ac855b81f6c2fb8b0b008c3434344665a5c5a862dd3d354e56a647414868a35485ec2b9abab2b3035354db6f1dc86c81780b1b1f142df08acac98f16ddbb617677d9aaa81a14dac2c343e3606cb854e24a0e4e4e3126cdfb6154ecfcd930fa32323741f8939e80bdac31684387f7579954838a323a3440ec3a00cb5da7496381f6d600e619b45248929caa7dc5434cacc791a529f225292e95ea91da9ab4595cfca7ca987c4fadf134d640d010bb8d21256c16ac1438f1c867ffb7ffd3cdc7dd7d7a94215e6e6dcdc1c743a5db8ecb24b4cce14f1c27677d82e137371787888e275d965175395b2e9e2ac77eeda01dbb64ec2f6add396a894597bca5698cbc110acc805e0aa5c257190ab8521e949410fd950816b0749d33365db3e96ea6667e6e1337ff537f0effedd7f8407ee7b90f69d934d3b010c2ecd191982a6c16159918a9f2974d6ad5659b9ce8ed1a577cefcfcc3bfcc7b2ed0b5523d64d1dccee1ef2d9b37a83fb37334ff79eb5b19c7cce89e2acee115aff84ef8899f7833ecdcb943defff2fe3fa7dffffd8afcfe6fa64ff02ff817fc0bfec36bc1bfe05ff05faf5bf09f5e27f817fc57d917fcd7db11fc0bfe05fffd8be0bf993ec1bfe05ff02ff80fafcf24fedb751b65e139758ec51c6ae24c93838b81c2f7af4a472c6031db553652fef4f30048f9d5341655c917d3117ba88673435bfe75533f9ac49dd785f7abc0e1fb188b71eab36a6e6c2fb1fb75391bf3298699d85e537189f9da2f36526b63392ef817fc0bfecf6efca3ecddbb077ef0075f0d0f3ffc087ceeb39f2792421775157f39ceb5f3734b9601dbd68bb5b4326ca746b40b22d4e07453a84a435955c754b3c1cbcc7e474296b27a8988637df5c916e11e911842040ede8bd365485cf8e7da13525b3353646775b90b9ffac4a761e7ae9df0f69f782b9cb77f2f5550d2ca12b9eaa0a9542fa547fb63a66a54ae0adb43a6bd5c862ddc541b766cdf06d3d353b07fdf6e38ffc02eb8effe87e1e4c99344a2c2ca53d87a70747814c64647a93215929ecedbb717e6e616e0b11327a92ad548a70badc216568ac2568148a05a5c5aa2b59313e330337b1ab66e9976e78fadfea8cd5fa688b884a4bac58505383d3c0c13e313b07dc7b6e2ac1f826d5bb752852bdc3e566342921756d9421be358d9aab083042b245fa19e95d5153a27dc3a91f2b4a93cb4ba622a5521d90edb24e2d1606c3bb9a96a6588371a3a852dd4431abaf6bb3da796ad9ac582243dac9445642d65720bdb2b620c5a21530ec9460aca33ac7ed46fac705b4af24383ab4665f30e7371fbb66df094a75c4695cd905875e185e7d3b94e4e4ed19923816d746c8c486dbb76ef84f1d1616a33b875eb146cd93205d35b264cb5325b4b4e51cb3e133fc293d6969085798ce5c3326a21e862031e994a47c283cb6c2538bce99e17992111e6b982cf16cf855ffee50fc1c30f3c42d7998707959935661d2b214418629431612a9929e55a0de616b7bcd6ff6edda2b3e73682284c9ca2ef560fc5003f9194699f57b9afc77b6eb34e266fb12e9c81f17cc1f39f03af7ef5f7c28e1ddbcb2396f7bfbcff417eff57ad8de5b8fcfe17fc0bfe05ff4d750afed7fa23f817fc0bfe05ff82ffb522f88fef55f02ff8e7b5311f04ff82ff98cd9408fe05ff827fc17fd55ac17fafddb302ffb10a58b1c0c492b49fa0a500969a17db506c4d938751383fe553a88725953cfefd26f189d9efd7e7263e3539a3ba18a4fcf7d7d49d47d3754dc6fac9037fac6a6e0c7855fb4ded8be7c4f659a733959375b95a05f8549ed53d20c2f5757e08fea1c75e3857f0df6c5d9331c1bf19c77684eff8e9f7c257bf722311b0c0b6e872f40d264d217189db0bda5683a1eeac9d111108092078df2751145f8884d3b26dcbf09a2b38e55ecb31225bd8716cdb676e00d90455fed877b1d6866091d94a405cc5c751458a8ff65006dff11d2f85f7fedcbf80abaebc12868790ecd5818c5a1282a95fc4841e5a9313d108ab1981475d51ee7cb5fd23a721575d80ae32566d2b3d574d489971243d1d3972845a3ececc9ca2aa54175f7c2169c7312419cdcccc12f96568780866e7e601a96a63636344dc595e59a6d8500bc2f10998999d810307f6531c90cc8355b76666e66078b80dcbcb4b740e85b7c5d80cb5383c7d7a0ef6eed955c45cc3d8f828cc9c3a05bb76ed28ee9f86567b885adb21d9697acb16585a5c243b5895e9c48993a41f892a1d4bac1a1e19a6334372d5e8e8082c14fe8f8c8c9a3676f63c703e92b4b02012cec12a5ab8af569147141b3c3a24616155a4e23bb64e4442962e3eb1452112ecb8a51c12cbf24e6eaa3e15b99851334b63cb9d8e4905731e39138b902554e62cdbc4582b607250065e674d283bf569abd0e8f1114bcd34a905a726929cb6b94d64a9627fb37373f0b79ffb02cc16315f585c821ddbb713690e896d175c70b088fd2cecdabd0b860bbc6075ab2ddba60db9c912a80cb910bf99167bcaeecb54a1cb5c7b41c60c610cf71678c9859ea81da0561efeb5494bea1b682a6a19f2d2106d19f3f0c31ffe087cf083bf06870e3d529c499baa7b110e3383036d71cf31d25056c0c373c23f8a8b6d1f8884a85640b60255b605047b9e605b16520c18ef9e2d9fa899518b4d5b11ac659e3d4884c4a3a17697b430a77bda3e6f501f12e23097478a3c7ed94b5f02fff6fdef7595afe4fdbfd62779ff9fbbefffbafda5d656cd8fedab891f4d7585eb626babf6dff4dc05ff82ff704cf02ff80fe70bfe05ffb1efbe8fa9b1d0068be03fad2b5c175b2bf817fc0bfe05ff827fc17f6c4cf02ff817fc37f349f02ff817fc3f31f04f04ac26818c19ef07504d021ad3dfe4609af810aeeb17d029ffaac69a48dd43203537dc53953f61b2c474368947d3f3aa8b4bbff916f3c3d7d9e4baa9fd549e34d9973f1edeab8a5dd378ade785117b60d4e13025827fc17fa8c3b725f88735f73612ff37dd742bfce70ffc2afce33f7e113a44a032e34c8cc00a58a51b8aee3b7e93256369c37a20b2089121708e362dfae887012011649548367c2ffa0cd2dad19b327b9fe6309bc4df97516ee6e11857db41928a06abc7b40d1c1d1b86673efb19f0ae77fd343ce739df045ba6c68bbbaba6d51d55c5d2a09c096df7a988b8b26e21ff72228ca03dfcc31674b42f6b0d094838beb2b20c4343c3741f894e488ac2368518ded595156a6987e4a8a1e161aa9ec42dfc70ddfcdc3cb53de42a49ed565911cb549d02585c5a80ddbbf710490bd7a3cde1915122eb0c0f8fc2316c9178e03c989d390553d896503389c6783a3232422d05db54bd8a5b48b6887033bfb00853d313442ec36a58e85abb3d5cd85ca4568618072465a13fc6470dab9d55186a0f911e438c52ae4a929f67fee997c43e5de62478158e7cf24eaecb9693f69acf845af85962919759e054abf219a4bd7269b82f1ee1dc5374869ae28dc4b4238f1e834e610b0974b83f24fb2c2d2dc1962d5be84cb1e52292115b595e993a5865cefff9aeec3f95a2c0d6cdd26e4dee930669d05c2be01861b5b10cbe7ecf03f0e10fff01fcc11f7c041e3b7a0cb0ad26feb920f96d44e95af3a1b8318a073f6f2c467b9eb3beabfc9cb138366764c8779929a9e788533dcf30e0f6868670a75ceb51559a75be98675a3737c43f7c9e61fe7ddbb7bd08defdee9f844b2fbd58deff20efff27ebfbbf6a5e3816dee3fb55e75d2775f14dedaba9dd58eea6709cb2178e09fe21aa5bf09fb621f88f8be05ff01feaf06d09fe61cd3dc1bfe03fa54bf0df4c04ff827fc1bfe05ff02ff817fc0bfe05ff82ff94bd70ec6cc5bf7ae49147741570ab00cff3620ef6fb1069224d36390860fa01639dfd7ec58fa52f4d1e94293debf16fd00745d5b9d6f9eeeb0b75fa5207bcd839f5b39fd4fc7e405f6527861f7fbf556b62f7ea6c54edbbea45c163827fc17f6a5cf0fff8e11f590a37dd8c24ac5f837ffaa72f114184352aa72fa37958bd882820b6c28d21caf895a7688123ece4960083241d6ca186449dae47b041a20555410270641f24f7ac62eb39db6a90f4a5fcb7641bf0f7ac8d27262eb925f6686a3d78d9532e8337bde98df0433ff81ad83a3d6e1dd6a4c76ec56e1ce3d29080553385fcb38a73c8ed99e15e725bb94b11c984aa8a615c73436c320414dd43b8a13d295325ac6dc96c143b2258e570f2d4296a758895b4b00d2112deda5451cae89c989c8213c78fc308b5bcdb05c74f9c80f1b131585a5e86ad5bb6d0b92c2cccc3f4d4148c8e8ec2ecec2cd9451d48be5a2a746335a78e25c52099a5db592582d1ca6a0786b00d62b127241ba17fb866195b278e8c1ab25bcb5475c2ea6623c3239437e81756b9ea764d6c909445a42e3c37ea278967ae2916545d0d5479ed491e3b2b0de5f9d84f226129534189f3df3f4b3a1f4aeaf2baf740359d152e43329655e78847a833ef1abd54e10d6385e4443079d8b5e422ac2495a93a025606e113bd8e80a50c683d0225590df428a389f250c1dcfc02fcc317be0c1ffa8ddf84bffffc3fd099994a532daae80696d0a5b8c5a8b501fc6cb404498e03d83c2d7d52ae8da823046aedc8741c3b97cbc5d850913f5d8a65b98ec8584cc6d4e0ad63221650ce603b4b137f65ddd4ce2fcccbe73cf77af8f97ff73e38ff8283e4434ae4fd2fef7ff9fd2fbfff05ff83fb23f817fc0bfe05ff55e3827fc17fb8b6cac7d43dc1bfe0dfd717eaf445f02ff84fd98cf928f81fcc1fc1bfe05ff02ff8af1a17fc0bfec3b5553ea6ee3dd9f1ef5a10d605a29ffb8304d53954012ebebf9e43ac5a931af37dab7a60d6f955f7b088d95dcf99a47435792056d96b7a66b1eb94ce988daa7ceae7a1980263ccff7ef434d94fbfeb9b4a3ffe6d442e09fe05ff82ffb307ff486cb8e3f6bbe1177ee197e08b5ffcaa6b0348767228895648eac016630aedb323b6ea95257c185215122b14dd347e22992677b648779ef710bdc0c6df276358f5e59e2d798bcf4a7bed0b8163418c98ac5c89d5adb05d5ce15337efc0ee3d7be0ad6ffd3178e5f7be1c2ebbf4625b398916962d09b97a501d014b35f80185f76de525226059320b91afb436eddfdcde94250829b71d6a9ba7ca4a4d54c907e7b60c7124334e1471edd0999c38790a8e1d7b0cc6c646617e7e912a2d2d2c2cc0debd7b89188524a9e3274e51dbb5d3a74f938e2d5bb6c2e1c387e0e0c1f361796901c6c627cde7d818554543320cb637c496857b76ef22929426a20c901d6cbb37393e4e95bb909435323a42ad15f18c90b065aa7badc0e8e8182079a945c49836ed09d71081a6181f1e1ea6b958bd6bb56b742141869b629a7926a47c56da9e11c5507ba41e5dc6ad7c6699bc75f1e7859c5ff61fe51afd29531d6d6d13425785c9b5c4b3441e6dab8d29dbea2f375d392de10aabadd98a62745d9d3bb9259cf525cafc71eea23dc0369976a354d10aef91ee8cda24fedeeffe01fccefffc03b8edf63ba055c4050976a60ba365252ac59b70c077cf6ecd07d24b94c471c62a8e723b42c66a972b9559b7a9bd2013b33c3c1331cf7be028bfda966bad68ce85db2052934a22d069b796cfe779cfbb0e7ef667df09575d7d85cb1579ffcbfb5f7effcbefff705e5311fc0bfe63fef7a3a7c97efa5ddf5404ff827fc1bfe0bf6e8cc75104ff50b946f05faf47f02ff8afbb2ff88feb13fca775c66c08fefbb323f86f2e827fc17fccff7ef434d94fbfeb9b8ae0ffdcc0bfab8095da6015589b027950c0a774f89be67ba1ef754008d73701ed7a80da2441d70b029ee34bec3c79bca9fe7e1e3c55a0f17daad2e15f876baa40d2743ce67368273577103d4de2df3467d6fbe289c557f02ff85f8f5ffe1cc1ff5a3d9b857f945b6fbd03def7bef7c39d777edd559e5154898a6929cab536439204922390d74095902cc1c2b40f2bcb49294b10ca73d37a0dab1fe17566c9194cba203fec7aac52c3d5ad98d805613e30f98a892f481ab1fe52f52a650927b6ad1c927e3af92a4e81e9a969f8e66f7921fcab9f7b0f5c7dd555444a42ff88b5a18cebcab5deab1015893dacfd514524aacc1245144d32f33494fc16aded3c248a61b5a8b6ab0ca6dc1ac3813195b40c818bed7375b2d5d50e9c3871029657569cdd6eae898875cfbdf7c1b66ddb8b18b560617e0e0e1e3c08333333b0b4b408fbf6ed8795e525d2f7d8f1e3303939097bf6ec217f1616166108db1f0e8fd0dca5c545d8bb6f0f3c76ec31d8bd6b17acacae922f9d6e07c6c7c6e9932a4c15ff2c2fafb8aa592d4bea431655b7f0172b6451352ddc07de2b3e91a835625b1652c0b4a6b32b855be2e526dada1c84b6b16a65c62ee561e143e648393a7d86f69c752f1b2b2adaabbee513b07c1c659a0fab3c7facd044790d654bc7bafc5a43c0b279529b973835537647da54c0d2b98d29e2ae059d550d5fbbe936f8b33ffb0bf8dddffd43387efc318a25b5bcccd918d89cb555a6d077dabe255d59212213d8e7912e6303fef38671aab57d76747bd69bfddaf3b6fac2df12fc3ce0b698aac008cecfa99a5c8bccd1b96796b098e7c0278f55dc9e76ed35f06fdfff3eb8e69a2be5fd2fef7f79ff6bf9fdcf22f88fcff145f02ff8afba665f04ff827fc1bfe05ff00fd131c1bfe05ff02ff84ff9d5cf98e05ff02ff8ef4f04ff82ffd89cd04e93f198cfa19dd4dc41f408fe05ff29fcbb0a58e1e47043a98d0c1ad0d8fa8d38a8f5d86f3a773dc95f7590284df4c4e655f95b3516fac6b2def8a7e2b51effcfb4d4c57ea37c6c0af6a6f7d7bb46f05f3d57f05f2f82fff5d9e907ffb7dc723bfcdf3fff1fe06b37dd029dd58e2560e17ded55acb2d7da5496426e87761576b46db9a64a320400b505a34a599628459590b035a18d03132e80d667a6659d571587eed9fb3670f49d792ece37c685bf4730add888a89159db852f4f7fc6d3e04d3ff63af867ffec65b063c776222999f6724804e99655927c45bea8f40ff535eb0287385e86a066c7b476fb32957bcacdf9e4a03564223b8fd6db5b58adaad3592582d591a3c7e0a20b2f84c5c54538796a862a4dcdcdcdd1f5c5175f4c7bc6739a9f9b877dfb76c3ea6a4e44aef985051adfba750bedb3d35981a9a96d802c9c53274fc0f4d4249dd3c8f010b58d44821112c7969797690c8979440c2bd6629bc1b9d373303131616386be2ad7be922aa7b55b748f897e4832c373523aef0d3a47c123dae5b9eabd6d635b45aa3236ec8f57a57ae31c39461a467b2a71d34a464425ef5c953917ace5c5dfd9bf2ae9d82a56dcf693aad0b1ee94284f73667222d7ab74a12db1ead0e1c7e0b39ffd3cfccaaffc1adc73cfbdc5b9761d49cc54ab53ae121be622e150eb9efcb4417355adb487c5e8b3daf7d1ab7e07de7aac8cd7d356d4c792775d2287acd9aa65660aed81abe4919d1c868bdc7bd18b9e0fef7ddfcfc025975c049b21f2fe5feb1b8bbcff9f38efff416cd4cd45a9fb2d2ebfffd37a04ff82fff5d811fc0bfe537a62f3aafcad1a0b7d6311fc0bfe05fff1b982ff7a11fcafcf8ee05ff09fd2139b57e56fd558e81b8be05ff02ff88fcf15fcd78be07f7d7604ff4f0efc47095829a5a939fd3850a7ab4eaa02b11ebd1bb97e90875f533deb99d754fa39abbaf3081feafdc4a6ee7e53dbfde428cf1f549aee6f3d67db1403eb79a10afe075f2ff8ef9dcb22f8af1e1b441f7ffff2976e805ffaa55f811b6fbc99883848a461a28326521512a7981463484bc542afa59a6989c7e42bd24d0dcfb0f291a6967648e870d5addc2adbaa2c33d59d50275647c2325b5491cb6bf346f3bdaa3a74eded8d2d13712437fe69285bfbe196f0fbbefd7be155af7a05bcfef5af850b2f3c1f868632da8fa28a52dad03c70a2d2a0f320a02af21f3979353927f4b3a95075a0e432437dd1b60a16939a664ecdc2c993a7a8cd1c92cb565656616474944859a3a323303232569c4546ed022726a78a306bfabe6ddb0e78e8e187e1b2cb2e83d373f385f12e8c8f4f904ed48364abb1d12177c658d90a493c23c3c3b0b4bc0c5bb76ca12a5ec78f9f801ddbb7c3fcc23c8c8d8f159ee1b9e6b0dac5f5a396f063897aad9621c9756de5aa9621fd502530dd89ef3a46c07241b65835aca57ac1143745b52aa511014bfbc42143340b4535a863952362b4e71f7fd7e64f47aa68f98433266d7529775ad02db07cf3ad77c2effffe1fc2c73ff629387cf85130b4304576881c9971f53a5365cd3c1332a799da9332c1c9eda5742d53c1ff070c30274cb979fe73dc5fcf8d43793ed9b6dfb92daaab3846770c56b9e215e50e3f9bb4d97fabade0b9cfb90e7ef6bdef84abafbea23756f2fe1f785e5391f7ff13effd9f5acbb211399c9a23bfffebd708fe05ff756383e813fc0bfe05ff82ffd45a16c1ff6022f8afbe2ff84ffbd4efd820fa04ff827fc1bfe03fb59645f03f9808feabef0bfed33ef53b36883ec1ff131bff4902564a695d926e6420abe6541dfe66fadf4f02d4f9d8cfe1f69bd04dec37b5d5cf9aa60fb170de7a80129e09eb1d04f49b99df55795065733db211b9e7cf17fc0bfeeb6ca108fecf2cfe911073cf37ee830ffce75f85cffdcd3fc0ea6ad78e77493f921c48bb3273893c41f6f0cf5240b4a552e07cdb428e5ac375bbd4e68c7f2830a102dc1a20a245d8a22cb72d0e715ffc5d2bb5e6ac9c0e9fac6549254c2821d72ddb43a91c5a43195cf3d46be095affce7f0ea577d0fec3b6f2fb4c054e251761b14f3905ca506246065fd9f0df9dd901c445373d3d2707e7e9e08584b4b4b54d10c4951dbb66f87c585056a1d88d5b0903885739000353a3a4a2d06b71673e64e9f86a1a16122414d4c8e13616a78a855ac1983b9b919989a9c2c6c1439a1b0b5e42ad99d989884858539d2b565cb16585d59b56d107322690d0f0f99b869134022d0b4db34676579a5b0374495b0f8cc5b99216d9571f0da4b5611b05cb02b8858aac436d41f5b4f8caba437272c260c28cad162a055d7825099768b614e917efa9fad3ce56188485959d6f31c585aeec27df73d087ff491ff057ffe671f877beeb9af382fc457abc405e2586953f50dddb2ad44bb5c918a7d2ace9bdb8a9ab5b605a1d68e24a9edfe943d439c44c42c2f22bef0f323f76c696f1e3f4f548f0ebc9bd9968e4cdde2e78361a88d8c0cc3cbbefdc5f0ae9f793b9c7fc181f29920ef7f79ffd7d8e6efacf7c9f4fe6fe24793f13a3d9b199f7ee608fe05ffa16dfece7a05ff82ffd8fcf5f82ff8afb78522f817fc37d1070d7d14fc0bfeeb6cf377d62bf817fcc7e6afc77fc17fbd2d14c1bfe0bf893e68e8a3e05ff05f679bbfb35ec1bfe03f367f3dfe6f38fe4302560a18eb05fc20895015ac411f2e75b2d1fa42dd289ba5bf5f59ef83b99f87dda07eade765b09976fbc1c366c966e817fc0bfe9bce17fc9f5df84732c46db7de09bff00bff09bef8c51bc0768c33057d6c751c2412e59ddcb41ca4b5e6eca8dd60d622e295eee69664a51da322b315ad9878a1cc203a61aa1e2943d8c9fc16845c4dcbfa896b7228f306005cb51c9a66ab6421990be7776d65252284a02b2d433ac11677397470016cdbb10d5efdaaef8537fed8ebe09aab2f07e64929e2eee8b5bc1b757611b0a0a56dfcb5213d157bd69d2e748850773fecdeb3134e9e3805e8e29d77dd050b0b8bb067f72e989e9ea66a58f3f3a761cb9669989d9985f30e9e4f2d09b76edb0a0bf38bb0b8b448a13d6fff7eaa8a56288685c545d852ac4572d7f8d868316709a6a6a66179790926c6c7d16b6a63383e360643c343c5f82a11b03a9d8e6b39886782d5b25af69cf0b3a7d293024bbc332d07bd88002714de8b56c0aa6a41a8204a62aaab4bd59480a5fdf6959ad2d6338d1739b46bced311b0d83f059620e511ba30263dfb52765f1961666e6e01fefe1fbf08bff55bbf077ffff97f80d333a78bf92d5a4f9868b5cdae8bf3ccf919a0cd3e4d45b26ecf7f6cb46c7b50c403552af30857947b852e6ae5c971b015ed30aa2637f33598411d99d54b442caddd3a0e9c4f2a2b4fbe8c0f0e9896a5dae5ccb39ffd34f8b97ff92e78dad3aee9399b50e4fdbf7e91f7ffe076e5f77fda1ff9fd9fd68d22f86f6e47f07f66f50bfe05ff4de70bfe05ff317f04ff69dd2882ffe67604ff6756bfe05ff0df74bee05ff01ff347f09fd68d22f86f6e47f07f66f50bfecf2cfea315b062406bea545d60c224f71d3a130fa8f504783dc9e9cf8faddda883df88874fecbae99c416d6ef6c36a10793c7d6a8285aaf358cf0b44f09f5e8b22f84fcf19d4a6e07fad6d9441f0ffe8a347e05ffdab7f0ffff48f5f82c5c5253347f1fd92c4c4a408ae3e43a48ecc54efc1b9e80291ad907c843a724d240e53454711598b49592c38bf6babea841572e8b350dae5ca5844dc31042e15ec8bd68269b666add25cd4a1d8db2c275d4800bae4924be02d6f7d23bcec652f860bcf3f1fda6d247ad82a60483ca1823b5809a80d3a682fa76cac1d4947abb5710f8f41db3352658bc448b12da8cb1e6a11a94ce52b131fa07684d89a0df775e4c85122a99c9e9f87a5e22c4fcdccc2f6eddbe8fed0f0309c387182e69d7ff0003cf8d04330393949f3f1cc56965660746c0c565757616a6a122626c6e0f88953b063db56983d3d4b24aee5e51522de4d4e4e50cc8f3df618ecdebd8b883ea7674fc3b6ed5bed199a1d61ee0c8f8cd01913018b99331c42a5a26df69a4a0ee05551d290057a98b3e54ceab573a2522ab5e7068eecc57ba0b4d04c3a54f4b597289543a66b7a23226110ca3c02657219c982dadae4fb3ac7dcc13e85197456733872f428dc72eb1df03bbff3e102bb5f8663478f996a62d4a2d290953add8e216aa932eedc5a94f641c5c774b967cda4486dbe63253b6dc96188a7cc3c0f88f468e34f31f55a9152dc756f72d39e94c13f7e72cbd02c2b4f037d2452a60508e39cda5866ca92f40cc112f3f3bbbeeb65f08e77be0df6eedd039b25f2fe6fb64edefff2fbbfc9dcd45af9fd5f6d5bf02ff8afb38d22f8df5811fc375b27f817fc37999b5a2bf8afb62df817fcd7d94611fc6fac08fe9bad13fc0bfe9bcc4dad15fc57db16fc0bfeeb6ca308fe3756ce36fc376e4158a770d07b7541afdae42000eef700aaecd6d9ef270661c2a36cc48374a3e737d5b7d10feb8dd251a567235f284dc607b937a8aecd10c1bfe03fa54ff05f3f3ec8bda6f36fbffd2ef8addffc5df8d4a7fe1a1617165d759be25fae42ce1addde7765af3115910405ca108db4eeb5a77597da9e81e68a3f56a76d73c88bb81a8eb2634cd8700a5540d95165059f94e07c6e7d88248f5dbb77c0b54fbf1a5ef5eaef85173cfff9b07fdf6e181e6e19720db547c4f6784878e146727e2b386d0838aaf011095821a9271673edfed5d37eadc7c326e9644941fc0f0d11212b83d5d515989f5f80b9e26f767696ce0c895839116bdab0b2b202ed22fe58996a726202563b1d224661cb48ac60f5d8632760dbb6ad747d7a6e1e76efda0933859e6ddb77c0cca99330353941d5b1268a4f6c71b875cb34d99a9e9aa4f1f650cb7c52cb4143fc196a0f417cab5ef5aa06bf2aeba0a6a9aa93eab191a9325e4ce8c9ea14f9f7533915eaf0ce9685325a697f825d6b4989344a4914e8315fa86294253b12f1a8f8babaaae191470ec357be7a137ce22f3e09377cf56b70ff838f10134a5b4c11a9519b6a704a956d3ab5d569cb5901b5f773ef0e43e4238859c2a38f299f6095dbca73f44cb015acf0bbab646716406f7cbc7794c5b1b638a73868edfc049b37bc176e7188d79897a3232305665f4e55ec2eb8e0606046defffd88bcff07d723bffffb13f9fd2ff84fe913fcd78f0f724ff09fd623f817fcaf478fe0bf3f11fc0bfe53fa04fff5e383dc13fca7f508fe05ffebd1a305ff7dc946ec4b0bfe05ff7dea5eaf08fe374604ffebc77f9280b5d9c9deef9caab9e121ae27c8ebd9f74624419d8ecd78b0d5d9d8285d9bfdf0584f6c065d7b261e7a550fa1f5e85bef9cc7c396e05ff0dfc456ec7a33d69eadf8c7b90f3cf020fcdeeffd317cf47ffd39cc9e9ec3c1de49891f8be56d45d576ccb83f1dbf68223e11292633b3a80d995d4795717a8df5b6f1f37db17be3316e5da8753d01cb904ca8bc15b4b07da2eec2f61ddbe19b9ef34df0ca57be1c5ef882e7c19e3dbb4cc52f228300b461edf3c36f538777b3d0f49a58991830c106f71e7629d4b61d5d23f149581c47aac29453fbbfd5d52e2c2e2ec2f2ca0a9c3a759242859584e6e74e5325ac4ea70ba3a3a3147bac82d52daeb1cad5d66ddb49779b5a076a98989ca68a58a6a5a0693bb965cb9642f73c0c177a8a10529525245c0d0d0d1159667868d8ba68883323432381ebe5f3977cc73c6db0ef4ce5752181dcd3a3dcbffca029c86ad85e780e8a2972c90a562ae14129587dcb11cc5479f63d15c0e86be62da76cf2888f1991b4b095e4bdf73e007ff7f92fc0a73ffd19b8e5e6dbe1e4f11943ac528ac875a6ba9c264220a9c31c6b65948a0417ae244555a4ba25010e5319db59d2f92a575dcd54b7cb9d2ed2ca5843bcdaaa75748e66523ab2f681a07ab7efc60c1e4abfc91e93b378acf065dbd6adf0ca57ff7378dbdbde00bb76ed843a91f7bfbcff9bd88a5d6fc6dab3f5fddf44df7ae73c1eb604ff82ff26b662d79bb156f07f666d09fe05ff4d6cc5ae3763ade0ffccda12fc0bfe9bd88a5d6fc65ac1ff99b525f817fc37b115bbde8cb582ff336b4bf02ff86f622b76bd196b05ff67d6d6b986ff3504acf538df746e18b0416c6d6482b02f4dfcab036a3f401e0458a9b94d746d946f1bfd4038935217bfaab1d8beab72f2f188cf2079d0f4de7ae70bfee3ba04ff674e9e4cf8473971e2147ce4231f85dff8d06fc1dce9792246b4b80d59b8d656a6218fd96f649d045bc05b563df1375c4b4165abe680a94aa43d9d78416410f6114c151ef243ebb22a96d63d1573aa0475700b396393ca75910f4838dab2651a9ef3dc6f82e73fef39f0addff62d70fef9e7c3c4f828b42de9446596f0c42d18699f9a5a1296d4296305208b78a02bc7726a2fd7c7f93b32978d03c7409bd876f26e7186a7a922d5ccec696815f1c4cfceea2a4c4c4ec1a953b3b065eb5658595e7255863ac5dc0b2eb80016171788a4636233049dce6a119fadd46a10c74646c78810b5b83007d3d353a6da92659421312b03531d094958ad567badebda9580b21590eab7db525d08e83b3d12a54ad1f4de35597903e2a2a03c872a0256da692283611a7b6d2a7bd7286fa6f588e6b488908544ba6e57c3e1478fc0576ffc1afcdddffe3dfcdddffd3d3cfce0c3b0b484e765749415ae0cf98e5a16eabcc4ac36f9ce67e35a5782c5822523d2b536ad37f1d3b5f9c44faa1ad7a13366c2247838cc6cde71c533c6b42f3eb9caffc4f9ee9967b1cc15b65a16efdc96746a7a12defef637c3f77ddff75295b63a91f7bfbcffd72bf2fb1f06beb7def94df0d5647df87d3d22f817fca7c605ffcdedf5eb1b8ae05ff07fa645f02ff80f75b22f82ff66fa04ff82ff41e60bfee3ba04ff674e04ff82ff5027fb22f86fa64ff02ff81f64bee03fa2cb27609da904aa0be4463fb4aafc401904784df5d7cd6faa33e66b2c6152f3c2b15047bfc23a637acfd483a7cecea00fdbf5bc10fbcda9c74bfa79e16ca66dc1bfe07f5011fc3793f9f979f8eddffefde2ef0fe0d4c91922742071c36f4386be304983f6c0be6943a85044c8d1544028b7842b6cff879580b09a125e1341c76b67c6fb7479eab71d2cf471eb422664905db66dd7fb7a7c5f5d9caddddc55e3326417b3a64b7a9050f4ac673f135ef6b26f8317bef0f970e1f9fb616a6ad2b6d5b384272739d1b9147eda5684b92579516c8ce648cb41e50861e55046dadc1e720d6bc86cf61fbf759fa9ea652a17398299d2d40ab24bf1d554d1ece89123b07dc70e989b9ba3735ae9e4f0e8a38fc2f0508b2a28eddcb90366664ec19e3dfbe0e1471e8683070f10d907495948bec27548c4c10a585966fc9c1c1f86d54e97e2d829623a3e3a0a4354018bf3dafc8bf7cfc7a54de868908f3864edf05ecabcef00d385589f8bb155aa7b8345774d6530dd33cc31e2614743526bfd880c9567606fd28737917ccc389fb91d67d9aad210997002b7d733f796973bd442f2befbef83bbefbe173ef9c9cfc057bffa3578f8a1870ce98ab091991cc4026d2ddb46502b479644921c5570e30a5e9ae39cd92a66862ea8bd3c2e636d42d9f52ad21146c010a23ab61d29b70674cf257bb0caedcddba7bde671242c762d76c3d686cac372e6617cefbe3df0e6b7bc0ebeeffb5e011313133d6721effff8bcd05779ffcbfb7f50a98b416ace46db6ee247ec9ee05ff02ff81f5c04ff827f5f47bf22f88feb12fcf7675bf02ff81f5404ff838be05ff0efebe85704ff715d82fffe6c0bfe05ff838ae07f7011fc9f1df8578f3cf288ee27e8fd3c301ecf04eb47d6f3100a0f66bde06d928c4dcf683d0ff72adbb1b9557e0c2267327faace6190f86fb43f9b29fd3e7405ff6bd709feabfd184404ff1bab7f6e6e1efee48fff0cfef08ffe17dcf38dfb4cd52a65c955760e11262c99093f336801d7be61328739fe928c8304109fa041770b3d7e752bca45df19aeb4e5b71a0ccfc07e2afb9d3fad03c6b815aeb6c57afdb5590b873b446c191b1b870b2fba00aeb9e66a78e10b9e034fb9fc0ab8e8c283b075eb1618191922fb8694b24a3b673289b6adee8c4d4b17b26de1ca4e84dc0a119827640833c494f1e21b9cabf2eebbbd533cc091b25c8b3fb6ae0d196ba5b34224adf98585c2d72198999985d5620c0931681bdb0b62f5aad9d999628fdb60617e0e464647a8fddde4c444119336f983d5c2c6c6c60ac55d3afb56713d52acc3fd64595995c9f9ec3d5b9038642a2969881583a2b95ebb3eae56866150da54c0f2493d3d715066af7eee68efdfb446f7fa9473bb3de5aff1e63b9a5720aa2494f9fe339189da6b2a8e3f13fd8c2eac0ec6042424236255b2e3274ec237be713fdc79e75df0d51bbe0677dd71073c7af86881c305f291ba612aff59ed82eb61a1e61960fdf30958e1b3cbeccb8f5aaf56333ff3365f3ee34195d5f0f839916bdd1b363bce6bd0177cb650852cbb17227779f72fbffc5278dbdb7e0cbeeda52fa2b6998fb7c8fb5fdefff2fb3f7e5d377fbdf3ce0611fc0bfe05fff1ebbaf9eb9d773688e05ff02ff88f5fd7cd5fefbcb34104ff827fc17ffcba6efe7ae79d0d22f817fc0bfee3d775f3d73bef6c10c1bfe05ff01fbfae9bbfde7967833caef8c70a58eb0d7abf1bd888c3a90246bf414089cdafd2b3990976b6242fc70665a3fc49256b93245eef79ace7e1b1de33e917a49b859141e609fe9bd9df2811fc0bfe3703238b8b8bf0d9bffe3c7ce003ff151eb8ff412a58c4e4086dc957bd52acd5c188f209399a884779a763eeb55a86a4c1042c9e65ab6281520e9f60edd1b5378f5aad159fae968f2555b1be3539a94b92973f87e7b55aca9272909c628924594e15a00e1c3c0faebdf6a970fdf5d7c1e5575c06975e7c216cd9ba034687155071af627d57dbb24eda92c4c8e78c2a47391fac1f4c6d513632da27b9f1bef5da10c772cd679d99b677da3f0453088962a888f4837b9b9f3304abbcab6179791916161788e0c2678224ab13274f11d90c6335363a06ab9d55181b19a17d6145309476abed4834587969add399235e3902960ee758525456c42c37fba35365f6951f00aa139645f946ba0ca8b9a6ef5ce9cac429a3b5664cdb325c991f2b47a40a086f3ed9caae326431dd6b50714e51892a3a7fca0d2c5a55fc0b2b891d3f31030f3ffc30dc7bcffd70c30d37c2fd0f3c085fbffbeb303b3b0f0bf3f3b0b2ba42ad1bb1b520d654eba2dfae82558becb06f5a0721e2580258df4a6cab1e829ff11b0975388daa5ea9f8b9b810586384593055edba79eec504cc7321385f9a8f20b13a323b477961cdf99e7dc62099ef9aabaf849ffaa9b7c20b5ef8dc223747605091f7ffe68bbcfffb5fdb44e7e3f5fe1f749efcfe6f667fa344f02ff817fcf7af43f0bff17eb008fe05ff83de5feffca63a04ff1bef078be05ff03fe8fdf5ce6faa43f0bff17eb008fe05ff83de5feffca63a04ff1bef078be05ff03fe8fdf5ce6faae38988ff9e1684fdca200e6e44f20d12e8734536f241b05932a88d2a306ca4dd705eec4533c81efa7dc89e09f1b1c2b2192fd3a6f704ffeb13c1fffaed3e99f18f9fdff8c6bdf0810ffc1afcd567fe86c6726e1bc8240af45b536d1dfa6e2fcd9f25b2940672dbc24f111948b14dbfba154462acbd367e1cb3dc9034726388f47125addcb650f3491fec87f15f5932537986b92340a1beb25d5b0e5d53a1c7ce191a1e82a9e969b8ecb24be0f22b2e87e73ff73ab8e8a283b06fef1ed8ba6d3bb5486bb787a89210daa778e6c69f72576637bc2b6a63485f3d320b963cea42afa80801cb567172c41a4365e95d67493a5d24dde48670d6c2d8d950e08495d555539da9186cb732aa78b5dae9c0d050db9ea7d93bf01921d70d094759d95a2fcbd6fe3cd4ba9e80457bb57b71adfd509f6a156bbac6a432959b88d4e655692adb3122212a2f03e5d17b7a6baae5a03b9016d54b5e8380cfe6ddb484ad2e11ab303e2bab5d224fad2cafc0e2e2022c2c2cc1e9d30bf0e0430fc1dd77dd0df73ff0301c3a74188e1e3d068f3c7208961696617575d96acb4c0eb632db1251530e3a5358678dc88a6deb6666da786a4bf6f23d8bfcc786c390bdc79f8821bc8fad0b992bc9f37b7429bfef794e612aabbe95d5aeb0a21a13b3f8bb5b87732d594f7b7e983d68d776706c6c145ef8cdcf8777fcf45b08635956ff7eafbb2fefff33bf7e336dc8fb7fe3457eff9f5b22f85fbf5dc1bfe0ff892a82fff5db15fc0bfe9fa822f85fbf5dc1bfe0ff892a82fff5db15fc0bfe9fa822f85fbf5dc1bfe0ff6c917511b0cea4f413ec7e0f669044da08598feeb311ec4d6533cf72bdf278c7f2897cae9b2982fff45ac1ffc6c99301ffa8f7e1870ec1cffffc2fc217bef045585959a571d7264c713b37b04489dedee248ba3164a9cc109154ef0fd7e2a26c97672b58b128ab03ffa8420fcfc77677adcc54bfb2c42947d0b22b518dd3af4cd51e659cb2b6715d49dec19ba813bdc12a514814ea74ba34465579b0d522567862b28b32c4a12d5ba761627c1c2ebdf412b8e0c20be0e081f360ebb66d34be657a0b8c4f8cc1e8c8100c0fb50a1f87a8ca16ef1f492f488b69b75a50dc86eddbb7c3ee3dbba1a52c414deb9e1fc3a6f09377c64c72b3faf0dfb9ad34e60bc5579baa4719da47225c57db8a5526fa9daea938d56e675683a60a4c44f02a66b4860d9106c946ca1c3cb4a04d6b4cc5a28cac8729d88880a554d996d09e1b127730e6cb2b2b70c71d77c17d0f3c5cf8087436980bb93d7b7f93588d2b2634b7d0d9c5ea5f791e9d8395a5b0a25aabbdb6b29bb6043f9d73be60bcbaa46f69690996169760797905e617e661767616666666e0d8b1c78abf6330777a0ee6e717a9f2d5ea6a87d207cfa165eda11e8c1db6b3c40a63a68d23c6ba6bf3d354ebc273eb7614adcb2d3f0af7dbe9e2f9f432f5b8a59fa90ea78ddfca12e6001cf9d15ed82de56493f182061c614b6bd75290f1c867c81833910247d4ca789ef5077dd1f6d3c4dbe4418b488ee05a9162a5abeff88e97c2bff8d99f865d3b773a725e3f22effff45a79ff6f9cc8efffb35304ffe9b582ff8d13c1ffd92982fff45ac1ffc689e0ffec14c17f7aade07fe344f07f768ae03fbd56f0bf7122f83f3b45f09f5e2bf8df3811fc9f9d72cee2ff8942c062d9c8609d2d09de0fe836c2e77315d84fd47d9d89333f5744f02ff84f89e0bf3f39f4c861f8dddffb23f8e38ffc19cccc9c866edeb5240b43e221f20600946c2a00bf831b1276a83255b1860819600957e82f57f2014f059254da2d43fa8090a05556bb627209d88a3c486021b2169418e15685da9249689c884bd458d1cd033b17d73b5257401a31fb2ed7684b3ae25a4b2ad330d41e85f650abf81b82e1e12143b6699b0a612d6abde813608c4bc3c323b073e776f8c11ffa3ef8beef7b154c4d4e8221e118a2189297da76ef3de2176c024b788bb492f3db141a7e8e6d1788a42a5d12e6948b0e58128ff64871baac2745e792d92a67a6a2918ae49e565d635a9b968f99b350ead4d60f3c0a2afc8567555c1c3f3e03fff903bf021ffff8a7e1f0a34768035cddacdbd56eef863864aa4479c120b21be3a1b3da71a18ba34351be398f74492e04a5d6aca1d4a56a621de876ba96b266fdb14437e03696a6c699ed90a97a5befd96a52e0579453ca12a24c9537da97c50712c5283f6d3530ae7e95b50cc9102721b18e4967ca565a3315b362ffdf2d76673686666fa6a217b54fe4770d00e52ec65f7b98652c735cf97bcbb6f8cc6dc52b3c53225b593c1b72a6998d04449cb763c77678d39b5e0baf78e5cb61d7ae9db05e91f7bfbcff5322efff735f04ff82ff9408fecf7d11fc0bfe5322f83ff745f02ff84f89e0ffdc17c1bfe03f2582ff735f04ff82ff9408fecf7d39d7f0dfee737e23a7c3391bb9d12a3d675bf09bdaf0e7d4cd7f2200f1c9fac01834ef6373fcb5299d9b89b39408fe9b8be0ffc9254f34fcefdbb717def0861f81a9c929f8d33ffd0b78e0fe4788406148489ac8373d0c0ce51ca639583929b32493dc12a9984445d3f0bb25d72019850825baac0045c4158fc0017949246961c520a5cbf682015d46077aec60f13f53290b3c970d29a4b77922af3155b494db97b184a49dcc92688abd767258d55d585929f6922f7bd5a8fc6a4d0a5c233d65085e58f1e9fe7bef83a3478ec2fcc222bceeb53f041393e3c8e6a17681c67e97482c3dd273868694c4958ed688eedd8fd9aaa58329ae76042e56a61ba0ea3d53cd11e07fdb4fed5544f24d2af07441d9329099603d55bc980ca6e091871e85fff9bb7f08bff11bbf0d0b8b4b8640e4ebd5bc21ef9ca06bd96566c2aab215bc9499cf369c0b7e2c8b335b5de9f6f8cfe4325ecf67ee7c67f61c95266b81f2ab3b59e21ac3c210afa8fc15686edd67cf9d6c52fbc1166f8cc84f199d49cbeac26b4dd5b3b4e3c499b8618e21098cfd4382967641326bb9c298b6f1cd6c152c24146a6d886f1c376abd99833b4ff491ea9b79d5e6007ae16e9d7655dbb05299f262cc9c405ec3e42d1a2b8c5f78c141f8e11ffe7ef8feef7f258c8f8fc14688bcffe5fd7fb68afcfe8fcf91dfffd522f83f3744f01f9f23f8af16c1ffb92182fff81cc17fb508fecf0d11fcc7e708feab45f07f6e88e03f3e47f05f2d82ff734304fff13982ffb43ce12a603ddef2647db83411898dc8b92e92e36991d83c3165797919bef4a5afc0073ff8dfe06b37de46a42023da23d71831a41373d65ced06094438dee54a3aaa9788c59f54892a37558098dca3a8028fa9cc634d92cd8c882b86d0e2fb605a0a9a567fe0d18694fd579e770c6944d91669c5f0f0d0109147a85d5ed672add1a80da10e5a022aae6555564aa2f92a036ea557ee29206029e5082d68ab85edf3b0ea51316fcfbeddf08e77fc24bcf10daf858989716a53875b5485bf6b3ae4058295a374f02b2dc4998b912525f135cdd3cdb1a975fdcf41aec6851f4cc652ec05258772c42e6ddbe59d3a350bbff8fffd32fcdeefff21cccecc03b631547a6d6e31b1cef9dacbc7a20baa3d8595cbb06a14b38cb4256105eded380f2841b4762d1329877cc2107eaab2321ab0fdbc6419a9ccabac061ee98df5db7c53d6167def21849975996a9b2a592669cd771acf6cfea159b337ae0ed6b215a568ff99cd336dca8b51bcc090c45045b7635a391239529555e0b8821630014b99d69c767ba6e299523dff81c7e2e73691b9b0cd22eb74b60c06860abc5d77dd33e02d6ffd51b8fefaeba862dcb922f28e4b8bc446e45c17c9f1b4486c44ce75911c4f8bc446e45c17c9f1b4486c44ce75911c4f8bc446e45c17c9f1b4486c44ce75d9e81c6fbdfbddef7e3f88341679c0a445622372ae8be4785a24364f4c69b7db70fef907e119cfb8168e1c3d0ac78e9d8095d555c37b51e048194c90f1491a2daf02964f383124a0b2e51b5744ca7d66bf6d699611a9a36c35c7e42db693db8a4774ad4b725496ad652e213985482260f938565f6ec954ca9244c0ded76c0f4aae8fb19bd94a4ba6e29171803f55e98fffe7d900575dc9f83b37370bb7df7e078c8e8ec2bef3ce83a98949ba675a3eb2f5b868bbe772934c1802f7674838aabcc753fdef59ef9ae85f033abeb2ffe88c4b4151892e6ad598a9dc56433231c4bcf8fad7ef83fff22bff0d7efbb77f0f16e6178a33190270b5a87cc5ca119058326abd67e243042365686fca04c6789365b0e6c9c33906256990cf95efbb3c669b9638e5ae73d32e90c7796e19074b51f472b5c488c582cd533fb75c3ee5daf34b95f94c24a99cb8648604d5fbff0d436dff722673718659bfb2b2c5a4a5bf994a576075695dc6c7ee35737ee7e05a69aab544361ff73cc6e42b26814d4e4dc22b5ff5cfe13dffe2a7e0eaabafa467cbb924f28e4b8bc446e45c17c9f1b4486c44ce75911c4f8bc446e45c17c9f1b4486c44ce75911c4f8bc446e45c17c9f1b4486c44ce75d9e81c7704ac3a66d77aeff72bc2a63c7ba4ea2ce49cce8d1808fe455222f8af967325063b776e872bae780a2c2f2fc2bdf7de0fcb4bcb3d7c1c26bdb80a39e0550ee27b3c59732b356f3d919af292a80265f520aa27d52d5bb17175202485a050c5aa8c2b419515887c52085712024b8261f28b238701b80a4b9a2b71853e42691ba02435816d5be797a262df78bfda91af4a720cdac64a583875617e0e6ebfe34ef2efa28bce872d5bb6424bd91689442ce2168660c94de64f537343153809bdc2ad05c37be556d6ae8949937aa8ac47e9b5e39acfb905dde23cbffef57be1973ff8ebf0477ff4c7b0b8b04c95af80fe3c8e0f571a83b88bda56b7423296ef2893e7803f89d494978aed67667394c842def938b2209479d153954b8169b7c7f9445b2e4976cadfb49b9e59b2614644295d6ec291ff1c61cb9e97f11d6c3931ac9766aaab85e586c1e5b9069f18e920c1fc2ac555eaac5d4b04831e82a076f855fefe70ae57058c31efb062c9962ea59838568cefd8b9035ef39a57c09bdefc3a387060ffba9e89f25e397b44deffd522bfffe5f7ffb92c82ff6a11fc0bfecf6511fc578be05ff07f2e8be0bf5a04ff82ff735904ffd522f817fc9fcb22f8af16c1bfe07f507104acbacd0e727f3d013c53c17f321c723f7b6c7a8e7e458627bb3cd16230085605ff4f5c11fc6fae9c4bf8dfbe7d1b55c2dab973073cf6d8313879e2946bcb9679eb1ca109c091371cb9c39232f83b139a5aca56b3ca942375d032aee1a34c8523f6c76ff556e69be7b4066f4e56def774d3bf9130c2d5b6b2b202127839acdc5cdd336eda169a7a5a8e346335e33426f738a20bc7d38f8747f43a3d3b0bb7de7a1b1c3afc28ecd8b11d76eede09c3436dd3962eef9a0a448e38a42d91260b6b45ad3dbff07e8c48d5845cd5406ca73c1b63264e65a652179d610b664f2fc0a73ff3b7f08bbff801f8cc5ffe152c2c2c17b35a966464a3adbcf67eb88ec93fded93892105ef019b65a25e14f95e43f435e2b73ae27cfbd84ed396fde539978eebab722955d1f60c710a3ca79864c5512afca3cf67ce533563e16550fcea81a55d6ab8724d7de7550018eb7600951cab62be49d525b4fbef273dfc79a77cfb7cb95eac0cf77bb9fa1761baeb8e23278c73b7f9c0858f8ece8f1790091f7ffc689bcff3757e4f7ff603afbd5b71922f8af9f2bf8af16c1bfe0ff6c16c1ffe68ae05ff07f368be07f7345f02ff83f9b45f0bfb922f817fc9fcd22f8df5c11fc0bfe071569412822222222222242826df2aebefa0ab8fcf2a7c0fdf73f00871f3d0abd2d017343364142872e692c869f62daff31a9c311a19820a3f3922003862cc26417d71e8ea66957d58788564c7ec96d5b355b11cbb76d2a25d9ea3d7e451fbf0a101374fc0d877bf0db28baca45a69216125ae83be43d241e269b69db928d2b1d618c382e0066ede2e212dc75e7dd70cbadb7c1962dd370fec183303a3666dafa2926e76444bc2a296a67cf8f7c4d6d06b15a5446fbc1fe762a1b220296d62d387eec38fcd6ef7c187ef557ff1bdcf0d59b6179b953cc1b76e42194ccf486b447529294007affc38e63c76438bee69677a06c55299e470e6a77c6fe5c930386d844e7e2fdf8f6897400650e386297f62b6305e4aee0bbcb09f0878d3e53012eeb5de3e9e7bd32ce14e7aebf37e75f3997098f9473aa245459304295f8b1e44fb2e3e180f74b55c4700ce357dc1f1e1e82977edb8be19d3ff313f082173c172626c6414444444444444444444444444444444444444444444444e4c92c8d095829a6d86633c842fdfe7595ed26f3ce76e6e246b30a3742ce96983dd998b71b99c31bb946f0bf7922f84f8be0bf7a7c105dbe20a163ff79fbe0852f782eb4db6db8f79efb607979c510302cd923b7157b7cd2088a4fbae2fba67da02587d82a471edd8afe97e7dc6aae6cade6f4ba166ae0c848ca279a285b3588352aaf629725b170fb38dded3a124dcbae63920a9173a0b4ed4aa1321187bcf5c83879d9dacddb8d4725caecb559d342d29ad5fbc82387e06fffeef370e7dddf80fdfbf7c1b66ddbe97e4bb5982746641dc57d09cf0ac103e29a57a6755eaeb022559baa5e7dea2f3f03ffe6fdbf00bffb3b1f81478f1c2b6ee25edaa6b0154dc748741df1ca11f2a037678c78cff02c736c379e91637cb417d770bd2a497ee0e710ebf0ce14fc5c024b22f46c818a54d5f2ee8521e29c702d076dfe93d656b6568ff274d19f211322d1892a7e310132cb3c7258182f56e1578f333860fbd1f97662e6b564cc89c86871886d348b6700931c99ccb877cf2ef8c99f7a0bfcc44fbc192eb9e4a2e239d1828d1679ffd78f9d4991f7ffe323f2fb7fedb5e0ffcc8be0fff111c1ffda6bc1ff9917c1ffe32382ffb5d782ff332f82ffc74704ff6baf05ff675e04ff8f8f08fed75e0bfecfbc08fe1f1f11fcafbd3e17f02f15b09e20b2d1c9b25e7d8f57f2f663f76c8bd9e321b10758ecff082b72768be0bf7fbb82fff5e37f727202aebaea72d8ba750b1c3af4289c3a35c38a7aab029506c027aa38f20b0ab702645293fd2bdbb20130eb455b1354a9885a169a7ba6f295d74a2ddc27e934d7a64297775fd9d673aa6c41c76d099954e24832412526a079ad9eefda12c2fc3d67b6aa93d96bcbd6aee20a56daaff104996ac1f2d222dc7befbdf0b5afdd02a7e7e6e0bcfdfb60627292f467b4158c8f327c1cd0652c8c0ad0595923eb4c085522832e5069258a59069d4e17eebafb1ef8f087ff08fed32ffd17b8f5a6db0de10aa96caa5de6826da96852023767e26eaa42991694e0557532f12c5bf3b97d7a31377acd1c2607b9ca6896e8d7b2f773cabbb5d5a7e8640c23cbde33e7a45d45363b3f68b5e92a4df93ff6b3deb680da6bd5c704c45eeaa14976ff5c35e4b6925a8bf6908755ad94a320f6c400d6345634b9cdb6cb7981585cba3dd8189896a065253b6e5dd82ae65e72e925f0136f7f13fcc00fbc8a9e11229b2bf2feefdfaebcffe5f7ffb92282fffeed0afe05ffe78a08fefbb72bf817fc9f2b22f8efdfaee05ff07fae88e0bf7fbb827fc1ffb92282fffeed0afe05ff67bba843870ee9f050e49036579ee8f14df9ff78ec6b109b92dfbd22f83fb322f8df7c5f367acdb92c4df0dfed74e18e3bef86fffedfff27fcede7fe81c84255d5745047ee8835ca141fca75f98350eb802aa20dd90620ea0b1398b04a16cecb491718428dce5dd12122a510092aa7354840e976bb9e1f6515ad9ce7b4cc1c24b910d9c556adca75975aec3139c791c62a7247d1bab29217d86bc70fd39ae2d21e1aa2cf9ce2905375a8d1d111b8eeba67c12b5ff9cfe19b5ff83cb8e0820b61647408da9969fdc6f390f4447dffd820dacbb3b514ac7040afbd1f12b74ac290b6ec347095c3baaebda3a2c25f8b0bcb70f7d7ef85cf7eeef3f0f1bff814dc7edb9d303fb708ad561b3af9aacd0543243367c8fa394ea5939c17d46092084765fb3baac6949bef4854326420cd1b28cf2307ca0db7553b457ba5b35047ab38ef4eb75bb695f4895cf84fd710dd54a9c27e1235ca10bb6c9b493a6b2688f9ad2b9140d56a41decd6dca683b056d1a029ba23cee5211304302b4f9995ba21d94c4b096ad8245fab99a15ae6b65842bd3313033243df0aad0d93c21921b184263ee91b1b865265704e358f8a42f4e1bbcdebe633b3cef79d7c31bdff823f0d46bae22fb22fd89bcff37df978d5e732e8bfcfe3fb322f8df7c5f367acdb92c82ff332b82ffcdf765a3d79ccb22f83fb322f8df7c5f367acdb92c82ff332b82ffcdf765a3d79ccb22f83fb322f8df7c5f367acdb92c82ff332b7e7ccf6805ac4119844dd60daa7ba3ec3f9ec2ffc7e5aafb67c2de998c538cd95937a76ebcca56d33583e66a68e34c9ee79912c1ffe688e05ff0bfd982248deddbb7c3d3aebd06c6c647e0f0a1c3303b7bda2fc6d4234cece0b682d4b08e09264c8cc9b5ab68447af2b21520b53d7364151a60cdc61eaf515e4128e555dfb2361cc14695c429e52920a24b6e7d71049accb5c4337bb76d03ad0d702432bc69aecbb68b8620430423da8f5e63d7ff01a66c15235cd1ede6f0c8238fc057bf7a23dc70c3d7e0f8f153343635350923c323851f6de0ea4fa53f48c67175b2ca0350bdf17031f1cf08ff5186ccc4ff98bd115587bee76570c94ea7abe1d863a70aff6e81dfff833f815fffd0ff804f7ee25370ff7d0f40b7a329567866ca1281f0dccc19d88069f3977355305dfa4ac42b8a9db2fb2acf4ef9442a5265c85819a9532e8e3d8968bf63a5313e2f831d4358f249824c3c23e297068e86a974061e71d0c64841e9b3b22d39f99c33fbdd275d710c5d9258ff4c7b40287542f91ddc512b9b6a2536ca3ccdec7eba3d6d1a5193a95c657064e269485574968c35fb97535b4e1b134b96b3bb35f9537c5e74d105f08637fc30bcee477f102ebbece2725f0d45deff9b23f2fe97f7ff134104ff9b23827fc1ff134104ff9b23827fc1ff134104ff9b23827fc1ff134104ff9b23827fc1ff134104ff9b23827fc1ff134104ff11ddb10a58eb52a8ce4e466268e34cd86cea4bea5e6cdee3e9774ad6e3d37af7b399f1687a4e67c26e5d5e6cb4bd33a94bf01fbf27f8dffcf583ea7e32e37f6565056ebce166f88fbff45fe0b65befa46b12cdf576ac4ed77210888704ce0efea0d165452cfa7d432c2d73c75618422248cb6b51c71e3259c6915cec0f2443e851604b2ed19c9c5bb91582241353d9c8cccf8b7f324b3462b28f21e498768434d78e95c431e5fc74fbb47be70f6d0947b477c8d79c1d9260b0e29621f6b4a055d8eb1069c91451c28a4fb9ee5045acdd7bf6c00b5ef03cf89617bd109e75ddb3e0bcfdfb616a729c2a0f5191248c39fa4a4435ef0c950b8b139d07d7a02345b14c1b40176bac14d6cde1f4dc3cdc7df73d70d3cdb7c0273ff19770c71d77c291c3476c25a91671e7b04a1312c64c1c721753b3779f20a6a80d24551af3f2ceb458cc98e34471479de65ef167752b4b7a533617f86cb86a54284ce642322056bfe29c41fa12fa6f3c0257cdc9d82df2afab3d82158e77c0969a32ffc164639fd98a54946bed96a96065f74dad11edb92b5b358b7d2eab8d95f9d3ebb7eac97b266075b9b29ca624b315e12cd6722f9e1497aec303d86a6fce5765c956762f3e998ce7e0f5f8d8283cfbba67c23bdff9e370cd3557c1d0501b5222effffe7d49dd93f7ffe6af1f54b7fcfedf385d82fff83dc1ffe6af1f54b7e07fe37409fee3f704ff9bbf7e50dd82ff8dd325f88fdf13fc6ffefa41750bfe374e97e03f7e4ff0bff9eb07d52df8df385d82fff83dc1ffe6af1f54b7e07fe3743d99f1af0e1f3eaceb1c391b40afecfff1c9fd1fad1af8b311495bb72e76b0eca77fff4cc7b05fbf3742e7d920679b8f4dfc395b7f0cf43367b345f0df9f08fecf0e3997f1ffc8238fc29f7ef4cfe1a31ffd183c7af85158595d2df33e2b2bee68af4c95c107b75e630216ded478d34cb2042cd71ecdead4d08b27f03066eeabb22290dfee10caea49e453664946da5401a22255d63e928d907c63083d992538e5652f3aaeb4e4d5fec24bd38a4ef7d8528e5866fdb3958b5c65306cbbd81a296c76682bad962a5bc661fbc3e2bad3e9c0d8e8080c0f8fc0a54fb904aebafa2a78f6b39f05975ff11438ffbc7d303dbd05864746606448c1502bb32df80c094d057c246adbe8f661e362b742cdf58afb9d8e86a5e5555858588463c74ec0e1c387e0ab37de04b7df7607dcf8959b8ab1c7607e7ed1abc8a468bf86d0c3e426306d21b55ef3dcf6e348c6e97fe653db50e5deb99b0a51963484042c4b6c63f29577c05edce3442cf68173c810e172e01e833e81cb9c514e3eb9ca6d3e4950ebb2129597a37cb66e0de72b27841de36b26f72997392683c1e69e75dc5567c3ef5a31e130b3fe994a71994fc052e07ca6d686d60eb71be418212991db1bfaf75131e2e0c0c1f3e0477ef835f0cfbee3a5705e916f8f97c8fbbf3f91f7ffd921f2fb7f6344f0df9f08fecf0e11fc6f8c08fefb13c1ffd92182ff8d11c17f7f22f83f3b44f0bf3122f8ef4f04ff678708fe374604fffd89e0ffec10c1ffc6c8b98a7faa80d5d4581307366aee135dcec6bd6e944fc9640a92af6a6e3fba373b961bf192ac8ac9d992078ff74b59f0fff88ae0bfdaa6e0bf7f5f979696e0e31ffb34fce11ffe09dc7acb1db0d2e9385d254946b93f9b22344e64118f28c22dd0f83ef9835578c0fb4163c9228e0265c95a60ed505b36c5ad04cb0a494cf2a1aa3eec071286f01f26e158ad1957ed52a55df695fc5396488343b662906b9548760d098c5af129e308ef455bc5547d882a19b5c82e9368725db65e344431ed2a18758b31ac3e84ad20cf3bb00f2ebbf4523870f000ecddbb1b2ebae820ecddbd13b66edd02a3a363303e3e46c4ac76ab6ddafa519524dc47977c40fd9dd50e2caf2cd319cecdcfc3a95333f0c00387e1f0a347a815e29d777e9d5a0b3e76ec38cccf2d14f3b182589bf697ebb2ad20c7866267f7ae0d230aa8055eab65da319a8335679863952f73d62d6efde8c85cda546ae24a4f36275c3b496d2b35157beaae7681c95b54ddcadae004d07cde96d047c39921da1962922528e17e20eb2161710eb2dfd4aed16b69692a72d94401ef9c3977b13a98dd7baf9eb2421bf838c13f2697f1986d15e8131199f0e513b04cb531f3c9f8a373ee764c552e2f2ef86f438cb4d5be30dfa8056189bdd1d15178def3ae8757bcf2bbe15bbff55b289f6222efffb8c8fbbf7a6e3fbae5fdbf3122bfffcf9c08feabe7f6a35bf0bf3122f83f7322f8af9edb8f6ec1ffc688e0ffcc89e0bf7a6e3fba05ff1b2382ff332782ffeab9fde816fc6f8c08fecf9c08feabe7f6a35bf0bf3122f83f73d274af3d04accd341e8eadf7e0fcb1c72bf9abf4a7fc0bc1be913e36d1d5cf9cf5fad604f0284d1fe6fde8de28399b1e30a93c42a93bafba786e969fa931c1bfe05ff03fb82f6713fe979757e0c1071f863ffde85fc0a73ef5d7f0d0430fd378ceb6b4697dd6ede4ae3210fbcb6425e4e0746d3b38e7a3f24b3569435ab1641dbc549620e5c83548bed296b063894f8e74a3caca5848a4518ef4949731b5edeff05f4430e239c5fdccab51c43e1a628f219695c41cdf654352e2566e4ca6e1b3c8adcf86b4a4a0873c9377bc4a454ccad160d93ac5b82129a17fc323433039350993139330353d4904ad5dbb76169fdb607c7c14468bfbed769b6286242e24dbcc2facc0dcdc1c1c3f7e028e1d7d0c4e9e3c09cb2b4b70eae40c11b2906cb5b2d2c13e8d441842b24f4e47509e99d9ae2ecf4b6bb77f65ab9ae19a968db3cb5957a98956dbfdd32ab34bdd5bbdac0c68390de7606b498e99b92e2b40e11963d4280735b8d67b210e7c12a002b5e60c7d31adfeba2e57885067cf9473b5c76fbcaf4ae2959d50b6fee324a6f3ee82230e4249c072444315f4a1d750ce579925fb31d1d1e608faab8dbf99b75697b30873d41ad18ee35961d5ab57beea9fc3ab5ffd3db063c776181e1e827e44defff2fe6f624bdeff83fb7236bdffabfc4c8d09fe05ff82ffc17d11fc0bfefbf1679039827fc1ff7afd4c8d09fe05ff82ffc17d11fc0bfefbf1679039827fc1ff7afd4c8d09fe05ff82ffc17d11fc9f23f83f5304accd902687b09136fa99d3d49faa3d34b58db2910f89aa84665b4d92bedf3309f7b219e77ba67273a3a49f186cc64b7d1011fc0bfe9bda4ae95fafae2a1b82ffcd91f9f905b8f1c69be1831ffc75b8e96bb7c26aa7632a4f812160e55d43da31be29fbdd10954c0bbc9c2a063141a5070bf6537bd7f89dd7684b72b20ddc9c503ca0249db8abe27f8ac960b65da16a296720f75ab6715e964a0d198a09304862c96c35a51e51406d0491f0c41587dcd9a89250539280c055e7a29870d528d5f648335ccdcbdc3715c18a382b7bee765eabddb6b672222a71ab3f135353658a6c68e3bf218b29226ae55d05761aedbfd3313ab4d5cea4a7cceec16fbf58ee3db715bccc1960c5a91e12146d52953ea912f75c2d0b2b6785826b31a6ddd5bccc2595597214b7f353f67c540fdbc8d68c02df538e3d11a2d02dcd510657850b2f5aed1691f23413b06cac5d0536fb9d2ba2693f7ffdb336096f86b3de2a5fb46f8e0fdeb315b41c51b03712165b00251a38bfc09da769b3c99b5d4b6c63fce0fe878786e0e94f7f2afcf88fff183ce739cf86d1b1511844e4fd2fefffa6b652fad7ababca86bcff375704ff82ffa6b652fad7ababca86e07f7345f02ff86f6a2ba57fbdbaaa6c08fe375704ff82ffa6b652fad7ababca86e07f7345f02ff86f6a2ba57fbdbaaa6c08fe375704ff82ffa6b652fad7ababca86e07f7345f0df07fe91805535b909e8aa9ce2f5aca3e9da416cd6f932e85af665a365900754ec01bd19c9d7efc3bfdfdc88c9997e68d4c5ef6c7e583739fba6f704ffd56bd9978d16c17faf08fe9bcbd98cff999959f8c847fe14fef88fff37dc7fff43d0ed6a472c32849e8e21051111c85610b2adf19014a52ce124e5075fb33fdcce0fa0ac0054e69865363141c5b66f233db8de9266ca4a59a6c29369d507c61fc04250c67fe5e245de988a5ab68a930e2832446ec90cb987c848cae8a2aa43ec8bb786c9481a028ce88c6c1bbfc178e3b6877e632cb1859cad9445a422acb0d4822e1171ac4eda0fd07cd4d3a2a25cb84f65c946e519b75b2d58ed745d6c90a44344336295593f5c2e183255ef81e56ecfd4dece12d6727b5e26d6ad9e33a54f0dc0ed20b95a5a28dcde10738862a9bc73231d1a5c552b1b738e2911ad7ace48436fc52b932fda11ddcc3ab495eb8e3b57ce25d0bab76215b7c3d41ad892c2fcb06432aef2a69425a1715534db06b08c59d9ded055cceac909e0c324f21b9e1fc7ce8fa752e598e63cb4dfb99adcc8f0305c7ae945f0dad7fd007cfbb77f2b6cd9325d4643deffce978d1679fff78abcff9b8bfcfe6f2e82ffcdf14df0fff889e0bfb908fe37c737c1ffe32782ffe622f8df1cdf04ff8f9f08fe9b8be07f737c13fc3f7e22f86f2e82ffcdf14df0fff889e0bfb99ccdf86fbde73def797fbfca5300da0887d6737f3d3662e355c01cc446ec1e5fe367ea5e952ffdea49cda9d39bbab7112049c5bec9dc266711c6b16e7e93798348a87b10bfc2b12639513536c8d909fe05ffbe4f82ff66f264c1ffe8e8085c7df51570e59557c0f1e347e1e48953b0bcbc5ad8e97a3ffaf85fb63d9a321599fa39f7cc8b23e955cab605348425650949658523ae4e0454cd881d51e4434615b0884855dcc37a4348bea23676a0bd0a5c8674c555b050572b5bebb3b6e419fab37bd63ea94c5b120ccd33641beada677d36d5bcc05658322d00499d0b40f9857ec8a91672d82c6d28a30a57606360b69a81a9d8655b31927b2deb9a02537c49436ba84dc5aca84216989674589d8c484048fcb2bdea5c7cddfeb4ef9489b3dda7f131b324304b5663bbdae8a6985b15dcfad1cf4b4788f2f69f65ca91a4480f57e7b23a7c121593cb5cecf8bbeecd276a29a9ca71243629eb9fb6beb2705533b74ffb2c54c1b9a360352fff79e9ec5b1d6c525b3db87faabea6b5235ff5e254d1d9a09adcfa923bfc184222e38a7d221d5c650bfd29666e999e8217bfe4f9f0ae77fd14bcf8c52f84b1b131a893cd783636d12bef7f79ffcbfbbf774c7eff6f8c0dc17fb5ded43dc17f7311fcaf4fafe05ff02ff8ef1d13fc6f8c0dc17fb5ded43dc17f7311fcaf4fafe05ff02ff8ef1d13fc6f8c0dc17fb5ded43dc17f7311fcaf4fefb988ffd6bbdffdeef737995877af2aa9ea12ae9fa46fe257dd832ce697fb3f8c55ac8b7d6f221bf5b04b8d357910860fa2d4584abfffd9af3449faf5da68622f66d3ffab9bbb91bef47bd6a97955391ae6743f0f5fc1bfe05ff02ff86f7a2f3c3f6c6577fef907e039cfb91eaeb8fc3278f4d147e1d863c789084355a1724370025d92920c27c7104b903ce2c82e9690d2b36724a660eb3cbb06ff703655f5a1a2429923746544902a2b6b21894869af8a14926b942549695335c8906bb85296dda3f3523b1f325b5d4ad31ab0d5b02cd14519dd5c9949f11ef15f5c118bec64b6399ea26a465c9988fd298d1b5f98a4034c2eb2241d771619943e2be5fcc8bc7c31b5b74cd53065f76a6e65d052992599d9fcd04c1232e42e264fd910d077ed2a56d9d6846e0f7c5e86dc86fa731b3f22bdd92a4e4e7fc695cd70c0b619b46764ee59729bad3cc67e9b35e51c33bf457b5401714bb9b82a6a2d58e69f763c32650dfb6d2a99b4c77bd310600e494d59e62a9e19921bc7d756a1020f77fcc7d871e797b9dca773358b5c7bc3ccde27b2992eab5c316b2ccf3b8e4ca6b5f6c889e0c85ca3a3a3f0ec673f1d7efa1d6f83d7bef607e1e28b2f741803583ffe636375ef5379ffcbfb7f501b4decc56cfa7f757337d2977ecf3a35ef6c7dffc7c604ff83cf13fcafdf5ecca6e05ff02ff8afb629f817fcd7dd13fc0fe673d37982fff5db8bd914fc0bfe05ffd53605ff82ffba7b82ffc17c6e3a4ff0bf7e7b319b827fc1ffe3897f2260c59c0a9ded57b17f3f75f0a9074295dd8d0405dbf193b0ea10eac653fba93b5cbe6e02063f3e653591fe93b2ca46cc9f2aff523aeaa4ee2c9b0036962f295d83ec29356f1090c71e9010d913e74b0c07a9eb301f62fe367939a7f6915a5725827fc17fd3f982ff7313ff13131370d1c517c2d39f712d6cdb360d0f3cf010cc2f2c98fb00bd4dfb2c5184c9275c5988fce029b6820fb06def1e338f287eb62a157e76b1959e32841513031ba72cf3483b6be3e30860b921b99059e44dd936752d4bb841e211574352c62543c4ca8c3faeed1bee0d5bd1e9d2bed19f97fb8120ae5a7bfb531ee14641c9d8515e15aa40b75d6fda1d1a12509665c17ecd1c1aa7bd0115ca3215ae5875b15f6c1ba9c0cdd13958a25c56e68a2e4962ae4296f39fad414906b304a1cc56feca429c40805724d0d973f5cf49f97aedbec19eb9a904a5ac5fd0a38f0860dad3677db7db702d22716eab6d8957fe733738b7965fb94b7b643a9e6749578a2b5c7979c3b8c7bcca7d529ddd530f4ea1374fb44fe6d2da55ee727f363e4385eefdfbf7c29bdff2dae2ef0d44c29a9c9cd834fcc7e6f9cfb8703cf5dc0b75c6f4361579ff57db88f953e55f4a479dd49d65ea7e787e61bea4740db2a7d4bcaaf38cbd4f9facefffd83cc1bfe03f9c2ff817fca7ce33a633a6b7a908feab6dc4fc698215c17fafad98af299d827fc1bfe0bfd97dc13f24d7c574a7c6aafc0dc753ebaa44f02ff86f3a5ff02ff84f9d674c674c6f5311fc57db88f9d3042b82ff5e5b315f533a05ff4f4efc5756c0aa73ac2a799b246d6aac6ece7a801e8ec78293ba1f0349ca167f56c5b05f09415637b7e978d559c592d49fb31e3f9ae4471369f2a00ecf3176a6311d293b31fda935b15c6822fd9c77958eaaf36b623b3657f02ff817fc0bfe9bac6fb532d8b56b075c7ffdb3e0da6baf8695951578ecb1e3b0b4b4045ac31a2c91cff63b1372f89a2d7295201deed5b69ecb6c65259cd3ed6a4782622d0a4c5b38bbb254420e9536dc7ceb5f665b036acb75f185f6611cb1158ab4f301380ff8bb9d1704ad9ce3f9c39589726b3453cdfe638874793fe6718d1f33becfb6e94f43b97fb37543e6b16ded6c389c9fca4d37842bd31a3073b6cc5e425fcd3a055e8c6cf52cedaa38e5e07735742d09d1768230c77be14a51e4a323b759ac66419e51b5afbcd745ef6ccddecb969521410c62cf0d5de69bb67fec8fcb5b93f8ee9a3da42de8a04da17f6dd7694e40ef1cb86d219f13cee12a71480cdbbe7d1b7cd777bd0c7ef667df01dffdf2efa06bbfea55cf096d20fe533a52637573e4fd2fefffd43a79ffc7edcbeffff8b8e05ff05fa5236527a63fb546f01fd7911aab9b23f817fca7d609fee3f605fff171c1bfe0bf4a47ca4e4c7f6a8de03fae2335563747f02ff84fad13fcc7ed0bfee3e3827fc17f958e949d98fed41ac17f5c476aac6eceb984ffd67bdef39ef7431fcec70eb40e00bed3be8eaa04abdb6c3f87d0f4906300dd4c09e3b3110f68fe0c635607b6a6ba63dfeb80d52481d5800ff9ba07c12052f592aacbddba07550a4f7567d0f4fcaa30d7afded87dc1ffc689e05ff0ff64c23ffe9d77de3e78ee73af872bafb81c161717e1f4cc6958585cf28a39956bc88632e4122695f8b1f25b14f6dcd3e57a552a03be459f5a4349c7ead9a0d5a1cb8a5648e821a28ab6ad05cb8a57607d6445f4556b4718a2f95ecb3a57c1492508449c0ffe98bf17e5efc79b675b1afac21596709cec32fea0dc3fdf2f756625c1c93b6b0d3e098863cd67e4175fd2ceb60ed85c3998b68c2e3656a772a4b65e021528ed14677e65297b3e2e1c9e9f2aaceea5b9e29725650579cba431aae8a54b5fcb49fef1986a69991fe730dfb5eefdd1ce7efbcf0d9ec744295ecfe4299b78dad71b9c95f6f24e712e26b0b97bd77678e94b5f026f7bdb1be1077ef05570c10507217c26a744deff9b27b1e7e346e84bbd5febd635d11dfb9e7a5fc6fcaaf24fdefffdf914bb5785b97ef5c6ee0bfe374e04ff827fc1bfe07fa36c0afe374604ffe9fb82ff8d13c1bfe05ff02ff8df289b82ff8d11c17ffabee07fe344f02ff817fc3fb9f19fac80c5139a26f72001a94b8e26f7c2a085608fe988cd69e26f93fbb1874decde463d6c63e3a9071940755caaec0ce273bfe71a7bb0877b89c5b72a1655dfeb6251e74f6a4dec0193f2af09be620faaa60fc67ecf3b65b7c9dc7ef4a208fe0717c1bfe03f66f76cc73fce1b1b1b838b2fbe10aebbfed9b06fff1e3874e8309c3e3d079d6ed7915b9088d26ab7a1a5980064842bfcb46c4b3dd7fa8fab1eb95a42bdcf19130b5b9929b324184f278dab92c4d3b32f151098bcb5ec83f1433b6210d8753ca7e75cb2cc916fa8029271c291ac14efd35b837b365595c0d8609252e6b5f9e3f95ef5257f1f3de7a9d49ad686e66be6e2a4c8ae2e0957dca210ec1856b052f6c7b18ee7290780485dc0e70350f6dd331774c6da54c2a2e1dcb1d94a1f798f762fd4ced18b33f00f757b86bcdaecc712c6acae924455e6885f398d3c0daa6565cacef362ebf22fc03c9f2fe72f700e2bef3f283c1d2e666827b37bc29ce3aa56d626e7067836706ecb91daca2a5dc323c370dd75cf84b7bded0df0233ffc1a78eab557c3e8e8485f5895f7bfbcfff97390f7473ff7e4fd5f3fef89fefef7e70afe0717c1bfe03f6657f02ff817fcc7f5a57484df9bfa23f8ef9ddb8f5e14c1ffe022f817fcc7ec0afe05ff82ffb8be948ef07b537f04ffbd73fbd18b22f81f5c04ff82ff985dc1ff130bff44c06a92c04d8c0f22750f8cf5e88d7d1f54877f1d1e76782fb6b6899da6075df520d8acbd5601bbcacff59c635d120fa22b755d351e3e009bbcfc52791d9bdbaff0daf5c6d7d7d7cf78bf73ead60bfe05ff757aaac6fad195baae1a17fcf7bfb67e21c0e4e4043ce52997c2b39ef50caa8c75f8f0a370faf43c746d9528dc636e8c782dda4a128a4fc022128bb68a3d31fc1e5b912a3731d3414b3c05e048343827cb82fcd53e035ff55ab08420bbc09169a267cbfaf02f2fab3fb9b3740e5bf28ff6aa4e8197fbae8d1d94a42b1d54c4629f74f99d8943aec2942506ad3918ce4d9a17b4ff2395da10946c2c15d3de82bc54612c4ae748af4f72220d9989bf216c71cb488b277b862e1600bd6437148f24c7242cb0fe06bd2aa9b816780d14b9159f5f69ab27b7f93958d8d0e83bebe3b3f0dafef9f3d907bfca57cf9987cf25ce27ad7bf2cc7ac420307a3dfd3896dbfdb7da2d3870703fbcfd27de0caf7fc30f11096b6a7a0a52f2b8e0df5b2fef7f79ffd7e9a91aeb4757eaba6a5cdefffdafedc7b6e05ff05fa7a76aac1f5da9ebaa71c17fff6bfbb12df817fcd7e9a91aeb4757eaba6a5cf0dfffda7e6c0bfe05ff757aaac6fad195baae1a17fcf7bfb61fdb827fc17f9d9eaab17e74a5aeabc605fffdafedc7b6e05ff05fa7a76aac1f5da9ebaa71c17fff6bfbb1fd64c67f6d052cff33653cb66610e957679d2f4d74349deb27bdff59b7b6df036912df4112b6698c0639bfd41eab1e321b019094cda6f3f81c9be84fbdfcc2b9fe0babe9d9d7ed61905cf0cf3295cf7522f8ef1d13fca7d7f99ffeb8e05ff05f25ad560b76efde09cf78e6b5f0cc673c1db0511d56c45a5e5a46654448716dfb98b8829fca23bdd8b68040c41e24a658a20efe61db3865884479f13d6bad8d195d713521d747d0ab760486986308406ea3c63f9e47ade3cc5a438a294949c60fed8860fcdd4e2873c5faeb936c325b2d0bab3de15f9e777b2a74b1afbe8d9e31b3911edf94bda7f89e7f7e598b7c413b002521c99d3912a6726f3e28c7f13224aacc86460501b6f755994354a9a9b087135cb52937b7770b10e61cef49eb1e1292c35f4fcb4adc3357f632e78495bb5cd532002f2ee68cb9125619664555a8b80a9956250990ec79d7beafeedafbe423e2f9dadf83977b78de44f8b2dfbdbe94eedfca3b63d4b773d70e78ed8f7c3fbcf3677e1c5efad217c38e1ddba1dd6e4328670bfe9bac95f7ff5a3f62e3757a9bdaee57e4fd5fce95f7bfe03f762df88faf89ad1bc476eaba6a9ee01f1af925f817fcd7adf33ffd71c1bfe0bfa908fe05ff83da4e5d57cd13fc4323bf04ff82ffba75fea73f2ef817fc3715c1bfe07f50dba9ebaa79827f68e497e07ff3f0af0e1d3aa4638b5349d644584f131db18d5425796c5e2a28557e34b5d14406d943ccd7aaeb7e74f7eb074b53f6669d9daa788489b81167d0c47668abdff3a99bd70498fdd88fade73da4f2bace077f4e958d98ee7e44f02ff817fc57eb13fcc7fde9743a70dbad77c0c73ef697f0f77fff8ff0e0438fc0d2e252f943065cd73a4b8c32c418a307c2fa54d60fd3952fef1aa24faebb6b26b8d8717bb71eff0017e345390fef718525f65f29e71f8d59621813c7f01a0967dc4ecedf3fb756ecf10949574ad138cfe922018b4a3081ab64957b44341e2b496940d5b67a983f15e792b58689948495a8a8ed612bb355bdd0cfdc54116bd9168aa82eebcd19f607f7c963dc4e50d90354365660ab6811a98cf3c0cec37d22090a0973b4ffa0ba950efea38086bbdddefdd38632624b51fc9080e5f53ef4738f62db35364d6c15743b5d57998b8866e867cbace5b90a4ab254eeb504747e72cced79b4ec3c65cf21d7ba975017884f24e3fca45ce9da9815f7468787e1e0f907e0a52f7b317cd777bd0caebcf2f235cf593f56f2fe97f7bf7f9f45deffd0f87e384fdeff715f9bcc15fc0bfed72382ffb82f55fe56d988e9ee4704ff827fc17fb53ec17fda1fc1bfe07fa36d87b604ff691b31ddfd88e05ff02ff8afd627f84ffb23f817fc6fb4edd096e03f6d23a6bb1f11fc37cf2f5701ab5f50c73e63739a8ca7e6d6d9f6415795f8fd8ed5edafeeba4aaa6236c8bcd89af50ac793bffb9f679354e55895bf3e60eb72b8e9beebf27d3dfa077d58f78b41c1ff5a1bb1f982ffb34304ffe9f9fe67eafea0363613ff482ed9b367373ced69d7c0339efe54181f1f87f9f979989939edee2369c5f52444b1841647be5225d7082c710805ab1e29bb4e41d9768e86f8bb9f1b5689e2aa58ca67dcdb7851a5241b37c56df394a9b8e455d352de7a3ff758077db73e717b39bcc547e27e782b173447feb1137aab6bd939beed8cc949ca548352b6dda0f3ddfa80eb9128a4942adb22da98b30d652b5d7987e8b52134c438cde36826f3aa42918f7c4eba27ef8cfb962c451779193730ad2695e7277869c07ed00f5b8ea123b52973fe3d143d43c8a338da568a10c4d4cd63df998807de7f50fafed8efcaf387cf40b13f4e575e5675f3e6b25dcd7186defff042c1aa56888d8b2fb9105efef2ef8037bfe547e1bbbef365b06fffde32af9f80f8efc7b6bcffe36bd62bf2fe6fa6279c5b352eef7fe87b6e9d6dc17f7ccd7a45f0df4c4f38b76a5cf00f7dcfadb32df88faf59af08fe9be909e7568d0bfea1efb975b605fff135eb15c17f333de1dcaa71c13ff43db7ceb6e03fbe66bd22f86fa6279c5b352ef887bee7d6d916fcc7d7ac5704ffcdf48473abc605ffd0f7dc3adbe702fed7b4205c0fd062093548e2f593984de73591505f95fed83efde0c60e2f963475fef8ff87b74193b7e999a4d6f693dc55e3e1fd26f18e81a1dffd34cd937e74365d3f889e26f888cd6d12ab3ae9777eb856f02ff86fb22f5f77682b35b76e4e6a4cf0df5cfa9d1faedd08fc2381646c6c14f6efdf07cf7ed6d3e169d75e039d6e07e64ecfc1a95333663d4fb6799959429021d2d84fcc5bd096b45312887adabaf9642800afb2936d63088e2bd4e3b731ab6cab3a26dd28f36fc5ed0a9559c804311f47bc07bb5e31094b1b3d4860320e190ff01e13a8c0129894d5e305d6679e9504255c4ba65a861c45feb5202f77e6fe14940433d37eb08ca50b3918821391a7f2b26a18af337bd4762c7361e8c9011eb1fbf52b6929f016e878ec4ce84ae29a3f0ede79b2118aa70d1a55c1c276948e7cc5a42fff19ad5ddc71ae8981b7015dda29cfd29be0486a011e6cdca8bd2093e670bd9d9b7967c767aea1cc3d26e81d38701e7cf7cb5f06ef78c7db888075fef907607864d89d3780bcfffd794da4c9fb28a553defff5e3e1fd26f16ef24eabdbcf46e1a01f1deb39b326f888cd6d12ab3ae9777eb856f02ff86fb22f5f77682b35b76e4e6a4cf0df5cfa9d1fae15fc0bfe9beccbd71dda4acdad9b931a13fc37977ee7876b05ff82ff26fbf27587b65273ebe6a4c604ffcda5dff9e15ac1bfe0bfc9be7cdda1add4dcba39a931c17f73e9777eb856f02ff86fb22f5f77682b35b76e4e6a4cf0df5cfa9d1fae15fcf7877f2260d51d5c4a79e88c6f20e668d5f7988e7e1f50a12fa935b1ef4da44960fdb194af55f363b1af622656254c2af99bbe04ea6c57ade9f76115c6a74ab77f1d7ea6ce2835bf1f1f53b1a88b23cff1f53439a7bab3a95aefdb82841fa931c17fda9ee0bf5a6267d2644d189f2addfe75f8993aa3d4fc7e7c14fc9f79fc0f0d0dc1debdbbe1faeb9f05cf79ce7544403979e2242c2d2d531bb86292abf8e3b7f053b6b212e9b58417fcdeede6651525ad5de5a8b22da02232d09afff0b17381f566489669d17cd7decf13edfd1b98d4a44c0527bf5a92f26a3291394b48d2ce7f5b69ca0b39b5dfd3a57e2240e5bde424feaef85319729729ca65ab82e95ebe164d6d7155305dda50d0531d4a7904a9b5b957c6c190ddbc5c8f3cd7298e59e6e268fcb555a8ec756eab53f96766a7f53aef5cb0e4a5301ff93cb57655a5683fda54fca26b265a696ef7e7e53d9495a8fc0a5fda6b29b8e61966f3cbf9ebc5c0e53957b9f25a4dfae432bc97592c9c77de3e78e94bbf05def773ef84effcce9715d7fb693c26e702feaba4ea595af54c8c7d6f22a19fa11fe1dc94af75e752a73b6527b636365fdeffd539d2c476ccffbaf5be9e26e754773655eb7d5b90f0233526f84fdb13fc574bec4c9aac09e353a5dbbf0e3f5367949adf8f8f827fc1bfe0bf5a6267d2644d189f2addfe75f8993aa3d4fc7e7c14fc0bfe05ffd5123b93266bc2f854e9f6afc3cfd419a5e6f7e3a3e05ff02ff8af96d899345913c6a74ab77f1d7ea6ce2835bf1f1f05ff827fc17fb5c4cea4c99a303e55bafdebf0337546a9f9fdf828f817fc6f34fed5e1c38735df882989052306307f2c04494a7f6a93fefc30c8b1b150aa801203703ffbf0d7e9c8ff312d6537b411ee35b417de4fd94ecd4bd94fc5368c672aa152fec4f6175b5be55b6a7d4c421faafcaad3179b5b87872a3feaee559d776a3cb5d7aab36f1283baf582ffb56b05ff82ff2a3feaee09fed352979bababab70db6d77c2a73ef957f0d9cffe1d1c3a7498c8583827d725f106a5d3e940abdd766de8b89a101373b471d6f9acc13b836eb7ac82e5d9a7f6871a095d85deaedd8f32eb9800e6c84a60aa2c3141479b0d9a38b8567499adb865ab69a99218e4c84b995e1bbbbcd7b6a944a5ac1f8648c4c41f5e9b15ff10790bd7d2ba8c4fafd777b5e6501cb9486b5b39cceae07b3dd3a9b294a25b9d4ed76855813e1b7b73be60dbf1793f4e1547b1acd39533d189e7d8796ba498473ee2bce21cf95c7ddd980344bab3fb4602566e0950ae2da0374f8197e39c93e091b83846b47f3fee1a5c4604cf50b0f6d957d6877ef86787d5e02eb9e46278f14bbe195efef26f870b2e381f4646869f94f88f8d8552b757ff3ab6b66a1ffebaaa77456837b411ee35b417ded7f2fe5fb32ef54e0cf5cafb5ff0efebf6af636b05ff827fc1bfe05ff0bf5687e05ff02ff817fc0bfed37ed4dd13fca745f0bfd65fc13f44d709fe05ff3111fca7ed0bfee3bac2b9827fc1ff99c03f11b062375260a97a60a48cf613c0263ea4d6a4e6a612a2ca377f6e6c6dd503abe98134894555cc530955f730f5e7faf7ebe25077ee55b1abf2b1c9c3c1f73505fed8674a62200aaf637a9aecab4a520f0d7f2fa19d26dfabae533996ca13c1bfe05ff02ff88ff97e36e01fc7575656e09e7bee871b6fbc193efde9bf863beeb81b4e9e384555b1b42537e596dcc25634afa73683651b38b2e3efc92717a12031a7f8def6483a48c03215a44a7f892064ca5c992a567917b0f521dbcd83fd18eda6cd9fb203c63f1b23db4e31b72df3d80ef9e9085c2e30b4275f326e13c83e428b484e8e6ce511b0fc3d30218c74e6bac761da3bb63224fb50cee1aa61c5d7563b036342bb983241ac670fe62630c34af13e7a24f708492626da91c5ecfcba671dc50bab5cd94a673cc6bee85e821b6dcb9eb5cb171b4f17279ecfa42dcf0fa74ba9b2389a5a6b93f414f9dab2042c26f7e17aac6ab573e736b8eaea2be05bbee585f0bce77d131c3c781e0c0f0f977e3e49f19f5a939a9b8a49956ffedcd85a2defffa84e79ffcbfb9fe7c6ec08fe05ffa18f82ffdebd847604ff71bde19ad45cc1bfe0bfca8fd4b5e07fadef827fc1bfe05ff0efcfafdba7e03fed97e05ff01fee25b423f88feb0dd7a4e60afe05ff557ea4ae05ff6b7d17fc6f30fe0f1d3aa453018d2561930df8ba9a2666cad1260f8fba6056255993a4aa3aa0704d5dccfa8d675d7c52d2e4615407ec267911da1c445231a83a9faa1c08ef873a7d9b31fbfe9a947fa1efa93d0d3aafc9835e35789055d9efe79c05ff82ffd81e439b8388e05ff01f9326f8c72a57478e1c859b6fba153ef2c7ff1bbef4c51b606971716d7b38f481494b9e5eaa9665bf6b1ec779b6051cafe33685cab34fea705e318e6b4de52dd49113e1873e550ead56bbc70fbf55a2a97ae5554c32ff73adef348de7c66dad1df14759bfbb36e654c1095b321a45ae7a179f89321ba46b32a3b4a98865dbfba1740b3b2ddbf2d0d9f19843ae35a31dc33de6c0edf7fcb858b213b67bc44fafb51edecbbc168f3ad7b63299dd3880236ae53a7773d7e416ef31227c5eb4ffa00216ee2bf3ce8174688f94e5e95699f15dd9fbe0e51dfb82ffc6b86b7b06a42b2fab7601b5612c0963ca237175f9bc1418529bf56d7acb34bce89b9f0fdffdf297c135d75c09bb76eda678fa7605ff506b5fdefff2fe0fe734d9afbf26e55fe87b6a4f83ceabcb7dc13fd4da17fc0bfec339827f88daaa5a23f817fcc7d609fe05ff827fc1bf6f731011fc0bfe6322f817fc0bfed7ce8fc52814c1bfe05ff00f6be608fe216aab6a8de07ff3f0dfae3ac4a61b8f6dbe6a13a1b3559b8c6dccb7e97f560593c7794e5df0fdb1706dca9f98af311ffccfba048fedd79754fcea1e0031dfc2efb133086dfbfea6f65d779e2910a5f61ada8ec5211c8be55a6a1f2980f9fb8cd98c9d6b4c9ac427159b304f6231f2732795d3291b5522f817fc87b605ff82fff0fa4ce21f492ffbf6ed853d7b76c3b3af7b26dc7edb9df0894f7c06bef8a5afc0e1c347617565c5e8c275de5a659432e7c7e9a6f6841e79092573441f652a30e1b8b6d796d44515affcf347621355c2ca7a5a10a25d6e7107761ef0bec83f6de950e63be8defbca8b954f0853107f06f01c379ff7a873577109bb202201a8a5ed5abf07a15a9b47488cc24a574833ca02c296f937128e2c81cddad72e27c1c582ae331b1722a26922688177ce442a53a547ce8fc8b333145c93937365b53022b1d97b1c5bd2cf9f763e9f0f58df31665dac6806651e31910b9884e7e9736d2e3df21e5fe399b7b1829a9d8731c08a57fbf7ef83ebaf7b06bce2952f874b2fb918b66ddf4a6b05fff2fe0ffd0bc7e4fd2fef7f7f5dca0ecf49d9a812c1bfe03fb42df817fc87d7827fc1bfe05ff01f5b2bf817fcc76224f817fc873e85eb6336fd4fc1bfe05ff02ff817fc0bfec331c1bfe0ff6cc43f55c08288a49224a53cb52ebc176eb4e9bdd481a76cc57cf6c7627aabfcaedb539def1ba1af89ef55e04e3de45289947a10ad0738b1fba1bd50eace39352f95b34d1e3a29b057d9e1efe1fcd89aaab5311f9a9c754a772affab44f0bf564fea9eef5393b582ffdefba1bd5004ff82ff7ef0bfb4b40477df7d0f7ce6339f854fffe5dfc0a38f1e81f985453357db1f549654e508329624857f3993aad09eb6d58fb4ad64555c23294815df917c44d7e823f961d618a20e40d632c421acd2d56ab7a8551fcde2d680796ed7643dedf474d7b4f763521756aba2addb35ad769bbe630525b70f8e037e5a3d2d5b7d49db31adcbff78309fb8575385896dd1f74c59bfbc38db1682b45ffa8eb68ccf99ad220576086218c7d02b9e00446553b6ad20c74b513c4d0bc4ac9559ff56d79c2fa9f0aa94adc9018e017fb7e74d7f3656ee8c2192ebbc357b26650bc4f2dae485a6b8f554e272b1d62e7fdcb5d5d9b5678879343d3505973ee56278d18b5e00dffeed2f818b2eba00464747dd990afefbc77fca677f2ca657deffbda2e5fddf735d6787bf87f3636baad6c67c10fc0bfe05ffd522f88feb16fc0bfe7ddd827fc17f9d08fed7ea49ddf37d6ab256f0df7b3fb4178ae05ff02ff817fcd789e03fae5bf02ff8f7750bfe05ff7522f85fab2775cff7a9c9da1e1f9880d5e460aa126f8de28a436f9a04a9b5285509150b6cea815307cc98ed984e5ffad95bd5fca6a0ede761d2e401d8e4bc52e712d353f720eb679ffde8ad922af035017a2ac79a9e6595dd582e367990d6e553ca97d458933d08fed78ae05ff09fb217da10fc43df36519ae2ffe8d1c7e0f6dbef80cf7deeefe1ab5fbe11eeb9ef41585858e8d1e32a19e9924c45558f2cb126f75ac8e1dc1cb8fa51cb2ecb1d49877c71ee68222a39fff093c93a9a99f5adc0774b42b2fb3044215db6c743abd86ed0dbb36b71677553b5a66e6e8854e48f4665648b565932155802161289b01da1d1d14b3872fbf208581433bb576a134855b08cdf7ccf6fef6762addcbe788d3b2b1b5bf69363a85ac63f9ca3cc6467bfa74d64984bc1b3505bc214ce6f65bd2d117bf288e7d398267295892512ee6c7b4136119ea7b593593d9c7fe49fdb93d9df96e929b8f8e20be159cf7e06bcf005df04d73eed5ad8ba755af0dfd0264a53fcf3dcd833ddff1e8bb96fc7b71dd3e94b3f7bab9a2fef7f79ff0bfe05ff55e382ffb4de2a11fc0bfefdef827fc1bf7fcfd72bf84fef41f0bf5604ff82ff94bdd086e01ffab68922f817fc0bfe05ff827fc17f4a6f9508fe05fffef7c70bffeaf0e1c33a65a84a51d3030b9d4d01cd97ba397ef0c375558919ce6992d0319da97d0d92d8a9e4acf2b12ad1d99770be3f5ee5476aafb1f5fde649dd39d79d4795bf75f955152f7f5d551ceaf2a3eaecfc35293faba49f3c1b746ecc56953f827fc17f53bda1ae943ec17f7abfbc4ef0df1ffe5166674fc3430f3d025ffad257e0cb5fbe11bef295afc1c913a72c1907ab5f65549908ab4f61f525aa6e847ad0069f27786dff885c042579c8568c22c252ab458425a6eb28dbbad0a85274af5cc97bd0de3ea0acccc43a94dd37fe79442bd746d04ed2d63e5657caacef4cd6c276880a4a93dafe8b5a246666aeef9f892518ef3916ecb98b897267c1fe613bc36ede09ce0e2b8769135bda02ce47cdb62120fb68ab5431e9cd86d991cc784e4f5b47a57aec7b01831ee17cb179827fdd4e1703690961ba3c6b8de432a071228cb5cab6914eb7aaf841aeb53b61247a0d0d0fc3b6ad5be099cf7c1a3cff05d7c3b39ef50c38fffc0354ed2acce39808fee5fd2fefffb4bff2fe17fc0bfe05ff827fc1bfe05ff0df44afe05ff02ff887a46e5f676cbde05ff0df8fcffc3d358f45f02ff817fc43729de05ff02ff817fcc77c59cfdc98ad2a7f04ff1b807fac80d5c4a170b331a94ad8410eb5e9215781ae49828573f95e6ccf29d037015bea6156a5afcad7417c89e9aa8a49134901a8ee0117de0be3117e8fdd8fe9accae17ef32fe59fef7bd5bc2640afda73ddf957edc5f7b1894f827fc1bfe05ff00f70eee23fefe6b0b4bc0c37dd742bfccddf7c1efef10b5f846f7ce33e585ded583296599fb56891abe2c4ba0c31a898a4b2b27296959c8955761c094ff41d956a1a80308a86a6a3dd3cb717e5f51ab76423a717f7e655bda2b68458d949eb9eca533c97c94ca55faaa79a1555c46a65bd73b52149d1be688e69d9e7fb4071e2fb1c6365aa46693bdfe513400f59caec97ab6c757b885c669d8973ab652a5651152ab6c92d1c3d7bd665b36ff6c1cb69bcc78435ae5285abba3ded1e0de1ca90e54c45af5c739b48e269953439cc15ca9396ab92c5767d52dcd8f8185c76e9c5f0fc173c17bee55b9e0fd73ef56a181e192e5b1a0294672af8df74fcf33d7f2cf54e8aed59deff6b45defff2fe4fd909d70bfe21ea8fe0bf579fe05ff02ff817fc0bfe05ff5522f817fc0bfee37bf17d6ce293e05ff02ff817fc0308fe05ff827fc1bfe03f948dc6bf6b41d8645383c846247deaa1d3447fcc1e4bea4196d257f5800cc763fbebe77bd503aa1f69f2704a254a5542559d5395fe7e74c4aeabc6eaf6e7db6d0aa0260fd5d83d5f6f683be567284d1f48752fa450067da80d22827fc1bfe01f927b16fc3f7ef85f5c5c8463c78ec33ffdd397e1ff147fb7dc7a3b1c3b7a0c4e9f5e287476694e6ec94fb8aa6bdbf1e11fb5f85371dd9afdd7866c443e92320da1a7992a4694adac15e6869d437bb6442b5a83df71dc6f15a8b5231db9395adb16860a95408fc3d61ec78f6c984dd077d6ab23ebcb35a6721878eb9c5e5bc9aa6bdbfff9b1512e5e2d304bf21e5f7a629a195f734bc07271f06365fda3ca63763dc7860951da12a640a91e129c39cb427766aa9671dcd85f3a73cb9c233f33cfae3d1747ecc24a5a859ea9c971d8b173073cf5a957c3377ff3f3e09bbee9d9b06bd70e68b7db82ff9afbf2fe97f77f5dcea7c6eaf6e7db95f77f5a04ff827fffdad7e5ef339c23f817fcf7ab4bf02ff817fc0bfefdebd047c1bfe03fb6a7946f29fbbe5dc17f5a04ff827fffdad7e5ef339c23f817fcf7ab4bf02ff817fc0bfefdebd0c77319ffd482b0ee50eb369cda445582f793c055c14f490aa0b17b314925823fb61989da3421fa0563ca26005482d5bf5f0794ba3dc5eef52375008c9d71ca76b8afbab835c180af73103fc235a91854f95e97a383e497e03f3e26f817fca7740ee247b82615832adf05fff118d6c5b7d3e9c0fdf73f04b7dc721b7cf9cb37c02d37df060f3d7c08666666cd7cbb86c94fa4c76b8b571830a4256dc84c38875bfe99b685963c1452b0b46981482a8c81353e2aab1b49483df141fb96e884e3ae3294ad82e548585d431cd26500c85f65fff0da54ff3224261ccbb9a293d5e10b13bdca768b79cfae703d55e4b2f1a14f0da684945d0fb6e294d9873615b868ccce551c83dc91aec2fd919ffe38c6c6238ef971b3864d6bc6ace5d6d27c9a6bc860593ba35689e84766f7c071caf30eed97e3c5b6f0ded6add370c105e713e9eafaeb9f09575e7979717d8048573c47f02fefff2a7b75fb8dcdaf1b57f2feafc59dbcff05ffb1fd877305ff82ffa63a07f1235c938a4195ef82ff780c05ff719b00827fc1bfe03fb6ff70aee05ff0df54e7207e846b5231a8f25df01f8fa1e03f6e1340f02ff817fcc7f61fce15fc0bfe9bea1cc48f704d2a0655be0bfee3313c5bf1dfd382306628169494b3fe9a707de870dda1faba63bef9f652f39afadd4fc2c76c87739bdcaf4afe2afb4d0f3c0506d615cef1c7abf434bd3f48dc53fe34dd779307413837a6233697a5c979c5e25b15a350772a4feace22b419ea0de7fdffecbd69932d49729e1755f7ce82d9374000c8118895204c1429d26426196526e36f9ae9eff3e72033998902096e2041521c0298ee9e15b371667a2b759eba79cb8fe7eb1e1e91796abbcf0bdca9cc088fd73d9627aa3f1c3be5fdb33ef8877ff887ffe7cefff2cd56bffce52fdb0f7ff8c3f6effedd5fb47ff92fff4dfbb33ffbf3f65ffecbb7db4f7ef2d3f6de7befdd7ee867f1bf1dbc18ddfe5c9f97e693fff5eb3f6dd79a9edfed9fbb7b3d99b5b0bbb55b3f00f4ea434827df5731cdacefed1c6e4edfe474b2b2e36e6ec235bc32eb78caf9ea1b9d3e5c3e08f5e1cded373ed9f825dff281a6571f3e3a7dfbd3fa01ac57e3cfd6bfad1f407bb5ce57af3ef8757a7fb556eb374fbd2ae875acf9f38ceb7c6ed6fa6feefe2ce4d5ab1ada6bdf9bd7316b9deb5aaddf14f6baa6577358f6eaf64f48deadccf5e9035b1fbdae69f973842f5fde7ecbd6275ebe6c5ff9ea97db1ffcc1efb57ff24ffe71fb9ffee11fb73ff9937fd0bef4a52fb64f7ffad3a7b1f0cfefffa8bfb78f51fe5ebc1da7f66ff5f231b63df3a9f6cfac7b544f75de1127fcfe877ff8877ff86f9bdcf00ffff07feea16257c13ffcfbdc3eb6d20ffff00ffff00fffda4bd5393367f8877f1be7bda33e1503fff00ffff0ef73fbd84affeaf7520546ca36442d7c36a1debbf550b95acb37beb2a87e5c45d925141d66b54619a8bd9a33f95a7a71d9bc6c6ce55dd51f5d0419a85e55907d9e2847b6deca579d37f54b2293ca99cd6bedcbc6a9e7688dd57e572e20bf36f00ffff00fffcf95ffe503369ffbdc674fffbefef5bfdbfef93fff3fdb8f7ef4b7eddbdffe6fedcffedf3f6f7ffaa7ff57fb57ffeadfb59ffef467b739d67f8beffaed4beb372ebd583f8cb4fcefe96fe9bdfac6a6db0ffcdcfee9bb8fd7f3a30fcfbed1e9eab6b0dbfd5fbf496ac9717575d6777372bc4d707dfa00d187afe34e1f307af56ffd962afb8d4eebb753ddbcaafbf55aaf6bb44cebf6e361b7df10b57ed3d6abe79b57b1b773b87e5dd332cb8f5eadfff279a6ebeb3bdf75dcb23ecbcfeb979f34dfcef56a0d970fac7db4eeefc76e571fad9bf67abdaf3e9ecf47af9e4f7bbdd47e5a83abd71fa8fae8d53a9cbe6d6ba965ddeb573f4f3ee6c3596bfcddba5fb5f53373376bdfc7ffffc52f7ea1fda37ffc27edfff867ffacfdc3fff94fdaeffeeeef9cda3ef9c94f9e9d2975e6e09fdffffcfebf7be6f73ffcc33ffcc33ffcfb77f8dfe6a808fee1dfaf9bf756e32a827ff8cfdee13f16fcdfad43e51dfeb7392a827ff8f7ebe6bdd5b88ae01ffeb377f88f05ff77eb50797f53f83ffd09c268122a893a106a02be2f83cc6faaf2a8801dd51841e87329a8d43c7a3547e3d4da56e79c1d1ab5e1d12513ed550f1ee5d1cb93edb9526fad545c34e7486aed2aefbdfdcbd642d5ee7d322f15abbc2a67a4ba36f00fff3ec67bc03ffc479ecf8dff45cb9f29fcf9cf7fdefeeabffd4dfbd33ffdbfdbfff32ffe65fbebbffe9bf6f6dbefb65ffce297ed83f7de6b1fdedcfd87f3ebaf743a795cb5f5cfea2d5daf3f80756abbfd70d432e6b6edfab67df966a9ababbb0f455d9dff87ded5add1edbb99c7faefd5a853dff5ab6fb25aff74defae7134fe35e7d80eaf53ab4f6fa9ba6d635f968f5bbb9f55aded66fbf8ad6fdf46cbc4fedb7495efdbcfde0d8abc9be6afbd8f7c5edda7d78fa56ab73d6960f947de4ce965d870d9b6b3d667d6cdf5ae3e9c35e3757af6b7df98997edc58bebf6d9cf7eba7df5ab5f6d7fffefff41fbdffef7ffb5fdd37ffabfb45ffff5afb6cf7ffe73a73f2fb8ae11fc6fbda239a9793c76feb3df39ca279abff5f0f38872abf946e7a87716a3bd5275fa18efd1cbd363c1abb7562a2e9a73a40a27eabdb77ff0bff58ae6a4e601ff2dccade60bfff00fff3a0efee13f12fcc33ffc3759b3f28e72f8b9c23ffc7baf684e6a1ef0dfc2dc6abef00ffff0afe3e01ffe23c13ffc3f2afeed07b0b2c94449ab073f2ab2775128a8b30589c6f6e6d4cb911da4a84fd5e8fdd41c7a79b3c31f1df4e8706731aab6e839823f83289b73e5228df247f1957519199f9d3335d6b7d9fad5d8685ed11add147e5945fd515df00fffb6b6e819fe634ff8cfeb7faafc2f1fc67af7ddefb5bffccbffdcfefc5ffdebf6e7fffa2fdab7ffebb7db8f7ef4e3f6f39ffdecf40d50a70ff62c5eaf3e80b5beaf5abeb5eae6a30f4edf52b5f87ee8be01ebc6d6b334df2de262768a59db4fdf70b57cbbd4e2f1e2f64fe59dc62e5ee65bae5efb2d3fcdb7402dedabc7e95bb3d6f6db424fb1278f57e3af5ffd79c3a5ed23f3e711edaa5fb57696d32cf4ed5a7cd45eadcbd55d1d66f0f2e1b4752fae5ef99dbe61ebd5377a9dd6e7d59f1f5ceb5ae7db5ef59dc6ae7f8ad17e9bd8fae1b093f7f5e9fdd77eed53ed6b5ffd4afbe33ffec3f6877ff47bed1ffda37fd87eeff7fe5efbcddffc8df699cf7cc64d01fedf64fe55adbd1abd9f9a432fafcaa1d6d6cfddc7794567cdd6163df3fb3ff684ffbc7ef8877ff8df8e857ff8877f2df8877f556f34b637a75e0ef8877ff8877ff8877f3f16fee15ff5c13ffcdbb1becdd6afc6c2fff3e77ff301acca6265e63e81f78a269a1da61ecc55602b974874b1791fef593930d96514cdb157473487de859b5ddcd9fc945f06536f5ebdbc59add173f5c2ebad41e45381dff7abf7a8b691b3d27bf6f38ac647827fede33de11ffebd27fc6fbdec1c9e1bffbffce52fdbf7bef7fdd33762fddb7ff3efdb5ffcc55fb6fff81ffff3a9edfbdfff61fbd5afde7bf521a565fccde9e747edc3bb6fa3babefb70d6d2f662f93094c9ebe7dbd6b657f55c99e7534dafde4fb1afbef96af9d6aa0fedb7605d5d9dcd67fd50959febeb1a3efeb77cf3d3fa27005f7f58cb78ad7f4671cd71f230becb07bf6ed68f53dd2ec859de8f3eba69b74396da3f7afd41b0756eeb376f2defeb7cd6f653fe9b9bf4fcacebf6b9cf7eb67de9cb5f6ebffd77fe87f6877ff007ed0ffef077db9ffcc91fb7af7ffdb7dad7bef6b5d39f15847ff8573ede7373c61abfff2bf3eae5cd6a8d9eb37a2bbe3d1ff8877fef09fff0ef3de17feb65e700ffed2c56cd11fee11ffee1bf3a1ff8877ff8cfe791e5877ff887ff7c4d7a7382ff788efed9c6aa39c23ffcc3fff3e5fff401ac6c62d1045491bd98ec804787c6fb47796d7bb4999505aec657163f8af7fd231746b666d97cb203e515ede1c85cbd9fafa572e6fc9848d95eccccbb7299781f75517acf6c4db37ad5fc6c6e9fcbfb8c5c3af00ffff00fffca03fe5b38173b6ef9f9fefb1f9cfe2ce18f7ef4b7edfbdfff7efbce77de69fffedffe87f61ffef23fb5fffc9ffe4bfbf18f7fda7ef5ab5fb55ffcf257ed347c19bbf8996f6c5a3edc749b7bfd938437a7bf92b7fcd9beabebdbd8e55bb64e63d78f34bdfa56a7e54348a76fd56aedf6c34ab74ea771cb279a961a5fcfe1b6e736c7ed4b5b3ef8749ad7abd8e5c350a76ff4ba6db8edbbbafb1384d7af7c3e5abf99ea6ea1d6875b6333c7e5f9f61bb7dc87ac6e6e3f7cb57aaf35dedc1670ca759acd8b8ff37cb07cdbd7d52b8f9b577f6ef1c5c7b57eb4fc55c393cff2ef139ff844fbf4af7dba7de1f39f6f7ff887bfd7fee88ffee0f46705ffeed7ff4efbaddffccdf6852f7ebe7dfad39f6a2f3e5eb36bfb4138f89775aa5ae19fdfffca93dffff0afe601ffe77d951ae0bf85fdf00fff91979a7f24f887ff91dc766ce609fff00ffff0bfb6fbda6d7b6f7e36b7cfe57de01ffee17f3bc60bfee1bf1a0ffff0eff3c03ffcab79fb78db9f7e03965ab09189540af031dedfbef7e0ca36b602640ffe68b17dcd55587a75475ed5395773fb7675a0d55815abea1a1d9bc9cf651d1fb5fb3c11d8d11e47e7c8e7ae5e2cd13c7a6b9d5d50d10554b98c7b1711fcc33ffcc33ffc1fc3fff2a1a21ffce007eddbfff5afdbfff75fff5bfb17ffe2cfdadffccddba76fcdfac1e95bb27ed93ef8e083d3b7407db07c00ebe473fbe7045f7f86e9e3ff7bf1f2ba7df8c147ede6eae6f481ac8f3efce0f61bae5edc7eebd4eb0f422de3963a4e3ed7afbe5d6a69bfed5fda3f7a55ebd5ab0f682d635ea53a9be7ebb5fce8c353dcfa61af9bf51bb54e311fddd663f7c2f92c79977fef7f3ccfe5c354a76fb4ba79f581b057df9a75fa66ab0f3ffef9e265bbfe78ae1f4ff3f46e6b5c7e9e624e09da69eccb972fdaa73e75fb41aadff88dafb6dffe3bbfddfedeef7cbdfd833ff9a3f6fbbffff7daeffeeeefb4af7ce52b294bf0df363ef0cfef7f3f56c5aaba46c766f27359c747ed3e8f5a4bf8d7827ff88f62fc78f887ff4cf00ffff01fcf03fee11ffefb827f5db3f7807ff8afe6f6edf00fffde07fee1df8f55b1aaaed1b199e05fd7ec3de0ff69f07ff59def7ce7265a5c35209ab08f891670e492a96c48ef02c9f2aa7195d82867e6937955e7e7d5cbd75a0ba1e9ed43065bb43e57c965350a722f7f564ff59c65175776695955d6cac62af538ca2e2b3fafca5af5c6c13ffcc33ffcfb798ed61de581ffbbb13ffef14fdacf7ef6b3f6b77ffb93f6cedbefb46f7ffbafdb5ffff55fb5bffaabefb4ef7def07edbffff75f7cdcf7b7ede73ffb45fbe0830f4f1f725a3ea0b5e8f4a7043f6e583e74f5e147e71f663a7d006acdb9d470fa937d2f4ee34edf10b57c88eaf6d35cadbdfa93886b4da7da979657df9c75ebb17cf3d48be5e563efdbfceb9f345cfad73f4178faf72ae7e26abfc5eaf69bb86eda8b172f4f63d66fe6527ff670f900d532cfabeb97afbf5debea55ecd2b7e8139f78d13ef7f9cfb4cf7fee0bed739ffb6cfbf55fff6afbfafff85bedeb5fff9df65bbff91bed777fef77da17bff885f6f9cf7fbefddaaf7d1afe8be3e09fdfffbd339d9defea9ca2b393d5edfb543dd57306fff0efc7c13ffcc33ffcc33ffc67b5f879f56a5573b4fef00fffbd762ff8877f3f2ff8877f2bf8877ff59e8d837ff8877ff8f7f31cad3bca03ff77634f1fc08a12540fb57a5709550e35a602bd7af675641745565be41bd5d93bb459ee0ce0c833db1fdbeeeb9e5dc3a8769f2b537611a839fa98deda677b518df1f556f6dfd769f3f57247eb6f73441e51add979f4b5f5fae11ffee1bfff0efff07f24ffcbbfe5cf17fef8c73f3e7d00ebfbdfff417be79defb6efbefbbdf6c31ffdb07df7bb3f68efbefbddf6931fffecf4e1ad9f7f1cf3938f637ff5de7b6b71a70f60bdf65cbe49eb6af9e6a8e59bb45a7b717ddd3efcb86df926acd6c47f145edd7e20eae6d59f1b5ce7b87e50eaa39bdb6fe65a3cdba9ef639fabf63af6e4b37e93d5f5ead55ec55e9dc6d93f43b8f45fbffa30d7871fdc7d93d7273ff189f6f9cf7fa17df6b39f39fde9c02f7ee90ba70f597dedd7bfd2befad5afb6dffeaddf6cffc36ffe46fbd297bed83ef7b9cf9d3e6cb57cd04aed03fc6f6bebf5c33fbfff7beba6cead52745e94a275f373ca7e7a3ff8dfd6d6eb877ff887fffe3bfcc33ffce7827ff88f6a807ff8579ef00ffff0bff5847ff88f6a857ff8877ff887ffa7c1ffcbacd02cd1faace2d7fe6c737ae046f05b5f55a7d2c846a885ccd6c7d79cf9d876bb7efe39f3ec5d64590d7e8cedb7b9d5defa3efb9c9d0f3bded71f5d2691d43e4420fbbcbe7e05b9f5cbe666c7dc24975354839f93f5c9e694d51df547e74d8d6b0dfee11ffe7b73b363e01ffead5fe663db33fe977fcb878e3ef3995f3b3dfffeefffeee9dba2960f4d2d31efbfff41fbc52f7ed1de7befbdf6d39ffeece39fef9fbe49ebfbdffffee9dbb27ef4a31fb79ffee4e7ed873ffc61fbc94f7fda7efcf1fb7befbfdf7ef5abf7db07ef7f787a7effe37f1f7ef4e1e9034fcb376b2d7fca6ff9f3818bfff27cfa80d6e91bb2b6fbd85efdc9c1f54356eb9f1d5cf4e2e58bf6f2e5274eb1cb9f033c7d63d5cb97ede58ba5fd65bb7e71d53ef9c94fb74f7de2e3b68fff7dfad39f6e5ffef297da97befcc5f6a52f7eb17dfe0b9f6d5ff9ca97db97bff4e5f6b55fffda690d960f557de6339ff938f653a7e7e51bb0d66fc25ad72b5a7bf887ffa7c67fe6e9d7529db5a886ec77a762449dc32b7eff9f8d817ff8b77e998f6d877ff8f7f380ff76d60ffff0effbe01ffee11ffee11ffea339647df00ffff00fff2a87efef8d5735673eb61dfee1dfcf03fedb59ff9bc07ff91bb07c92e8f06607bb7a68d5c26490a8fa33c0a3dcca4ff956e7a36aa81c14554376206f924b28aa399b439433ca5dd9575597af393a376abceaab9ebb68bed9fca2385f8b9a5376c67b7b7353bcf495bff2c9b8b3fdde43f9daf72897ef8bea877ff8877ff8f735f8bc3e37fc6fe3970f50b576fb1fc7cb07b7de7bef83f6ab5ffda2fdfce7bf68efbff77efbe5af7e79faf0d6af7ef5dee9c35ccb07b0de7fffbd571fc4fab07db4fcbb59c6af6be3d6f854ebc73faf97dc1fbdaae7fa54cbfac1a8e5dff2a1abe5c357cb9f0afce4273ff9f1cf4fb44f7dea13edd39ffeb5d3f3f2e1aae503589ffce4276efface0ab34cb87b8fc9ce11ffead5716f726f31fcd57e5eef130c247b60e515dbee6e8dca8f1aa0ffee11ffee11ffee11ffe5b5867d607fff00fff5bc17f0b73c33ffc47b97c5f543ffcc33ffcc3bfafc1e7f5b9e11ffee1bf497fe503ff8f8bff97ca7c6d53c545ef768c9d88ed5793b379b3cbc4e6cd0ea6f7559be0f347b2be763e6b0d7ebccd97d56e7d7af5f42e6b9f3b3b90958b29bb847d5fb6b70a62558b3a27eaddb7456be9eb8d80513565b9ada2f36dfbd485179d279b37ba40d631ea42518aea5a9f6d0cfc6bc13ffc2bc13ffc3f56fe17bd78717dd6be7cf8e9f39fff6cfbdad7f4f8e8fc67e7e4a6784744fcfb3a945f7627c07f4b6b857f7efff76aeaf1e3df7d5fb6b7eaac452c64e3a336f86f69adf00fffbd9ae01ffe7d8d365ec5c03ffcab5ae1ffbc0ffee13faaddfaf4ea817fdd06ff2dad15fee1bf5713fcc3bfafd1c6ab18f8877f552bfc9ff7c13ffc47b55b9f97f6c5175e29c8f7a931bd45f0e3d5425a29804672d958ff6cfbfdb382d8e7521be36b8f0e51efb28df6285a03bf4f514d237b93e5b5392a30f97ad7be6c5f7be733ba4cd51c7b9799ba94bc5fd4a6f244975c265593af47ada19a9b8fb36704fee13f12fc9ffbdbb1f07fdea7c6c0ff79edf00fff2a4e09fee11ffee15fbdc33ffc477381ffb6a947c55bc13ffcdb7eff0cfff00ffff0bf3efb381fab04fff0efdb33c13ffcc33ffcab77f887ff682ef0df36f5a8782bf8877fdbef9f2fc1ffe94f10fac9f70e6694a8b2e9b31af1880ee55a637458ab319579ee391cb616abe8c23b628d472e2e9557c567eb16ad4f33738d6a8bf2677b64c7f97cd9fa55d7a12573cc543d1fd13eabf39bf9f56a847ff85fdf6d9e281efee1dfb7f56a9e11fcc37f2f3ffcc37f9417fefbf9e01ffee11ffed7779b278a877ff8f76dbd9a6704fff0dfcb0ffff01fe585ff7e3ef8877ff887fff5dde689e2e11ffe7d5bafe619c13ffcf7f2c33ffc47791f2bffd7d9a1b383fdbfec60aa436e7faa0d55714ad9583b71fbee6bcd2e3255b3bf1cbcb7f5b55e2373f0ed6b2dd173af3695c7af939a837ab7ed767e36bfad3103d68ef7e3d4fc54bdd62f5b7b7596543ebf2e3eb79a57b4e67e5cefacfbbd8de2fd2f1bc59fe5d3f7791ffb9ed5e77de1bf6dbce11ffe95e01ffeab73f0edf00fff361efe5bd80efff00ffff0afe604fffa1dfedb59fd360ffcc37f340f9f17fee13f12fcc37f750ebe1dfee1dfc6c37f0bdbe11ffee11ffed59ce05fbfc37f3babdfe6817ff88fe6e1f3eee5ffeaedb7df4e4f5204939a68f41e6d7494cffbabf151db5adf685bcf47d5a76246d645bd47cad620ab293a687bb4a7e6a8bfb79fea5c44792af5cd9cc9191f3fc7551556b2395bbf91b9f73ca31c56f00ffff05ff3817ff887ff7e3ffcc37f541ffcc33ffcc3bf8af36d6b7da36d3d1f559f8a81ff7a1cfceb18f887ff286f540bfcc3ff483df00fffb382ffba0ffcc33ffcf7fbe11ffea3fae01ffee11ffe559c6f5beb1b6debf9a8fa54cc28efd751f2554bb0ffa7e4dbfda15b7eaacd8c64fd9497aa23aacf8e5fc72a28221f5fbbf78872d9f79e6c0eb556998f3db0514dd981f13fa37dca0e735697ca93d560c7572e0b75f67a67ad774965b1ea1c46ebe6d73e3b333e476feeca6ff6528dfae01ffe7d2cfcc3bfaa45bdc33ffcab3c590d763cfcc33ffcc33ffceb39c0ffb6d6c80bfee13f1aa3ea8962e15fd7a2dee11ffe559eac063b1efee11ffee11ffef51ce07f5b6be405fff01f8d51f544b1f0af6b51eff00fff2a4f56831d0ffff0ff9cf9df7c03961fe80f992f4c1d4e15d383a81a178db53567fe2a7ef4b2f47eca3ff252f3acaecd88a231d95ca39a2af5ad9e953351f18c6a9b598b9e67f6acde7b5eaa6f9182fec87d3de20cc17f3d27fc6f3de15ff72d827ff887ff56f28c6a837ff857636dcd99bf8a87ffed18f8877f3f0efeb59ff2877fed9bb56582ff3c0ffcc37f650cfc6f3db367f5def3527d8be01ffee1bf953ca3dae01ffed5585b73e6afe2e17f3b06fee1df8f837feda7fce15ffb666d99e03fcf03ff4f93ff6bdbb116b106a83edfa6e2fc01b3ef515151bcf2f4b235abb16a1e7eb1b2cb4ae5ce36abda176d7c96376bb33fa31aa2b96635550ef61a63f7cf5fccd1781beffff91a2ab58fca8fcbc08cf6268ab1ebe3e7e3c7f4f6bdc7825fb71e8711e3f0dfcf0dffed2c17fcc33ffc8ff5c13ffcdb31f00fffbdbcf00ffff07f2ef8877f9b6f54f00ffff01fd704fff0bff6a939666df01ff7c13ffcdb31f00fffbdbcf00ffff07f2ef8877f9b6f54f00fff0fc5ffd577bef39d9bb5c382ab20cec0aea8e7399a33bac06cdf2abbb11960be3dcbe16bb43f7d9e687d7d9badd7c6a97cbd36df6f15cdd9cebb52b31f9fd59dd596f98eae7d5443efbc44b1d90564dba35a7b67289aff488e11f973bafac3ffd617fee11ffe5bd83693376a837ff8877ff8ef8db17d6bed51df2af8877ff8dfe6b3fef0bff5857ff887ff16b6cde48ddae01ffee11ffe7b636cdf5a7bd4b70afee11ffeb7f9ac3ffc6f7de11ffee1bf856d3379a336f8877ff887ffde18dbb7d61ef5ad7aacfc5f2fffa3363f3a08ebb39fa06af3e37a9e6b1deb3fdb16e55097d7da6efff536d2e75535ab71aa169b33f28a0e5a7638bc9ffd67f3a9bdb1decacbaf951d67d725db475fb71de3d736aa7356d119f667c9e6eead977df65e510dcadfee69e52caf7d764f548c925f735f8fba24e01ffee17feb679fe17feb1fe58ec6c13ffcabd82304ffe79ef00fffbe36f8877ff86fb26ef8877fef679fe17feb1fe58ec6c13ffcabd82304ffe79ef00fffbe36f8877ff86fb26ef8877fef679fe17feb1fe58ec6c13ffcabd82304ffe79ef0ff34f8bf7afbedb76fbc99dfa04c7643ed626490668ba6fca3fe0c1c558fcaa76aadbef7e61ce5efade93ac6d798ad45e4d11bdfabb3b28e3d3f5553f6ae3c7d9bf7547dd97ca3b589e222cf688e2a26faa9d662a6be68ff7a6b10d56cc744827ff8eff9a99ab277e5e9dbbca7ea837ff8f735fa58f8877ff8877f3f9fca3bfcb7929faa297b579ebecd7baa3ef8877f5fa38f857ff8877ff8f7f3a9bcc37f2bf9a99ab277e5e9dbbca7ea837ff8f735fa58f8877ff8877f3f9fca3bfcb7929faa297b579ebecd7baa3ef8877f5fa38f857ff8877ff8f7f3a9bc1fc1ffeb0f60f989659b1ec5afcf7e72d146fb62ac5fd4a7d483be3acee6cc2e8fd18bd08e5fc7f420bce95c5e233067f5f460ebc56479b2cba9772165738dda7a1a59a30c56b516d1d9c82e4e1b17d533b2e7d11c2a1786ca09fff00fff4dd6bbb6792ff887ff2cdf3a06fee1dfc645f5c07f5ff00fff15c13ffc67e36c4ef8afc5c07fdbd4e3c7d858f8877ff8877ff83f9f7b56774ff00fff15c13ffc67e36c4ef8afc5c07fdbd4e3c7d858f8877ff8877ff83f9f7b56774f4f99ffd307b0b2455183a2898c4edee7cb2e131fdbf39db994b20b40e5ae5e7295b195b55d55ada3da3e72e0a33aaa9747e6397b915672f52e771f5ba9a7f78b6bb6ef88351bb994e07fdb0ffff97c7c3cfc1fd707fff0efdbe15ffb5673c13ffcc37f9c5bf92ac17f2d0efe73c13ffccfe49fcd05fff00fff716ee5ab04ffb538f8cf05fff03f937f3617fcc33ffcc7b995af12fcd7e2e03f17fcc3ff4cfed95cf00fff8f91ff6bdbb834a882a3c978737f51f822fcb32f2cdac4b506bf20be0ee5696332187cbf7ab6b111b83626ab47cd51e5b0f1762fb23aac67b6f6362e5a1bbfbed15ef40e62045496dfd7e27f8e8019edadf5f4f5f4ce72c44336c68fcb3cd5795735466b128d5567d47ada38f8877ff8d7b1f00fff2a8f8d8dea81ff7e7e5f8bff09fff00ffff0effd7bf9e11ffe7d3df00fffbe1ef8877ffb0cfff00fff6d930bfeb7b5c0ff36475407fcc3bf1d0ffff0af6af13fe11ffee11ffebd7f2f3ffcc3bfafe7a1f8dffc0942b5d0d1c28c2a5b846c716c7e1beb0f40f532a8d4e8f3ec5983deb8918ba45243259f9fdb8ccfa8a2b3d58baf9e8ff579b69e28c6fbfaba46f7243ac3a37c8ca8ea01fff05ff51915fcc33ffcebfe55f0bf8d877ff887ff9a57351ffcc37f250efee1ff28c13ffcc3bfee5f05ffdb78f8877ff8af7955f3c13ffc57e2e01ffe8f12fcc33ffcebfe55f0bf8d877ff87faefcbffe0056154a657e329a84a05a6816bf6781d41835a755a3f555c7540fe75197ab1f13d539922f8b89fc67fc2ea53de777969fc8e388b1a31cc3ff5ddb22f887ff91b1f00fff91cf4c1daa1dfe2f27f887ffb56d11fcc3ffc858f887ffc867a60ed50eff9713fcc3ffdab608fee17f642cfcc37fe43353876a87ffcb09fee17f6d5b04fff03f3216fee13ff299a943b5c3ffe504fff0bfb62dba0ffeaf7d90df888ad6f8e5a71d6bc7afef91a75d44db56cd6b7f2a6ff5ecc7d8b5b0edebfbc865e6fdd7f6684eaa0e7538b2bdb16b387218b375cbf6a49aa3ba766bacaac3fef4cf47c9d7a9ce6b65ff46f9511e7bc6dab6ccc7ce0ffee13ff3b36dbebfe20fffb5fcbd3c95b1b60dfee1bf27f8bfcb0dfff00fff5b3fdbe6fb2bfef05fcbdfcb53196bdbe01ffe7b82ffbbdcf00ffff0bff5b36dbebfe20fffb5fcbd3c95b1b60dfee1bf27f8bfcb0dfff00fff5b3fdbe6fb2bfef05fcbdfcb53196bdbe01ffe7b82ffbbdcf00ffff7c9ff7504f8faac8ab76dd166a949787f95b7927f54d1a5e4e753bd24bca2f5c8e65f013bda3c7558d5b8ccdbf747007bbfd935f2f946b4ceb557476f4da35f5a59de8c8f9e7ab1bddcbdf8ec42cb7c6c1ffcc33ffcc779e11ffe7b82ff31c17f2d772f1efe6b827ff857ef519bcd01fff0df13fc8f09fe6bb97bf1f05f13fcc3bf7a8fda6c0ef887ff9ee07f4cf05fcbdd8b87ff9ae01ffed57bd46673c03ffcf704ff6382ff5aee5e3cfcd7f498f9bfee99562f91ec42f0e3fdd8ea415479d5a1f05a17233a6c239799f78d627a9be9e712c5db8d8c7eaaf5c80e72548bca9d69663fa3cbbe37369b4375efd42fa659e8bc8f7deecd7fd473959acfecde55eb847ff8af8c83ffbb67f8d782ff6d3ffc6ffba35cf00fffd158f86fa11ffceb77f887ff5eedf0df4ae3e0ffee19feb5e07fdb0fffdbfe2817fcc37f3416fe5be807fffa1dfee1bf573bfcb7d238f8bf7b867f2df8dff6c3ffb63fca05fff01f8d7daafc5fbdfdf6db375982230ee8a53d9f53feac7fcfd811ad3ed14f159b3dcfe48efa16ddf7fe8dcce5e83db8af7197f6baa4e773ca0ffff0bfc707fe9f767ef887ff3d3ef0ffb4f3c33ffceff181ffa79d1ffee17f8f0ffc3fedfcf00fff7b7ce0ff69e7877ff8dfe303ff4f3b3ffcc3ff1e1ff87fdaf9e11ffef7f8c0ffd3ca7fbd1e6aa51eecd58451f2a3d4abc5f68fe4f7becb7b75decaa73236abcf5e6ea3637b529eab9fffa962b2677f0632a971f6e28d7cad66f62852e5979e958a55e7c8b7abb946e33355f2473554bc32cf5e1cfcc33ffcc3ffd182fffed89ee0bf855ef0df4a9ebd38f8877ff887ffa305fffdb13dc17f0bbde0bf953c7b71f00ffff00fff470bfefb637b82ff167ac17f2b79f6e2e01ffee11ffe8f16fcf7c7f604ff2df482ff56f2ecc5c1ff76ec756fc3a2cd8bc64507721d33b3713d550e4ae657cde52f808a875dabea05e2fb2b90f7d4bbacfce559fda531da5fa9ddd7148dc9daa3339c5d46f6ddd660d7a2379f5e4c7419547fc929f5e6a7d623cb07ff711ef8aff946fdf00fff15c13ffcc37f0bdb7c4ef56cc7c33ffc67edf07ffe0effb11ffcd73de01ffee11ffed578f86fa59ae01ffe33c17f5ff0bf7d8edae01ffea33cf05ff38dfae11ffe2b827ff887ff16b6f99cead98e87ffc7c9ff752b24cde40f54b431d186abc33dba5999d69aa2bc6bcc88a2431ee5af5e662ac61f767fc8470e8fca3572b9a9bcbdb5cb0e6de4dfcbddf3b0f3f273ac5c70d1bb9d8b5f8bea7a1ca1ec82f7f545bf60ab82ffade01ffee17f5b0bfcebfcf00fff470afeeb82ffbb3a542ef887ff4cf0bf15fcc33ffc6f6b817f9d1ffee1ff48c17f5df07f5787ca05fff09f09feb7827ff887ff6d2df0aff3c33ffc1f29f8afebb1f37ff5f6db6fdf78f375d0250ed3257c33e02af92e35d72c4f2f67d63f5aaf5a9355593dd53c7be63212332abfde8b66e71bf5ed59b791787f29cdee4b75afe0ff78c1ffbcc78ce01ffee11ffed7e745f00fffd9f84b0afee73d6604fff00ffff0bf3e2f827ff8cfc65f52f03fef3123f8877ff887fff57911fcc37f36fe9282ff798f19c13ffcc33ffcafcf8be01ffeb3f19794aa73f301acfbd27d4dfa085db2d611b055df43afe31117d0ec98a7a4cac5f0dcd7c00afe736ff87f5e82ff73c17fee0dffcf4bf07f2ef8cfbde1ff7909fecf05ffb937fc3f2fc1ffb9e03ff786ffe725f83f17fce7def0ffbc04ffe782ffdc1bfe9f97e0ff5cf09f7bc3fff312fc9f0bfe6f751d255c3f6957d568bc3a7c3dcfd11c91f6d67aa422ef6c7d6cdf9edaaaeb701feb95fdb2b90f5d3a8f9fdfd5ab4f9946fbfa50677d1d03ff77827ff8df2bf83f17fce7def03f977b56f07f3e06feef04fff0bf57f07f2ef8cfbde17f2ef7ace0ff7c0cfcdf09fee17fafe0ff5cf09f7bc3ff5cee59c1fff918f8bf13fcc3ff5ec1ffb9e03ff786ffb9dcb382fff331f07fa78bf2ff10df80b52cc05193aa781d996f36cf7dd5f0507aeef343c709fe9f9fe01f5505ffcf4ff08faa82ffe727f84755c1fff313fca3aae0fff909fe5155f0fffc04ffa82af87f7e827f5415fc3f3fc13faa0afeef74dd2e5448a6b590caa7de7aaa7c6af0c8cdaed612f58dce6f46f791c3eb3e3e897b2965f92f59dbd1de0fbd8eabe03fee83ffade0ff71facd0afee33ef8df0afe1fa7dface03fee83ffade0ff71facd0afee33ef8df0afe1fa7dface03fee83ffade0ff71facd0afee33ef8df0afe1fa7dface03fee83ffade0ff71facd0afee33ef8df0afe1fa7dface03feef31e0ff20d5808218410420821841042082184104208218410420821f41c74916fc07a0a1afda4ddde4f173e964f27a29ad8afe72df84799d8afe72df84799d8afe72df84799d8afe72df84799d8afe72df84799d8afe72df84799d8afe72df84799d8afe72df84799d8afe7adc7c2ffa3fa00d6515f1576895c7b6a5bc6dee7dc1e93d4bcab6b718935bbcfdc6fea9ecf0afe9f9fe01f5505ffcf4ff08faa82ffe727f84755c1fff313fca3aae0fff909fe5155f0fffc04ffa82af87f7e827f5415fc3f3fc13faaea4de49f3f418810420821841042082184104208218410420821841042937a367f8270e6136acfed138a8f6d3e7beb79ee9f20e513b2c709fee1ffa909fe8f13fcc3ff5313fc1f27f887ffa726f83f4ef00fff4f4df07f9ce01ffe9f9ae0ff38c13ffc3f35c1ff71827ff87f6a82ffe304fff0ffd404ffc7e9a9f27ff807b046bf6eaf175bfd0abbec6f344635d93197fcbabbea1cf66af4ef54aa7539b2ae6a3d0f09c243e67e8e7f6716fecff3da9faaef48c1ffb8e0ff58c1ff795efb53f51d29f81f17fc1f2bf83fcf6b7faabe2305ffe382ff6305ffe779ed4fd577a4e07f5cf07face0ff3caffda9fa8e14fc8f0bfe8f15fc9fe7b53f55df9182ff71c1ffb182fff3bcf6a7ea3b52f03f2ef83f56f07f9ed7fe547d470afec705ffc70afecff3da9faaaffb01acde82fabe65522307abb770471cd24a4d51ff9ec3b47a2aefd979cd1c143bc63eab75b9f4a530028f6f1f85bba2fb9a6fb5eed1f84b0bfee1ff48c1ffb1f19716fcc3ff9182ff63e32f2df887ff2305ffc7c65f5af00fff470afe8f8dbfb4e01ffe8f14fc1f1b7f69c13ffc1f29f83f36fed2827ff83f52f07f6cfca505fff07fa4e0ffd8f84b0bfe2fcbff752f4154fc3aeea803ac36ba0a66c57b24afcf55dd844bc33cab913958d9cbe088cb6f2f50bdb3a8daa35f3e1555e2b3dc8b3208957cfca52f62f887ff48f00ffff00ffff03f1e03ffe375c0ff6504ff795c24f8877ff887ff9918f81faf03fe2f23f8cfe322c13ffcc33ffccfc4c0ff781df07f19c17f1e1709fee11ffee17f2606fec7eb7813f99ffe1384fe80ec3ddcd1461fa1ccb79233da04b579519c7a9f816bf402f2e3fcf8dee15b7fce1c7ab5c7d9facc5ef291afaf7b64ee2a5e8d55b9f7fea219a9e112bf542a827ff8cf04fff03f5207fcd704ffba4ef8af09feeb7d3606fee15f8d857ff8f76d2a4ebdc3ff5d6c34a622f8af0bfeeb7d3606fee15f8d857ff8f76d2a4ebdc3ff5d6c34a622f8af0bfeeb7d3606fee15f8d857ff8f76d2a4ebdc3ff5d6c34a622f8af0bfeeb7d36e652fcbff8c637bef1ad26942da8edabc655e2673c7d2d516d95c33a7b38ed82ae9b65ffcd5c5e7b741464f7916f647dee735e47e7aa805a1d7b54ecac0ffceb71f03f3716fec7c61e153beb03ff7a1cfccf8d85ffb1b147c5cefac0bf1e07ff7363e17f6cec51b1b33ef0afc7c1ffdc58f81f1b7b54ecac0ffceb71f03f3716fec7c61e153beb03ff7a1cfccf8d85ffb1b147c5cefac0bf1e07ff7363e17f6cec51b1b33ef0afc7c1ffdc58f81f1b7b54ecac0ffceb7133fcbff8e637bff92d6f349a7464cc1ed8ec04673d66da1f8b46d7fae8c35ef1b17947cec8de4ba69a2bdb7b0fd21ecdcef93ef76ccf58f8bf7fc17f1c0bfff5f1478c85fffb17fcc7b1f05f1f7fc458f8bf7fc17f1c0bfff5f1478c85fffb17fcc7b1f05f1f7fc458f8bf7fc17f1c0bfff5f1478c85fffb17fcc7b1f05f1f7fc458f8bf7fc17f1c0bfff5f1478c85fffb17fcc7b1f05f1f7fc458f83f46671fc09a912dfab15f60914636e488391eb54ede67e602892eea232ea1d547d5578957ed515d47ef4bb4b6b39e973a370fcd1cfc1f976faf0ffcef13fc8f0bfe8fcbb7d707fef709fec705ffc7e5dbeb03fffb04ffe382ffe3f2edf581ff7d82ff71c1ff71f9f6fac0ff3ec1ffb8e0ffb87c7b7de07f9fe07f5cf07f5cbebd3ef0bf4ff03f2ef83f2edf5e1ff8df27f81f17fcefcb273f80359364056374ece8248f38d0471f94bd7e7b377ae400ed89ab7a1c7106a2b8cadf65adb6cfe6c9c65d52d12fcb4be4991903ffc78d877f1d07fff0df8b99c9b947f00fffebb84b0afee1bf3a7ec403fec7f264e32e29f887ffeaf8110ff81fcb938dbba4e01ffeabe3473ce07f2c4f36ee92827ff8af8e1ff180ffb13cd9b84b0afee1bf3a7ec403fec7f264e32e29f887ffeaf8110ff81fcb938dbba4e0fff9f1bff900963f7c15f391317b2f1b9b2ff38a0e6b06d70cc0d981e8cdabbab647a97749ae6b56b94c8ffaa5710450def7888beaa8f1d9faf5ce6a34be1a3bcae1da0effb5769f6f645ca5bf1a5315fc8ff91c311efeb7b195b64a3ef81f13fc8ff91c311efeb7b195b64a3ef81f13fc8ff91c311efeb7b195b64a3ef81f13fc8ff91c311efeb7b195b64a3ef81f13fc8ff91c311efeb7b195b64a3ef81f13fc8ff91c311efeb7b195b64a3ef81f13fc8ff91c311efeb7b195b64a3ef81f13fc8ff91c311efeb7b195b64abee7c0ffe9035897be0866c0ecc5f84d98ad6fcf819839ac97507661562ed34bd461db2a17ccc8d70a567f4954fb1f8b8efea551bd84e0bfdf168d857f9dcbb6c17f4df00fff33827ff857e3e17f2e1ffcefafc3b6c17f4df00fff33827ff857e3e17f2e1ffcefafc3b6c17f4df00fff33827ff857e3e17f2e1ffcefafc3b6c17f4df00fff33827ff857e3e17f2edf7df2ffe2adb7defad66a52994c065355f7792964b97bed47d4d6bb1c7b1754cf336bf3fdd127ff46b5f70c8ceebfcd975dd8239e959cea7df6ec467eaaafe2515d37bf4eead2807ff81f11fceb98193fd557f180fffdb97bedf01f7bc13ffccf0afeefdee11ffee1fffe04fffb05ff3a66c64ff5553ce07f7fee5e3bfcc75ef00fffb382ffbb77f8877ff8bf3fc1ff7ec1bf8e99f1537d150ff8df9fbbd70effb117fcc3fface0ffeefdc537bef18d6fb5019323632a97c6487b14d75b845e4ccf37f3ec5d14eba7f8463e013a529bef530055c0ebe5db336624bf5db3f57d049edee552d1513c5c42ea9755d43fe2798918f887ffd1fcf0dfcf0bffe771f07fd707fff00fff63313d5ff8df3706fe8f13fc6fe3e0ffae0ffee11ffec7627abef0bf6f0cfc1f27f8dfc6c1ff5d1ffcc33ffc8fc5f47ce17fdf18f83f4ef0bf8d83ffbb3ef8877ff81f8be9f9eee1ffea9d77de398bb087bbf289490f44e5c2599f6dbee8937a7e8c1aafde7bf947e5ebf15ed925eae356f975ebcdaf577bb48e5ebd3ab3b554fba17efaf1d5f5516d3367ab72aeacb25f80bd5f9223676a743fa2f3ef63accf28bfbe16f88fc7c23ffc47792b827ff85f05fff09fe5867ff8877ff8877ff8877ff8877ff8afd4e8dbbce01ffea37ae03f1f0bfff01fe5ad08fee17f15fcc37f961bfee11ffee11ffe9f2fffd7cbffd87f3df8d67ebf802a4ec55a658b617d2b8bbbd66f6b1891cde5f3668b6ef3f7bc6d9c8d8fda548cafad27eb6d3d7be3b3b5b46be473ac6be4f72fbbe0fc7951077b041c5f8ff7b771d13ca3bcd18595ada79a9baad1fbda9f6adffddeaaf1513deb4ff83f8f87fff3712a16fecf3de01ffee11ffe23c13ffcfb38f887ff6c3cfcc33ffcc33ffcc3bfed83ffbee01ffee17feb0dfff0efbde0ff3c1efecfc7a958f83ff7807ff8877ff88f04fff0efe3d69fd73e2032536045a6b6dd1f6aa5dec2ae63fd4432a87b5e3e3e9a43d6d63ba43636ba8caa17abaad31f9668bd7b97841dab2ecbe81743efd0550ea5f7cbce62ef3255fe91fc7cd5bef8f3d65b639fd7fa67f565e7727d8ece9bcda3daa27aa25a6c3d3e06fe5b293ffcc3bfad17fe759e2c9fef837ff8877ff8b7edf00fff2acefa4782ff2673d97a7c0cfcb7527ef8877f5b2ffceb3c593edff7a6f01fe9a9f1dfebdfc3bf1d5fc915d5e6e73bcbbf9d03fc9fb7c33fbfffa37ee507ff4d3ec33ffcfb3adf54fea33556fe91fc7cd5be54f9b7f9e1ffbc1dfee13fea577ef0dfe433fc3f4ffeafde7efbeda1ff72f550ab49d93675b86d9f7a577dde57e5a9d61f1dc0ec80ab792820d57b143f52b35596a3e2d3db936c6fbc4f14d31b6bc78f9c8f99b5eb9dd18a5f564ff51791aaa5b2de599bf2a8f865ed99e01ffee11ffee11ffee11ffee11ffe554c6fac1d0ffff00fff79cd56f00fff9907fce7f5c33ffcc33ffc47fed118f8dffa4431bdb1763cfcc33ffce7355bc13ffc671ef09fd70ffff00ffff01ff947632ec1ff8bb7de7aeb5bbe106bb03cfb0b202bd28f8f62d573d6a6f28c8cabc8ce35aad1c7f416dbfbabf76c8d472e9ccc3f8ab731950ba59a5be5f079d67ff6dc54cf9bcf5999eba2e822ea79a8b58de2a2f6e83c643e592d6acf6c5b261507fff09fadef486e95c3e7817ff8afac759467645c45f00fff9107fcb7929f1f1fc5aae7ac4de519195711fcc37fe401ffade4e7c747b1ea396b537946c65504fff01f79c07f2bf9f9f151ac7aceda549e917115c13ffc471ef0df4a7e7e7c14ab9eb3369567645c45f00fff9107fcb7929f1f1fc5aae7ac4de519195711fcc37fe401ffade4e7c747b1ea396b537946c65504fff01f79c07f2bf9f9f151ac7aceda549e9171153d04ff2fbef18d6f7ccb1b560e69af2dea571aedf787b37760d482f50ebf3fd0fe39abd3c765eb1a7954daa23598c951898f725d12b82c9fda6bdf6fdbfcc5df0a758eae951aabce838a573ed1651af5a90b42b5d98b5bf5a97cbed6ac2dea8fe639d27f05ffe91acce4a8c4c37f4df01ff747f31ce9bf82ff740d667254e2e1bf26f88ffba3798ef45fc17fba0633392af1f05f13fcc7fdd13c47faafe03f5d83991c9578f8af09fee3fe689e23fd57f09faec14c8e4a3cfcd704ff717f34cf91fe2bf84fd7602647251efe6b82ffb83f9ae748ff15fca76b3093a3120fff35c17fdc1fcd73a4ff0afed33598c9518987ff9ae03fee8fe639d27ff58cf87ff1cd6f7ef35bada0bd9065e3b243e617c0bf477ef63dcbd98bcdea8e3c47c655fab3b5adbcf700f1efd1c51579f6fc7a39547bd6ef0ff2485ea56c2f2bbf34edb8913195b350f553eb335a7fc5bfd20ffff571957ef887ffaa17fcc3bf7f877ff8df2bf8df7a449e23e32afdf00fff552ff8877fff0efff0bf57f0bff5883c47c655fae11ffeab5ef00ffffe1dfee17fafe07feb11798e8cabf4c33ffc57bde01ffefd3bfcc3ff5ec1ffd623f21c1977dd8a52858e1ef46c8c5ad4a5cde6b5efd5c395c1ea37d63fdb31bd79ad5ebd75b29b347229a8f1f67d6dcb0ef3daef0f9f9fbf5a57b51795da6cfeece02bbfea39535ed12f1175d1fbbec857fdf4f1d9e5b0b6abbd1a612acaad3c2acf15c13ffcc33ffc8fd65ae987ffade01ffe7deee83df2aa3e5704fff00ffff03f5a6ba51ffeb7827ff8f7b9a3f7c8abfa5c11fcc33ffcc3ff68ad957ef8df0afee1dfe78ede23afea7345f00ffff00fffa3b556fae17f2bf8877f9f3b7a8fbcaacf15c1ff65f8df7c00ab52884a668da34b202bde7bfb5a942a0ba37279ffb55dcd4bbd473e76feeaa71fab0e87ef53074dbdfbf58f72a93df207ddd7a32ea6dedaab4b5d5dfc7e4c54831f1b79a80bdefa446d6afffd6559d555e7178a7fb6fed9859379457b597956bef0afc7643ef07f3e16feb7edead9fac33ffcdb3ef8877f150ffff0ef6b877ff887ff7e2eefbfb6c37f93f5c03ffcdb36f8877ff88f05fff0af7efab1f00fffb66efbd3d704ffba56f8877f551bfc37e9a79e952ffceb31990ffc9f8f85ff6dbb7ab6fef0ff66f17ff5f6db6fcb9d8840f1897bef765c74e8fcf8e800ab1c368f1deb2f8f0a9059ae9e4f96af077f6fbeab462e81c8275a2355af1ad3f38d6aad5c4ad1d819552ececa19cfc6f5ce90afa1b29e51ae6acda3e7aac219fcc33ffc6fc7c13ffcc33ffc6771d60ffee13ff286ff168e9d11fcc33ffc6f7de05fd704ff715ef8877ff8df8e837ff8877ff8cfe2ac1ffcc37fe40dff2d1c3b23f8877ff8dffac0bfae09fee3bcf07f39fe5ffa814a55705482d6e28398e58936336a5b9eab8bd1cb65e3546daa3ddbccde46cff6ab039b1de2de1c54bb5f573f46e5b071ea82b663b233b117d8a8e6e8bd899ad51c7a7b1ec544cf3eaf8dc92ed66afd4aeabcf462558e48f0df1fbbb71ffee13fca1fe5f0f9e11ffee15fe785ffd6aab5aa7ef86fddbe3dfdf00fff51fe2887cf0ffff00fff3a2ffcb756ad55f5c37febf6ede9877ff88ff247397c7ef8877ff8d779e1bfb56aadaa1ffe5bb76f4f3ffcc37f943fcae1f3c33ffcc3bfce0bffad556b55fdf0dfba7d2f15c07652ea0044c6fe80650bed21ed5d42d6df1f40bbe0d1c2658740415cb9d0a27ebff9ead98f57efd1c1e8ed835adbde7cb2c36bd7355ae7a88ecaa513adbb6d5bdfa3cb2dbb68bdc74de7174eaf3e75962a7bd6bbb4bcbfcd7353b8047b97417626e11ffee11ffee15fe7827ff88f7ce07ffb0efff00fffe71ef0affbe01ffe7d0cfcc3bf1a67e3e1ffbc561be3e3e11ffee11ffee11ffee11ffee1bfa531597df0df641ef887ffac56f8877fd5f746f36fff04a182afd2a70ab2ef4a958b454dae92dfe78800880e9d7d561b66c767e047b544637b5ed1c519a93207159b1dceccaf7a26fc7cec58952b1a1fd518e58eceb59a7fe465637d7ef53c93279a43f4d3c7d97c3ea7f25579d67195be9ea712fcc33ffcc37f344ecddbfac1ff3647541ffcc33ffcc33ffcc3bff25579d67195be9ea712fcc33ffcc37f344ecddbfac1ff3647541ffcc33ffcc33ffcc3bff25579d67195be9ea712fcc33ffcc37f344ecddbfac1ff3647541ffcc33ffc3f4efe4f1fc08a0ebd1aa80ac8a01d1de7d55bb4c8d78fcf2e8d5ead513dd9266517560453759c7ab66346d7b6b2ee4abd439a4156392fd9851ad53e7a39a95cd57daa5c8cd1f99badcdc7ab7a9546ce1afc6ff3a97ae01ffee11ffee11ffee11ffee11ffee11ffe7d3ef86f67f5f958f8877f9bcbcfd58fadd4e3c7c33ffcc33ffcc3ff762ef00ffff00fffaa2f13fcc3ff15fcc33ffcc33ffc4ff17f6d07f516c3b747f1d9e2fa9febb38dcfeab093886a8c16d9fbfaf1aa36eb176d6ee41b6d4eb6e1517fb46eaafec8d3af811f13e5b3738ae6efd746e5ecedd7eaa1f6c6e7b067269b971da7ce406bf11a44fb636b5540fa1875d6adbfafcdc7ab9ad49af9b13e9ff285ffd6a21abd1ffcc3bfed837ff86f0dfee1ffce0ffee11ffe754ef8877ff8877f55dbfa13fee11ffee11ffe634fff13fe9b8cb11ef00ffff00fff3ea78fcb72c23ffcdb3ef887ffd6e01ffeeffce01ffe47f87ffd270855b09aa03f34d986289fa82dda30953fcae97347f54700accf2d984fe4e16bb7f1d99a441b64fba2f66c5ecacfb7fb1ceaa752762945eb9aed57e5fcf414ad978f896af0fd6b4c742647ce8aefcff63d1b6f6bf2cf918fcda7dab233bdc6a91a320f9517fe9bcc03fff00ffff01fc5c13ffcab9afc73e463f3a936f8877ff8dff6af31f00fffd633f2807f3d67f86f9b7ef8877f5523fcc37f36ded6e49f231f9b4fb5c13ffcc3ffb67f8d817ff8b79e9107fceb39c37fdbf4c33ffcab1ae11ffeb3f1b626ff1cf9d87caa0dfe1f9eff6b1bb4fc5c0dbde9dae78dfc4f75406c4c3441b56036a78f57b2e3d71a557d769e955afd7c9587cfaffa7bf38de6e4f743ed4b044f06ba9aa7eff3ffa21a7d6ebf76fe20aa5cb6cf9ec3ec5f26bf76fe0cabfc7e4dd47aa839476dcadb7af9b5f7fb6cff45e755cd29aa25cae36b827ff887ff26d743cd396a53ded60bfe9becf3397dbc12fcc3bfaf0bfee1dfd707ff4dd601ffdb3551eba1e61cb5296feb05ff4df6f99c3e5e09fee1dfd705fff0efeb83ff26eb80ffed9aa8f550738eda94b7f582ff26fb7c4e1faf04fff0efeb827ff8f7f5c17f9375c0ff764dd47aa839476dcadb7ac17f937d3ea78f57827ff8f775c13ffcfbfa1e13ffe13760d9a4ea39023e9a98973fb8d106f4c647a0f889f76a5273b2f5f99c23cf917c4cb40eaa46df96cd515d12de23ab47d55dcd5dad47ad79b48f953552e7d2be67b5f5dea3baa3b6a8a65ecd91bf1a37c2506fdfe01ffed5bbf7807ff8877ff887ff96b6c1ff56f00fffdecbe7807ff8877f5da36f83ff6d8cf2877ff887ffad6f568faabb9abb5a0ffcc33ffcc3bfcf0dfff09f8d877ff857b9e0ff3c261be7e3e01ffed5bbf7807ff887ff7dfcbfb4c9b3c41174bd8da81e00eb61f3479bbdf6a98d881652f5a93aa279fbf8eac26787bd7a50d45c22007b978ecad77bb7f97d1e3fe7e8d2b0e3a2bd51738a6a56fba0f6d13e4750f839b564ced1fcaab9bcbf9faf3a8fb63d3b8b23f9fcdce03f9fb78f877ff8877ff8877ff8f7f3c872797ff83fafcfcf1bfee13faa09fee1dfe6f53578c17f93f97aeff00fff3e16fee15fcd19fee11ffef379c03ffcc33ffcc37f3b8b857ff8f7f3867ff88f6a827ff8b7797d0d5e8f8dffd7df80e5a50e59b4417e5c54b42aa6e21b4d4e4dc8c7f7daa24312e51f996f74f0fc7394333bb8513d6bdcaa6c8daa6058cf08b85e1db69ee85c64b5f9f6de19aa7af6f2457e99aafb535dcbdee5965d90b382ff968ef51eaa4ef887ff4a3ffcc37fe6abea8dea59e356c17feed9cb17f965827ff8877ff8877ff8cf62e11ffe67e3e11ffee11ffee11ffed5d855f09f7bf6f2457e99e01ffee11ffee11ffeb358f887ffd978f87f1efc5faf83969f7e72de607db77d769c4fea9fd582d8a2fd44d462479ba3ea5473aa806afff9b1aa6f1d67e7106dbe1da73cfccfa816dbafe613c1ade26dfdf6a7f7b4b1aa4d8df167a01713f946e7d4d66efff97ef5eef745e5c8cebaf7f6632a9e595daac6f5391a9b9d31550bfcc3bfadff0afee1bfc13ffcc3bfafef0afe377df07f370efee1df8e817ff85ffb547ef887ffac2e55e3fa1c8d857ff8877ff8f7edde1ffeb7f1760cfcc3bfafef0afe377df07f370efee1df8e817ff85ffb547ef887ffac2e55e3fa1c8d85ffa7c5ff4b5f4c047ad466dfabf16a93d4e1f2938816d08fef7945aa5c3e516df66794dbae51b4569503121dd66cfd7d8caac77b794f754147978282511d5adfafa08fd644c5ab7eb52eaad66c2ebd7a7afb9601e93d7dedea9253efd9be7bdfa866f8877ff8877feb01fff00fff793df00ffff0dfc25cf00fffeb3bfcc37f6bf00ffff00fffe7fdf00fffbe4fcd3baa1bfee1dff7c33ffcc33ffcc33ffc67827ff85fc7fafe5e3df00fff53fcbff3ce3b373df36833558cda18ef9d4db0b790d18155b97a39a305b3795a5093f2aae6892eb856984b343faf08f21b7171f52e842857b646bdfdb3cfd93ea879a9da6deeecdd7bf9585f77af5ee55d390bd1faf8dab20b2e63a9e79d9d33f8877ff8877f1f1bf9c23ffcc33ffc47757b6ff8877ffb0cfff0affce1bf26f86f67b9b377efe563e11ffe6d9ed6e03fca05ff4d7ac33ffc6779e01ffee13fcee7e3e03f17fcb7b3dcd9bbf7f2b1f00fff364f6bf01fe582ff26bde11ffeb33c36f6eaedb7dfbe890e99dafc6c21661441911d3c3fb6775954a1ccda23302b30588fd6e62ebccaa1cde2b3313e263ae49575505e2d984f6fde9533d69b6f747eb2b3adfc7a177354b38fb5fe2317a8fa256afb95affa99c5a83edb56a97f54f00fff2375a8b8d52ff3f031f00fff3646c5d976f8877ff8efd73f2af887ff913a54dcea9779f818f8877f1ba3e26c3bfcc33ffcf7eb1f15fcc3ff481d2a6ef5cb3c7c0cfcc3bf8d5171b61dfee11ffefbf58f0afee17fa40e15b7fa651e3e06fee1dfc6a838db0efff00ffffdfa47f5c6f3efbf012b4b5c99845205649f2f9b5c943fbb38bc7a3955ad95b5f0de2aae77f1d8bad4b31ae321a9d45b5d6b35975e2d513ee595ed8f8def9dbbabce45ddbbbc568fec1ca87cd959f69ed95952f5da779b338a1bf1a85e1af00ffff00fff3ddf48f00fff9917fcc7f5da779b338a1bf1807ff887ffb6f180ff782d32df48f00fff9917fcc7f5da779b338a1bf1807ff887ffb6f180ff782d32df48f00fff9917fcc7f5da779b338a1bf1807ff887ffb6f180ff782d32df48f00fff9917fcc7f5da779b338a1bf180ff87e7fff40d586aa1b2416a32a3056645f7208efc7ae0f4c053f3b23545f5298d00138151dd5cdbaeea55b5a95c51bc1ad75a7e3967b9ad46a0f13947f7a17a16239f4a9c8d5ff356c7fbbdf0baeafcf2bd12bf84556c940bfeb7f3b23545f529c13ffcaf79abe3e11ffeb3f9f47ced5ce0bf160ffff06ffbb29aa2fa94e01ffed7bcd5f1f00fffd97c7abe762ef05f8b877ff8b77d594d517d4af00fff6bdeea78f887ff6c3e3d5f3b17f8afc5c33ffcdbbeaca6a83e25f887ff356f753cfcc37f369f9eaf9d0bfcd7e2e11ffe6d5f5653549f5295ffd3376055125537ea085580ac1c88a3fd7a8b1a6df4c80597c54407313ba0eac25371bd7ca36dbe5fd590c5ceae813abf33bfe0540d6a5c14dfcb9de5eb5d287b94cd01fee13fca37dae6fb550d592cfcc37ff41eb555c7c33ffcc33ffc8ffac13ffcc33ffcc33ffcc3ffd6f328c13ffcc33ffc47ef515b753cfcc33ffcc3ffa81ffcc33ffcc33ffcc33ffc6f3d8fd29bc4bffc1384d1a1ceccb30356e98b269dc5550f6a06b5ba1ca2b965b544b58ff4cd8ccf0e53f562dc536b45d945575d0b155f99dfc8da54d66355ef12cdf28dce3d6aef9df388df1e0ff00fffbd3a4604fff0dfebabd431b20ef00ffff00fffbe56e56105ff71ae59c13ffcf7fa2a758cac03fcc33ffcc3bfaf557958c17f9c6b56f00fffbdbe4a1d23eb00fff00ffff0ef6b551e56f01fe79a15fcc37fafaf52c7c83ac03ffcc33ffcfb5a9587d57df37fbd36fa09f9a254116b5b5690edeb6da67d5e637b8bef73a8785bab9f9beacfe6a6d647ad976a8fdefd4f151f1d22558fed53aa5c34eac2b07d514edb9fe5f1672a93dd137f567ddef579cd11b54517a58d8d6a56e7389a8b3a478aa728a7edf7fb60eb5573f6b9ed4fb536f00fff6b3ffcb7b066f8877f5f8b8f51edf07f5e1bfc6f6b57827ff8877ff887ffb6a9c1e780fff33e25f8877ff88ffbe11ffe95e0ff3c37fc37e909fff06ffbec4f150ffff0df8b877ff887ffb6a9c1e780fff33e25f8877ff88ffbef93ff6b75a0edcfac3f3b3436898a8b36bd7a50d5e1b33f7bb5a998ec60fab8a816fb536d9ecddf83368b5507d9b7afefbdba7cac3a903e5706457621ad71aab6ea25e3eb8deaf1ed2a36baacd4a5a0e2a3ba7c8dd979ccd64bad69547fb427d95983ffb6c907fff00ffff00ffff01f3dc33ffcc37f9375c23ffcfbfcbdbcf0afe3a258f887ffd6e01ffee13faac7b7c3bff6b779ac3ffcc33ffcc37fd40ffff0afea847ff8f7f97b79e15fc745b1f00fffadc1ff53e7ffa59fc86aae36a32295445d16aab86c33a24394b5a985535e365fb6062a36abcf7bf9f9f90ba6ba563ebf5a335fbf9ab35fa7defafacb5929cba3ded573e55254eb58d9936c7e517b7426238f683facb2fd56f389ceb23a3f91afca6fdfe11ffe551e3f06fe730ff8efcf11fee1dffbc33ffcc33ffcc33ffc475e593edf06fff00ffff0afdae11ffe7dfea81dfee13f8acdea837ff8877ff8877ff887ff96fac13ffcc33ffcbf69fcbff48115507d51be30ef9915e5c72befa8cde7559b1ac5aa0556b5da78e5ab0e4bb6e8caa3028ccda72e2dafca21f2b12a8f6f5735446dd1fa47f546f5fb8b5fad818ac9fc7b8a72af7dd653fd22edf92a9f5e0d51dfea11e556b1d11985ff385ef9c27fdc06ffb1aff2e9d510f5ad1ef00fffaa86a8661fabf2c03ffcc33ffcabbe5e1bfcc7becaa75743d4b77ac03ffcab1aa29a7dacca03fff00ffff0affa7a6df01ffb2a9f5e0d51dfea01fff0af6a886af6b12a0ffcc33ffcc3bfeaebb5c17fecab7c7a35447dab07fcc3bfaa21aad9c7aa3cf00ffff07f39feafde79e79d1b7580d4426507c2f6abc2460e69e69be55213ec794510f43ca39a6c5c1546b5b9d54b2eeaaf40dbcb57a9d77bccf8aa3cab87ba3c7af5f5da23ffe8bc2b9f680d2af96c7cefcc8c30a76aac9c21f8877ff8877f950ffeb527fc37e931e3abf2ac1ef00ffff0df86fc7ae3e11ffea336f86f254ff86fd263c657e5593de01ffee1bf0df9f5c6c33ffc476df0df4a9ef0dfa4c78cafcab37ac03ffcc37f1bf2eb8d877ff88fdae0bf953ce1bf498f195f9567f5807ff87fd6fc2f1fc06a892a073f4d101cbae890d9b8bdf21742f55064f554e7a4ea8872d9faa283edc7db79a9fc916f76e02afeaa2fd3e8fc66d637f38be6578530aabb7709f5cef2e8f9193d2fbdcba22af8877f356fe5a104fff00ffff00fffdbb8eafce07f4ef00fffd1fce0bf26f8877f356fe5a104fff00ffff00fffdbb8eafce07f4ef00fffd1fce0bf26f8877f356fe5a104fff00ffff00fffdbb8eafce07f4e4f81ffee07b07a931b3d349750efc0f52e978a67d45f3ddc2310671e56d925d2cb175dea2335f60ee325c1ea5d96bd9f33debd31d1395b94adf39e0b52d5e1db543edf36932bf380ffbe1ffccf09fef33a7c9bcae7db6672651ef0dff783ff39c17f5e876f53f97cdb4caecc03fefb7ef03f27f8cfebf06d2a9f6f9bc99579c07fdf0ffee704ff791dbe4de5f36d33b9320ff8effbc1ff9ce03fafc3b7a97cbe6d2657e601ff7d3ff89f13fce775f83695cfb7cde4ca3ce0bfef07ff7382ffbc0edfa6f2f9b6995c9907fcf7fde07f4ef09fd7e1db543edf36932bf380ffbedfa9cdfe09c2ea4245492bb05635ba6995051bf1539e55382b9be30fc5ec9a67f18b7a39d48154f3b6fdbd5cd6233bfc765cb55ee51fe5ee69cfc5d7fb85350aa81f1fbdaf6dabb21cea8cf5ceebea395263afbdd717c5576aa9fac33ffc471e33827ff8efb5c13ffcc37fbf2f8aafd452f5877ff88f3c6604fff0df6b837ff887ff7e5f145fa9a5ea0ffff01f79cc08fee1bfd706fff00ffffdbe28be524bd51ffee13ff29811fcc37faf0dfee11ffefb7d517ca596aaff63e57ffa1bb032ed39d899d7a57cd5bb6fdf0352ef42ab6e981dd36befd5b66a14b60ac823755c6a3f47fa168dfcc2a9f846b197f0ae8ebd144bd5fc7bbde01ffe67bce07fdb0efff05f1907ff7339a358f89ff7827ff89ff182ff6d3bfcc37f651cfccfe58c62e17fde0bfee17fc60bfeb7edf00fff9571f03f97338a85ff792ff887ff192ff8dfb6c33ffc57c6c1ff5cce2816fee7bd9e23ffafbf016b6d884cb2b62cf943289afc7d019fc56400da8bd1ab52b79f6b659c8d1b5dabde5c462ec4caa5e1df7bbfd8b2bad5b8cad89e976d5ffda2359ef1afaed38807fcefcb311203fff00fff9715fcc37faf6ef887ffa3728cc4c03ffcc3ff6505fff0dfab1bfee1ffa81c2331f00ffff07f59c13ffcf7ea867ff83f2ac7480cfcc33ffc5f56f00fffbdbae11ffe8fca3112e3d7faec1bb0d60133e6a3c0ed89abeaa10fc2a5eab8d4a1bab466ea3e6a7e475e7ca3677fd19e398cacdb1e2ee1ff3282ff5bc1ff9ce07f4ef0ffb804ff7382ff39c1ffe312fccf09fee704ff8f4bf03f27f89f13fc3f2ec1ff9ce07f4ef0ffb804ff7382ff39c1ffe312fccf09fee704ff8f4bf03f27f89fd353e0ff7a3d208bfce2af7d362632cc265a5d8435ff88547cefd047f3ca3c47fabdfcbc2ae3ab39aa6b96ed65756c2f66745df668748fd6751ad9071bafce92ef536bb0f4552f9f48767ccf6b944bf8af798ef47bc1fff182ff7e6c2f1efee11ffe730ff887ffd17e2ff83f5ef0df8fedc5c33ffcc37fee01fff03fdaef05ffc70bfefbb1bd78f8877ff8cf3de01ffe47fbbde0ff78c17f3fb6170ffff00fffb907fcc3ff68bf57c6ffeb3f41585d24558cddb06cd2d51cd5587558b25a7abe7bd6e1316a9d8fff79a93cb363178d8c1fcd573d0b769d2a353df419abf2d6ab13fe6bfd4f4df01fc7c33ffccfe67d2a82ff381efee17f36ef5311fcc7f1f00fffb3799f8ae03f8e877ff89fcdfb5404ff713cfcc3ff6cdea722f88fe3e11ffe67f33e15c17f1c0ffff03f9bf7a908fee378f887ffd9bc47eaec4f10565501fbbe26337bb91c75584695c1bdf7c2bc44fc5e50672ed83dba6f88ee239fcf317341a9b6d9dae17f5ef07f59c17f4be3e0ff7cdc68df5ec1ff6505ff2d8d83fff371a37d7b05ff9715fcb7340efecfc78df6ed15fc5f56f0dfd238f83f1f37dab757f07f59c17f4be3e0ff7cdc68df5ec1ff6505ff2d8d83fff371a37d7b05ff9715fcb7340efecfc78df6ed15fc5f56f0dfd238f83f1f37dab7579e876bdbb17656b59af8c39125aec8c6aecfaa4dd592c5a878e55f3ddc23b20b1fad975fcfea21f09b9ad566f7b9e23f026bb42fd9383f469d43b5479157ef2cceee5b3476664df62adbe7e81cab3df7fdf0afbd7a355604ffb531f0df17fcc37fe4ed3d552cfcd705fffd3ae0bf2ff8af8d81ffbee01ffe236fefa962e1bf2ef8efd701ff7dc17f6d0cfcf705fff01f797b4f150bff75c17fbf0ef8ef0bfe6b63e0bf2ff887ffc8db7baa58f8af0bfefb75c07f5f11ffd7330bb31844e3a2cba37a39f8b1d64fe5b7cf3387ddd73bea55855c1d90f5c28872566bf0fb911da0ec429c81d08e1db9686d3dfe3d3a3b3dd0fd5aaa33375ae328d8b3dab3f6d6239a5fb6e723feab17fcb7300efe757f548f7f87ff790ff81f13fc6f6b9a11fcef13fcc33ffcc3ff5e0ff81f13fc6f6b9a11fcef13fcc33ffcc3ff5e0ff81f13fc6f6b9a11fcef13fcc33ffcc3ff5e0ff81f13fc6f6b9a11fcef13fcbfa1fccffc09c25e41d9215731953147e49d899d89ef79d88b576dcc9e5cea108fd6d78bc96267f6ff085dc2b3e2ef7f894617b6dffb8ab7efdb7b5e2ebd46d51cf00ffff0dff7f67df05ff73c2a7626bee701fff05ff1f67df05ff73c2a7626bee701fff05ff1f67df05ff73c2a7626bee701fff05ff1f67df05ff73c2a7626bee701fff05ff1f67df05ff73c2a7626bee701fff05ff1f67df05ff73c2a7626bee701fff05ff1f67df05ff73c2a7626bee701fff05ff1f67df05ff73c2a7626bee771c6d1d11fc0ca128ff4cdfa1f7dc8fcc22dca407c0a3a6a4f1e6ace47e69dfd85a57cb28b52c5fbbc97fae575895ff47b7257fa66fde1bf2ff83ff75a04ff9711fc3f3ec1ffb9d722f8bf8ce0fff109fecfbd16c1ff6504ff8f4ff07feeb508fe2f23f87f7c82ff73af45f07f19c1ffe313fc9f7b2d82ffcb08fe1f9fe0ffdc6b11fc5f46f0fff804ffe75e8be0ff3282ff5c17fb00d663bb90f66c542f76edefc15cf5a96acf1a4763ab9e76ce97d8e7d10b2df25874e42f94a7f48bf62105fff33e55c17fdf6311fcdfbfe07fdea72af8ef7b2c82fffb17fccffb5405ff7d8f45f07fff82ff799faae0bfefb108feef5ff03fef5315fcf73d16c1fffd0bfee77daa82ffbec722f8bf7fc1ffbc4f55f0dff75804fff72ff89ff7a90afefb1e8be0fffe05ff359f8b7f0316ea6bcf05b3f79073a168edf965b8e79707fbf1e609fe1f9fe01fdd97e0fff109fed17d09fe1f9fe01fdd97e0fff109fed17d09fe1f9fe01fdd97e0fff109fed17d09fe1f9fe01fdd97e0fff109fed17de8497c00eb391ecaa732a74bd6f9506bc025f7b404ff0f27f8470f2df87f38c13f7a68c1ffc309fed1430bfe1f4ef08f1e5af0ff70827ff4d082ff8713fca38716fc3f9ce01f3db4e0ffe104ffe8a1f59cf9e71bb0104208218410420821841042082184104208218410426852d70d21841042082184104208218410420821841042082184d094de880f603d96af2fe36befb67a53d784b3707f82ffc72bf8479716fc3f5ec13fbab4e0fff10afed1a505ff8f57f08f2e2df87fbc827f7469c1ffe315fca34b0bfe1fafe01f5d5af0ff7805ffe8d27accfc3fdb0f60d9c92e7f6f51b51f9da7a7993ab2b8873a58b379d538bb260fa9de9c8e5eeb6cde5ccefb05ff9713fcef17fc5f56f07f39c1ff7ec1ff6505ff9713fcef17fc5f56f07f39c1ff7ec1ff6505ff9713fcef17fc5f56f07f39c1ff7ec1ff6505ff9713fcef17fc5f56f07f39c1ff7ec1ff65f554f8bf9e3553f18fe9e04407bc02fc3a8fca7c662f90eab82ceee8cbabba7f7be77c9fe764cf9c22882f59cfd23f926b662da331f07f2bf8cf05ff97ab07fe8f11fc8f0bfee3b1f0df8f87ffbae0ff7e04fff531f07f2bf8cf05ff97ab07fe8f11fc8f0bfee3b1f0df8f87ffbae0ff7e04fff531f07f2bf8cf05ff97ab07fe8f11fc8f0bfee3b1f0df8f87ffbad671d736d1d140f7549960e5c01ead751dee6b3d66d6e148f056cd7e5270b496fbb888ab974c56c325ea8cced4e8a5b887593f06fecf05fff0bf08fe2f2bf8bf15fc1f2ff8afd700ffe7827ff85f04ff9715fcdf0afe8f17fcd76b80ff73c13ffc2f82ffcb0afe6f05ffc70bfeeb35c0ffb9e01ffe17c1ff6505ffb782ffe305fff51ae0ff5cf7c9fff59ac84ee49260586507bc7a284617a93ab751202ab1d1d8ca1c7ccc11e059f9da46fcf77e0a7056d91e4510cdace3ec2fa69131bede915f96bdbcbd7308fff538f81ff3bdc4b855f05fcb0bff5bdfd938f81ff3bdc4b855f05fcb0bff5bdfd938f81ff3bdc4b855f05fcb0bff5bdfd938f81ff3bdc4b855f05fcb0bff5bdfd938f81ff3bdc4b855f05fcb0bff5bdfd938f81ff3bdc4b855f05fcb0bff5bdfd938f81ff3bdc4b855f05fcb0bff5bdfd938f81ff3bdc4b855f05fcb0bff5bdfd9b8a3f97ff1cd6f7ef35b59c0decd58c6577ced61b100459eca635699cfcc9acc42ba67fd33d8463c3210670fafaf6dc6f708204672ecd9e399f319edbdbf1ca3b1a3eba0c6c07fad0ffefb3e596df05ff383ffd85379cc0afe752cfcd762a33ef887ff911ae0bfee3932b62af887ff4a1ffcf77db2dae0bfe607ffb1a7f29815fceb58f8afc5467df00fff2335c07fdd73646c55f00fff953ef8effb64b5c17fcd0ffe634fe5312bf8d7b1f05f8b8dfae01ffe7b7afd01ac6cb36727ba8edbfb4947ef578d9d39947bfa8fc873f4e1ba94462eabde78f57e8446cf4be5b2cbc6f7fc667ed18cd6b5a77ef887ffaae07f3bbee707fffb04ff8f47f0bf1ddff383ff7d82ffc723f8df8eeff9c1ff3ec1ffe311fc6fc7f7fce07f9fe0fff108feb7e37b7ef0bf4ff0ff7804ffdbf13d3ff8df27f87f3c82ffedf89e1ffcef13fc3f1ec1ff767ccf0ffef7e939f27fbd3eac8baf0ecd9eaf98ab7ea55a247521550ef59a7be4b2b3b5fa0d1c01a117d35bcfea9afbfa460fe89e185bdfecd7215aff91b58de27cbbaf2bfbe5167d656155b67ebb7faa86ca2f1f7f26a3b9441add5ff887ff9118f8df8e87ff73c17f0b6beb8d87ffb826f8877ff5ac72c37f7fec4c0cfc6fc7c3ffb9e0bf85b5f5c6c37f5c13fcc3bf7a56b9e1bf3f762606feb7e3e1ff5cf0dfc2da7ae3e13fae09fee15f3dabdcf0df1f3b1303ffdbf1f07f2ef86f616dbdf1f01fd704fff0af9e55eec7ceffd99f20bc4f6530d898d9cdcf3c2fed379aa302a4efdb9b43b5db43e7d77e06f80c94dede8ecc33cb5de93b022a1b57f9053bd2371a1fc51c7df6f708fee378f887ff3df1510cfcc37fd60eff6339a238f8ef0bfee378f887ff3df1510cfcc37fd60eff6339a238f8ef0bfee378f887ff3df1510cfcc37fd60eff6339a238f8ef0bfee378f887ff3df1510cfcc37fd60eff6339a238f8ef0bfe6f35f401ac0ce20cb66cec684c55f7b1d023fd97925ffbd1d86c7cc5fb21d7794feee8178f8fb9c4fc8fe6a37ac1eedd2bf89fefbf94e07f7f1ef8af09fee7fb2f25f8df9f07fe6b82fff9fe4b09fef7e781ff9ae07fbeff5282fffd79e0bf26f89fefbf94e07f7f1ef8af09fee7fb2f25f8df9f07fe6b82fff9fe4b09fef7e781ff9ae07fbeff5282fffd79e0bf26f89fef8f749d99544dfd2725d5a7d62a5fa156fde46056d7da57f1aa5e5ed19cecd8a87fe6c218d907fb356f3d3f159b7d455cf4356ebd795734f28bc3d7e49fa35f2c593e3b56ad8b6f8f2e95deb9ceceece83a669f48ae5ccea39fcc85ffbb58f8dffad9f1a3827ff8efc544f5c17f3bcb0bfff00fffe731f0bf1d0ffff09f09fee358f8dffad9f1a3827ff8efc544f5c17f3bcb0bfff00fffe731f0bf1d0ffff09f09fee358f8dffad9f1a3827ff8efc544f5c17f3bcb0bfff00fffe7314f85ff5d7f82506d5815c8eac4ef53d98654363f1b57c911e5cb3637bb7c66d76f14447fc8a29a7a9750e497c556949dcbd95f90ab67656d6c7e95af7a26bceffab3f24ba07781cc08fe6b9ed51c513ef88f632b827ff8af08fecfc747ef593cfcc7b1ebbbedb3312382ffe305ffe7e3a3f72c1efee3d8f5ddf6d99811c1fff182fff3f1d17b160fff71ecfa6efb6ccc88e0ff78c1fff9f8e83d8b87ff38767db77d366644f07fbce0ff7c7cf49ec5c37f1cbbbedb3e1b3322f83f5ef07f3e3e7acfe2e13f8e5ddf6d9f8d1911fc1f2ff83f1f1fbd67f1f01fc7aeefb6cfc68c08fe8fd743f07fdd268a694141d9811c81d8fa557357d51bd3832a3b609539f43eb1ea81edd5117daab5724144979fbf2c463efd67ebf235553e1919f945b92bcad6d2ae51f68b2ef2ac5c383e87df17d5a6647fb9656ca84fa48ef0657365fdf6e7ea07ff793cfceb79446df0bfad05fee11ffeeb827ff8f76323c13ffcfb5cf0affbedcfd50ffef378f8d7f388dae07f5b0bfcc33ffcd705fff0efc746827ff8f7b9e05ff7db9fab1ffce7f1f0afe711b5c1ffb616f8877ff8af0bfee1df8f8d74dffc4f7f0356e59258e3a2b17621fdc4fd01f03e517ebf39954dea1db4aa873ff8bebf37bebaa6ca77345f35ae0254357774162ab159bed17edf360372e63d7b09cde4ed5d529712fcb7b41ef8af79c1ffbebcf0aff3c13ffc677eb60dfe8fcdede3a2b1f0bfcd03ff7d4ff8bff3837f9d0ffee13ff3b36df07f6c6e1f178d85ff6d1ef8ef7bc2ff9d1ffceb7cf00fff999f6d83ff6373fbb8682cfc6ff3c07fdf13feeffce05fe7837ff8cffc6c1bfc1f9bdbc74563e1bfb56b35c99983690bb5e37a9f3ef431f6228a3e09a926bbe68c16215b90914f3156e1579f7c53ebebe3559d6a5dfdb33ad07bb5e7e0a9b1d54fc046ebd33b2f76acf7f180af3ebdb1fe39d25a47e4e7ebb6632adeea3dca3322f887ff48f0bf7d8e04fff06f9fe11ffee11ffefd781b03fff00ffff06fe3e11ffe95b77a87ff6d6ef8877fe503ff7d6ff86f69adf0af63e01ffee1fffc19fee11ffee1df8fb731f00ffff0ff66f3ffe2adb7defa5664d8db8008f86c9cba1cfcc1ed7964e3a27a22d0ab175ea596ac46d59e01e437555d22d1dcd57b76007bb98ff65ffb7beb5c995f2ff7ec19ce6a18f5b397b38df1cf915f963f3bff95739cad27fcc33ffcc37f6f2cfcc3ff88ffda0ffff0bfb6c17fec07ffe76d2a2e7a9f3dc3590da37ef0bfadcdb7c17fec07ffe76d2a2e7a9f3dc3590da37ef0bfadcdb7c17fec07ffe76d2a2e7a9f3dc3590da37ef0bfadcdb7c17fec07ffe76d2a2e7a9f3dc3590da37ef0bfadcdb7c17fec07ffe76d2a2e7a9f3dc3590da37ef0bfadcdb7c17fec07ffe76d2a2e7a9f3dc3590da37ef0bfadcdb7bda9fc5faf1be503d505e0fbd6b1d1e494a2c3e4dbfd02aa85596254fd593dbe2e7fa959cfd527bb28559e0cea4a8d515d3d5832ef6c7d235d0d5cb61ef8ec5cf975a9ac51564b045bc5c77b65fbd7bb587cae55eb99f2e7c93edbdcd125ae6a8dce66c4a5aa0dfee15f09fee1dfe66a62aef0bfcd03fff06f05ffe7f92aedf00ffff00fff363ffcc3bfaa05fee11ffee1dfbfc33ffc5b0ffbd3e6817f2df8877f9bab89b9c2ff360ffcc3bf15fc9fe7abb4c33ffcc33ffcdbfccf91ffab77df7df7c6176213468bec37dfc6f51257374d2d4ee4178df139476a5579b3b9f4bca2dabcaa97487498d5dc22cf9e97f2b463d5e18c7cb34b41f9467bafc64439a2f79eb7cf53dd63eb37bb067e8caa45c5aab36d637a6709fee33a2a7381ff96fac23ffcc33ffc47fede17fee35ae01ffee11ffe7d3ef8877ff8877ff8dffac03ffcc33ffcc33ffcc37fdbd408ff711d95b9c07f4b7de11ffee11ffe237fef0bff712df0ff3cf84fff0461afcdf6d9fede78dfbf8eef4d34cbbfc68de4882e107f80edfcd4e15639ed98de06a9dc995f34b7688dfcb86cadb339fb7cbd835bd9b35ecd762dd41c55bd995f45d93adbe7e8ccf8f5e9cd71a4bdf7cb42f9a88bd2c7a871bd36db07fff09ff1d2ab29ea87ffb6c95115fce739e03f8f817ff8cfda6c1ffcc37fc64bafa6a81ffedb264755f09fe780ff3c06fee13f6bb37df00fff192fbd9aa27ef86f9b1c55c17f9e03fef318f887ffaccdf6c13ffc67bcf46a8afae1bf6d725405ff790ef8cf63e01ffeb336db07ff73fc5fdb802c813f5cbecfc6a8c296beecf04439adb792f5f6e0f889478044feaa5f5d443e46e58fe4f3da7944f545b5daf9f514c5ac1e7eeda33df76742d51601aa2ead6cedd41952f5fa7360e3549f8a5573f4e74ccd335a3395cffbab769f5fd5efd72eda8ba876f8877f9f1ffee15f8d857ff85fdb561ff86f67f3b1f994bf8f857ff8b7eff00fff7e1ef00fffd198b50dfee1dff6c13ffc47827ff8b7edbe2ef8877ff8877f3f16fee17f6d5b7de0bf9dcdc7e653fe3e16fee1dfbec33ffcfb79c0fff3e4ffea9d77deb9f19351efbeaf2a75a0d6e254beb52ff2b131aabea8ceacfe6c6cb426957c519daadde7aaf465f54687309bd7aade1a54fc5ad3fb19d5177954dab37333aade195cd5cba5f6599d7fff5e590f9f67f47c56d719fee15fc556fc5a83ff350efee11ffef3fa228f4a3bfcc33ffc6feb5bf3f4fc6c5d764c145bf16b0dfed738f8877ff8cfeb8b3c2aedf00ffff0bfad6fcdd3f3b375d931516cc5af35f85fe3e01ffee13faf2ff2a8b4c33ffcc3ffb6be354fcfcfd665c744b115bfd6e07f8d837ff887ffbcbec8a3d20efff0ff54f9bf5efec72e8c0d5203b218f5ec8b5bded7b6d573fd67fb221f3fdefa7a4f9f776db3fdf6a71feb172e3b4051bd954bc2d7aaeacd0e5a74318c5e4e0a861e246aada3f9a97df4fbefe3fdfaf97a7b6065efbd9ad519b2e7cf7bfa7d50976ce4979d357f56bd878df35e6a6dfc18f8877f1f03fff06f9fe11ffe6d3bfc6f3de11ffed538f887ffb52df2837ff8cfea857ff8877ff887fff37ae11ffeed33fcc3bfcf1bbdf76a867ff8877ff8b7f9e03f8e51cff00ffff07fd716f9c1ff9bc9ffd5bbefbe7ba33a94d461ca2e02955401bd294a4c20aa251b9b8defe5abd4a07cd69ab2fa6c4c0fa0ac5e75c8a2d8686c354f5697cae16b8af6b917a3e2b23a955fb5bdeaabfa321fe5b93eab717b3c7d6dd97ecde4f1f1f07feeb3d604ffe735c1fff9b8f5598d837ff887ff382eab53f9c1bf8e87ff381ffcc33ffc6fc7f978f83ff7596b82fff39ae0ff7cdcfaacc6c13ffcc37f1c97d5a9fce05fc7c37f9c0ffee11ffeb7e37c3cfc9ffbac35c1ff794df07f3e6e7d56e3e01ffee13f8ecbea547ef0afe3e13fce775ffcbff8e637bff92ddfa906f40a89367369f71e6a5c16d31b1bc5fa45f4b5f8d8cc2fca61c7dbf64ade993cbe6d510670e45569b76ba8d6ded7e5d73ccb178d537354ebd90a73896a8e2ee223e4f73b5a1b05ab5a3f1bab72f59eb37195bd82ff96e6b0e36d3bfce7f9a271f00fffd5330affb782ffd8379a8ff5ddb3372a37fcc33ffce7f9a271f00fffd5330affb782ffd8379a8ff5ddb3372a37fcc33ffce7f9a271f00fffd5330affb782ffd8379a8ff5ddb3372a37fcc33ffce7f9a271f00fffd5330affb782ffd8379a8ff5ddb3372a37fcc37f95ffb30f6059d35e9bedab423712973df7eae9e5557e0af0c8df5f1819d0d9a1b535a89aa21ab3bad47b65bdb2c3ede7652f8fd13ab23dcfe6ee632b712a87bf00a33a7ae7c0e7ad5e24bdb6cafa64f5d8bdcaf654bd8fb4a99cbdf891b8ecb9729eb3bcca0ffee1dfd6d13b073e2ffcc3ffd515fcc3ffd5a65f8d89da2aeb93d503ff57ddbcca0ffee3b301ffb19f1de37de17f1b0bffba065553546356977a87fff37964b1f00ffff00fffbd73e0f3c23ffc5f5dc13ffcc33ffcc37f5407fcc37f252e7bae9ce72caff2837ff8b775f4ce81cf0bfff07f7535ceffb582aa028c2a2c2a62956ff79ba136d317afe0ee1d36e5d15bac68de4bfffacf7bab8556074ed5e6e710ad6174206d4db6cedea15717abf2f363b27ab23589f6d3ae6b54b7daab68cd2a9046f17eeeea9cd9fa3c80d1ba45174b36277b56edc5aad6c3d6a2e4c7db71f00fff999f1f93d503fff00fff7a8eadc1bf9a0ffcc33ffcc33ffcc33ffcc33ffc9fe786fff33c7e0cfcc33ffcc3bf8a837ff88f72c23ffcdb76f8877ff8877ff8877f9f1bfe63c1ff1cff57efbcf3ce8d2ac00746c5f88df7138b16bdd7af622230d5c4551dd633cba7e619f567736d9d3965f28741cdddf7d91ad4dc7bf9546cefa2b36d594d59bee8dc55f755ad55758e6a5e2367c2d715e5cab818390bd6dbd7dcbb747cfc481e25f8877fdb96d594e5837ff8f7fd51cdf00ffff00fff51cdbe1dfee13ff385ff5a1e25f8877fdb96d594e5837ff8f7fd51cdf00ffff00fff51cdbe1dfee13ff385ff5a1e25f8877fdb96d594e5837ff8f7fd51cdf00ffff00fff51cdbefdb1f37ff5eebbefdea844d50dcd92649351c528ff0cf6dec1f46d99879ab38dabd4ec637c5cef40aff1bd9a6cdcfa6ce712d51b1dc6ec1253eb11f5f7d6dcc6a8fa7c5baf6ee5e7fbbd879f43b63eca2b9b57e59752b5d64acdaa2f5234cf68ffe0ff3cae52b38ff171f00fffd55ae11ffee11ffe7bedbe2f12fcb7d403fee11ffee11ffee11ffee1dfc7556af6313e0efee1bf5a2bfcc33ffcc37fafddf74582ff967ac03ffcc33ffcc33ffcc33ffcfbb84acd3ec6c7a95a5eaa431241ef636c5b7458ec18fbace0ef1dc668d2d984a3b9a879288fa8669fd7d61b79f8189f6f16f6de41b4fb13ed891fabcec21adfbbac7a6b65fbd45cbcaf5fcb5e4e1febe79a4196e5f239ad7f74d62ae75cd56cc7589fca85e2c75a3f1f0bfff00ffff06fdfe11ffee15fbfdb9cd61ffee11ffee11ffee13ff2f0313e1ffcc3bfcd09fff0af04fff01f8d817ff887ffb6993ffcc37f6f1eca23aad9e7b5f5461e3ec6e7837ff8b739e11ffe95e01ffea331f0ffb4f83ffd0942b57899b298ca05539d402fdef679ff6c0e95fe289f1d3f7ba86cbdd1a152be591d236b63df555bef2045fba46a8b7246f5a9fe6a4e95bf72d67c7ce5ec8cae91edf37ed5b17ebcba987afcf4f605fee11ffee13f13fcc33ffcc37fb50ffeb7de765c24f8877ff8877ff8877f351efee11ffeb5e01ffe67eaefe5b3e3e11ffed7362bf8877ff8877ff8bf6bf3b5d9773516fee11ffee13f8bb77ddeffa9f07fed03fca4fd01b0ef2aa672e896773b81e567044c766178bf285f556b1dd57eb5e07e1e955a6d9b7fafe4f76b63ebb0ed7e9cad3dbaac7c8edec5d68be99dabca1ced5aabdc76ee762dd52f3fbf67fe42f3f55a5fd5a66af17351ca2eadc857cd71edefed67ef1dfefbfdf00fff518dd95c94e01ffed57b253ffc9ffbc03ffccf08fee11ffee1bfda0ffff01fd598cd4509fee15fbd57f2c3ffb90ffcc3ff8ce01ffee11ffeabfdf00fff518dd95c94e01ffed57b253ffc9ffbc03ffccfe841f8ef7d03963a94ded08fc9c665b9a2b6b5dd4fdabf57fabc9f9f4315985ead231753740154d6a6376fef935d36bdbdb17d6b7ce6677d2b73aacc2fdb93ea1e56f64d3d6770da7c47cd29aa415d72511df6dd7bab3ef887ff2897ed5be3e1ff5cf09f8f837ff8cfea87ff3c0efee13f6a837ff8f775d877efadfae01ffea35cb66f8d87ff73c17f3e0efee13fab1ffef338f887ffa80dfee1dfd761dfbdb7ea837ff88f72d9be351efecf05fff938f887ffac7ef8cfe3e0ff79f17f6d8bacc0e3ffad7d76bc3a2c6bbc8dcd0e926df3c0fbda4616cfe7ae80e5e716d5ab6af639d5e6a84369e7a6e26c6c96bb57b38ab3735573f1cf6a9fd519f1f3f46d7e2e11c4d17ed8f5cd208ee666fd545dcdd4abce756f7e2a5756a3eacb2eb568decadbd70cffb1e01ffee11ffefd7314b3beab9c37f07f3616fe5bb7cdf6c13ffcfbd82c77af661507fff06fe3e01ffe5b302f5bc75a23fcc33ffcc33ffcc33ffcc33ffcb7305756a3ea837ff8f7b159ee5ecd2a0efee1dfc6c13ffcb7605eb68eb546f8877ff8aff1ff322a464dd4275cfba2a4d1e47a637c8c8f8de2fc04fd64b3fafc1a441ed125d8ebf3edead91f28eb196da01dab7228106cbb8a89949d896c9fd5bc949f9a6f54436f4fbc97af39cba72053b0abbde9d564dfb37df479a2bdb131114bd90507fff00ffff06fbdfc18f887ffa856d56763a279293f35dfa806f83faf0bfee15fe5807ff887ff7e7df00ffff00ffff01f0bfee1df8f877ff8f77d36269a97f253f38d6a80fff3bae01ffe550ef8877ff8efd707fff00ffff07fdffc5f67d0f98d570be0db1494fe60abc31dc545b1b62faa415d3ef69f9f6374c87c0e5f97ef539b990164632b6d3e6724bf77115c3e268af5f1ebbb3aa011cc0a5c2bbf77de53e5b335ab33ebf73bca69e37d5e9fcf8fcd2e24558382d52a6342adb11fe7e713ed3dfcc3bf8f817ff857fdf00ffff00ffff00ffff61dfee11ffee13faa1dfee1dfcfc1e789e2e01ffee11ffe7d2cfcc3bf8d83ffb6c96f73c33ffcc33ffcc33ffcc3bfce69e37d5e9fcf8f857ff89fe1ffa59f989a888de96db8f28afa6d1edfe6c72a20a343afbc543d36b78a8de6165d60be2605766fbd7c7ce659b94c54ddd965a4605507393a230a52e511d516e58df2a9daa30b2e9a5b14abfca3b6e89c47fbe1e3a2f3adf65ced63c492cfa39efd3bfcb78dafaa43c5c03ffcfb7aac6f5437fcc3bff7817ff887ff78def00fffaa76f8877fffacea817ff887ffed3bfcb78dafaa43c5c03ffcfb7aac6f5437fcc3bff7817ff887ff78def00fffaa76f8877fffacea817ff887ffed3bfcb78dafaa43c58cf27ff5eebbefdef426d65ba0ca84fd02460b1129cbeb739d4d30385895f63d75f4d6a477f08e58efa8f66a7dd66f55b43e6a4dd53875a1f6c6f5e6a4fc7bf351ef9573a0d6ae72eeab9744af9ea8dfce29ab59b5c33ffc67f55bc13ffcc33ffc47def65d8d837ff8877ff8877ff85f05fff09f8d837ff8877ff8877ff857797d2e2bf8afe78deab7827ff8877ff88fbcedbb1a07fff00ffff00ffff0bfea8de67ff900562f30ebf713550bebd53b7c6a6cefc252fe7eb2ca6bf61067804563d55a448a72f572f8b6c853f5dbb81e5cd99c2aebafeac8ce9ff29a598fd139649746b5c69171aa2f9a9b5a879be4175f5603fcc3bf8d837ff887ffb6f1b1efaaae6c2cfc6ffbd798a81dfee3b9c13ffcfb1cbe2df254fd360efee11ffedbc6c7beabbab2b1f0bfed5f63a276f88fe706fff0ef73f8b6c853f5db38f8877ff86f1b1ffbaeeacac6c2ffb67f8d89dae13f9e1bfcc3bfcfe1db224fd56fe3e01ffee1bf6d7cecbbaa2b1b0bffdbfe35266a87ff786ef0fff4f97fe98bb2efd6343a2c3e71e5e2e85d44cabb7240b34588bcfd1cb38d50f35ee32b975d341f1567eb88364fc9fb4675648746f5f931ca23f3cc00f2fdeaf28f7246f3f279a239f93cd91952738d2e1035f728871d1779a8b3a7eaaeacbfaa1dfee13ff2587de01ffe7bf9e0bfc9f1f00fff510ef86fd203fee17f8d8d7246f3827f5d03fcc33ffcb7337ff887fff519fee13f12fcb76e3ef86f723cfcc37f9403fe9bf4807ff85f63a39cd1bce05fd700fff00fffedcc1ffee17f7dbe6ffe5fff09420548548835f0d0f8e22a8b12f97abf287734368a893c6dbedefcf74ae58bd63e82b67260a23e5f43962faa2dabab97cb7ad8dc519dbd73a900adccaf02dc687c651ea363d7986c2ebd713d4ee1bf9d8d53714709fee17f64ec1a93cda5370efee13faa21cb07fff01fd50bff73827ff81f19bbc66473e98d837ff88f6ac8f2c13ffc47f5c2ff9ce01ffe47c6ae31d95c7ae3e01ffea31ab27cf00fff51bdf03f27f887ff91b16b4c3697de38f887ffa8862c1ffcc37f542ffccf69f5bfb605fa855b0b5983ed4f35393bc6faa9c45151b696f59f57eff0db3a7cacf5549bdc03286ab77dd1b38df5738b9efdbb5a5bd5ef73f83d5cc745ebe173d9e76cad22af2867b4c791547cf4cb489d21156bdf33f07dfeec2c7ba9cbcdfb66e7268a8d6a5fcf993f076acfe11ffe7d9fcf05ffdbfcf00fff6b3bfc9fd702ff4dd6188d837ff857cffe1dfee11ffee11ffecfeb847ff8877ff85f6354ae2816fee11ffee17fedb7b9b2fcf00fff6b3bfc9fd702ff4dd6188d837ff857cffe1dfee11ffe9f0fff57efbcf3ce4db4686a02be2d5b6c15d38bafe4ecb56775643e6a6e15ff6c5ce651598bac3faba3ba9f97502fcf687fa56eb5e623ebd7dba7d1335ff5aee41a99bb6acfd6a6b5984bf887ff19c17fddbb920bfee1bf22f8877fd50eff351ff83f46f05ff7aee4827ff8af08fee15fb5c37fcd07fe8f11fcd7bd2bb9e01ffe2b827ff857edf05ff381ff6304ff75ef4a2ef887ff8ae01ffe553bfcd77c1e82ffd39f206c83092bf1a31be5fba38d586346376566316737c0b7576aaacc5f8d5f15ad95ea8b6a98556f9eb39e8b2abed54b203a8f76ad229fbd6741c58cf063dba31a7b63f7f4edf5827ff81ff55c04ffdbf6a8c6ded83d7d7bbde01ffe473d17c1ffb63daab137764fdf5e2ff887ff51cf45f0bf6d8f6aec8dddd3b7d70bfee17fd47311fc6fdba31a7b63f7f4edf5827ff81ff55c04ffdbf6a8c6ded83d7d7bbde01ffe473d17c1ffb63daab137764fdf5e2ff887ff51cf45f0bf6d8f6aec8dddd3b7d70bfee17fd47311fc6fdba31a7b63f7f4edf582ffa0df7e006bc47444958d3a12e268f3b20d1df1ee8d39e262ad5e3cfe9057bdb33159fed135ab5e24d1c1f7bf487af547b557cfc448ff685c45477a5da20ef8877f3b9f9ee07f4cf00ffff00fff0f2df88fbd7b63e03fce17f9c0fff15e97a803fee1dfcea727f81f13fcc33ffcc3ff430bfe63efde18f88ff3453ef07fbcd725ea807ff8b7f3e909fec704fff00ffff0ffd082ffd8bb376684ffeb564c98b52fcf599c07c16aedf33f476a8a3ca3d8f5df12b76e7a55eb985e4c149f5d467eb346f2a87735afca189f3faa4fe5b0eb1bf9abdcd9b35dc3ac7e0571b5665bbbf256f1914714d3f3e8c529ce7ced3666e45c57f28fd465fbe01ffe553cfcf705ff79eecc338a85ff7c0cfc37992b1aef3da2989e472f0efeb507fceb77f887ffbd82ffb6f1867ff88fdae07f1b07fff0afe2e1bf2ff8cf73679e512cfce763e0bfc95cd178ef11c5f43c7a71f07f2bf8d75ef00ffff00fffde77740cfc37992b1aef3da2989e472f0efeb5c753e6ff3a5ac8de818fdad562440722dab89105eec90339b2d1bdba2ab19531b6a61170b31cea12b27dd1dafb1a3c4095c35759db354f6faf555f36a67a56fcbcd6d80c5ee5e1c7f9f336a2e81787bf3094b25f52bd9cf619fe63c13ffc7b0f3f0efee1dfc6c27f1c07ff714df01fbfc37f3f07fcdfe589bce01ffe4704fff0ef3dfc38f8877f1b0bff711cfcc735c17ffc0efffd1cf07f9727f2827ff81f11fcc3bff7f0e3e01ffe6d2cfcc771f01fd704fff1fb73e2ffdac355b904bcb11de327e6273f02b31f1fc96f7e7681d8f868bc7ff6f554a4d62192adbb32d71e24515c659c8fefed976a5bc74517fc8c9fafdb9f3f557ba4e81cfaf393ad4ff60b2e9b43e61d719471999d97d13ae01ffe7d3cfcebf1f00fff959a2ae3e0bfefe7eb86ff38d76c1df00fff3e1efef578f887ff4a4d9571f0dff7f375c37f9c6bb60ef8877f1f0fff7a3cfcc37fa5a6ca38f8effbf9bae13fce355b07fcc3bf8f877f3d1efee1bf5253651cfcf7fd7cddf01fe79aad03feef9fffab77de79e7a6b21099a2451a6daff65f5295dcf75ddf11f9bcc7a8e791731ef1bac45aabcb7336875ad73d7e91ef687f35c722f81fcb0dfffb04ffe3bea3fdd51c8be07f2c37fcef13fc8ffb8ef657732c82ffb1dcf0bf4ff03fee3bda5fcdb108fec772c3ff3ec1ffb8ef687f35c722f81fcb0dfffb04ffe3bea3fdd51c8be07f2c37fcef13fc8ffb8ef657732c82ffb1dcf0bf4ff03fee3bda5fcdb108fec772c3ff3ec1ffb8ef687f35c722f81fcb7d647d57efbefbeecdd1a648eb526bbcc7d75f46973c038fe58c3da579ae5e6fcade3c67c13ffccf7ac1ffd317fcc3ffac17fc3f7dc13ffccf7ac1ffd317fcc3ffac17fc3f7dc13ffccf7ac1ffd317fcc3ffac17fc3f7dc13ffccf7ac1ffd317fcc3ffac17fc3f7da9357efd012c841e5a5c0208bdb9827f84de5cc13f426faee01fa13757f08fd09b2bf847e8cd15fc23f4e60afe117a7305ffe84dd37543e89188cb17a13757f08fd09b2bf847e8cd15fc23f4e60afe117a7305ff08bdb9827f84de5cc13f426faee01fbd693a7d00eba91cfc87a873cdc9e5f0b07a13d7ff8839573ce0bf9f13fe1f56f07f390ff8efe784ff8715fc5fce03fefb39e1ff6105ff97f380ff7e4ef87f58c1ffe53ce0bf9f13fe1f56f07f390ff8efe784ff8715fc5fce03fefb39e1ff6105ff97f380ff7e4ef87f58c1ffe53ce0bf9ff352b94f1fc05abefaad52c44c5b4555af5e9d7eec11f5ac397deecc7b26efacdf916b7edf6346fcfcd71346cf59ae28ae5263367e365fcfaf7adeb31cd9d73ada988ad74c5b45f00fff3dc1ff98e01ffe67f3ddd718f887ff112ff81feb9bcd775f63e01ffe47bce07fac6f36df7d8d817ff81ff182ffb1bed97cf73506fee17fc40bfec7fa66f3ddd718f887ff112ff81feb9bcd775f63e01ffe47bce07fac6f36df7d8d817ff81ff17a8efc5fbdfbeebb37ca60ddb83d87a054c03de478488dceaf177f9febf518f626aaa1d29e9de3c772ee1ee3f987ffe304ff97a901fe2f27f83f4ef07f991ae0ff7282ffe304ff97a901fe2f27f83f4ef07f991ae0ff7282ffe304ff97a901fe2f27f83f4ef07f991ae0ff7282ffe304ff97a901fe2f27f83f4ef07f991ae0ff7282ffe3d49bdfb51ab06af4e2985194c3fbd9f7d95c5545fe3379ed659079ae3f7b6b5e5daf8a7a6b6a732dfda3eba2dab336ffd3d6e0c745eb60dbd767155b3ddbd9bc556cef79b68e8ad711827ff857b9e0bf16db7b9eada3e27584e01ffe552ef8afc5f69e67eba8781d21f8877f950bfe6bb1bde7d93a2a5e4708fee15fe582ff5a6cef79b68e8ad711827ff857b9e0bf16db7b9eada3e27584e01ffe552ef8afc5f69e67eba8781d21f8877f950bfe6bb1bde7d93a2a5e4708fee15fe582ff5a6cef79b68e8ad71182fffbe5ff3a3ad8ebc1530750bdab851c39bc51e1f6fd884da88cf3b96cfbac77e6a9d64f5d463df5f62abbfc2a17bf3a4cd9de67175f067a346ecffe1ff1cba3275ba3ff4550fd65d48bf1bf14b3d8cc3f3a5ff00fff4af0df17fc6f73c07fdf13fee13f12fcdfbdc37febe6877f2df8d7bef0af73c3ffb64ef8cf05ffdb1cf0dff7847ff88f04ff77eff0dfbaf9e15f0bfeb52ffcebdcf0bfad13fe73c1ff3607fcf73de11ffe23c1ffddfb73e4ffc55b6fbdf5ad764f520b380352b4d933debdd84a7b259f8fa90219c55720f06d111023a0ac9e7efe23e333df99f16a6db3b9ee51b40f365f2fc7ecd9cfbc66e28fc8bf3737fc6bcf9e37fc6fdbe07f2c1efee13f6b57827ff89f15fcc33ffcc3bf7d867fedd9f386ff6d1bfc8fc5c33ffc67ed4af00fffb3827ff8877ff8b7cff0af3d7bdef0bf6d83ffb178f887ffac5d09fee17f56f0bffd79dd3ae6ebbb3ad0515c16b36a5d440b73059ce81380d1a70fb339d8cd54beb6ff2ab964a24fb146b52b68ad8fda50db97adb1edf773b29f6255febe5dcd57ad97f755fea3edd93aa89aec2f866c4fb33556b9a3da7c3deb1af83dae4a9d4dd55fadb70535fbf947b55472c3ff796ef8d7f39e69cfd641d504fff0af62b239c03ffcc37f4bc765b9e1ff3c37fceb79cfb467eba06a827ff85731d91ce01ffee1bfa5e3b2dcf07f9e1bfef5bc67dab3755035c13ffcab986c0ef00ffff0dfd271596ef83fcf0dff7ade33edd93aa89ae01ffe554c3607f8877ff86fe9b82c37fc9fe79ee1fff40d58d9615445a9b6ac003f193f3e1b136de848be1ec0bd835f8d55358fd4e1e35b90d31fdad175559ed53ad4fc7afe59bf3d3bd1c5981dfecad9557ed19af518e89db9ea7e54f2441781fae96bf0cfca5fc56735f918f8877ff83fef87ff7c4eca331b03fff01f9d63ef53c9b73ec33ffc7b4fffdc7b877ff8f7fdf09fcf49796663aeaee0ff0afee11ffe3739e11ffe2b6757f9c13ffc7b4fffdc7b877ff8f7fdf09fcf49796663e01ffea373ec7d2af9d667f8877fefe99f7beff00fffbe1ffef33929cf6ccc73e7ffc537bff9cd6f558a880aaf5c189518d5660f615687cd136d9a5a04e5e10f6696cfc6cda8b7cefe72f2711198d965a6ea577dd17e47eb5859832a3c15cfe8ccac3fb30bbb3207bf86517e9fb77241673ebd7e7f31afb556d4633ceb83ff6dac8d9b11fcd7e27b5eaa0dfe757ef55ce983ff6dac8d9b11fcd7e27b5eaa0dfe757ef55ce983ff6dac8d9b11fcd7e27b5eaa0dfe757ef55ce983ff6dac8d9b11fcd7e27b5eaa0dfe757ef55ce983ff6dac8d9b11fcd7e27b5eaa0dfe757ef55ce983ff6dac8d9b11fcd7e27b5eaa0dfe757ef55ce983ff6dac8d9b11fcd7e27b5eaa0dfe757ef55ce983ff6dac8d9b11fcd7e27b5eaa0dfe757ef55ce983ff6dac8d9bd132eef40d58b62182dac7f48ced84aa0b320375af06fbae16381b738446d72b82d9fbf88b363b2895c3e76b99d5e8f8ea01be2a5c44b6afe71fed7d765164b554ea8efab37dade6ac9ce5e86cf95ae0ff38c17f3f1efee1df3fabf711c17fdbf8c0bff6b3eff00fff470afefbf1f00ffffe59bd8f08fedbc607feb59f7d877ff83f52f0df8f877ff8f7cfea7d44f0df363ef0affdec3bfcc3ff9182ff7e3cfcc3bf7f56ef2382ffb6f1817fed67dfe11ffe8f546fbdaed7a055b6c0f5d92fa29f44b4d9ea90a8f6288f1fe72f2affae64fb6c5e0fa9cdef0f5ed4a66a519eaac608b0ea9adbb80826ef93416b7da30bbb07750693ca67ebaf80bd8ef3fbd89bbfaacfc7d9d828deaf5165beea9cf99c6a5fecfefa795414b1a4bce01ffee11ffe7d1ffcc37f14efdbe01ffea339d836f86fe17bb616f07f370efefb82ffb6e9837ff88fe26c6c140fffdb18f8877f3bbe12ebeb837ff8cf6a867ff8b7e3e0bf2ff86f9b3ef887ff28cec646f1f0bf8d817ff8b7e32bb1be3ef887fface6e7ccff4b9bd876ae6dfea75a385b8c82411d22ebe7f34439548c5a5cef1f5d3c6a8c5d8f0c325f8b3d5cea60fbfcfe304435f7d6dc8eb7fd3dc8a23d8dc0c9e6e10fb01aeb2f76754eb21c6a5cef52f297a5dfb3e8e28b80547bab2e8fe80c649769b6273e26aad7b647eb30ea0bfff00ffff00ffff01fcd03fee1df8e53fe3e1efee1bf22f86f9b98a85edb1eadc3a82ffcc33ffcc33ffcc37f340ff8877f3b4ef9fb78f887ff8ae0bf6d62a27a6d7bb40ea3bef00ffff00ffff00fffd13ce01ffeed38e5efe3e1ff76dcb56d5007532dba3d94b6ad9754fd8c266c7f2a4fdf9e5d240a1a0597fd696bcb2e8bde8513f9aababcfcf8e8c2f1eba0f6e7a678e1293f751e1410d11e2bd86cadaa467ba0ed3cfc38dfe7f3fa79659760556a0e8aa1ec9761c48f8d8f2e337511d9f3eccf9eafd3d662e703fff0affce0bf853ef00fffea1dfee11ffedbc607feb7b1f00ffff00fff5ef00fff5ef0df643bfcdf3dc33ffcc33ffc2b9f8ae03f17fcc37f96d78e837ff8877ff8877ff8877ff8f7b12f3d48d9a1f709d47b56a08a8d16d7e6cfde959f3f8c91b76a8f2e954c6a4dd45ca3755673f2f38b0eaccdafe223efa82eefe7bda2b964b52b552e49fb9e9d835e2d11dcbece082615179dade897552f97aa4ffde2aa9e5375be542cfc6f6b837ff8efc5c13ffcabb1eb3bfc6ff358c13ffcfb7ed506fff00ffff01fbd2b3ff8877fe509fff00fff7731153ff8d771f00fff6aecfa0effdb3c56f00fffbe5fb5c13ffcc33ffc47efca0ffee15f79c23ffc3f36fe5f560fad358992aae72cc6f6f540b675f978556b05623fafe8b04460f52ec00a242abf9a8bdf5cbfa60aaaecd9e657f3b6f353bebe3f9a6bef6c45735663549c5feb688c6f53e3a37e5fb7af3d9a9faa2f3abf6a6cc4983d036a2d7a7e7e6ef00fff514dbe5ef887ffac36ff9cc5d83ef8877ff887ff9ee01ffee11ffee11ffe7d5ef8877f9fd3f6c33ffcc3ff766ef00fff514dbe5ef887ffac36ff9cc5d83ef8877ff887ff9ee01ffee11ffe9f0bff57dffdee776f54313eb057b04ae67d94d4e4d4659479651b375243b658ca33033c6a8b7228c87a73cf0e93f28f2eeeec82cafc23a99ab37d557d6a8c1f17e5b671ca2ff3565e51bd59accf91f1513933deb727b5d6b64fd5adfab318f887ffa82ef8877ff8dfb6c13ffcab5a7c7ef8877ff8877ff8877ff8877fefdb13fcdfd555ad01fee15f7945f566b13e07fc6bc1ffd613fee11ffee11ffee11ffee11ffee1dffbf604ff7775556b7808fe371fc0f20bdb6b5745659388c0ab1ecc4a0e5f931de3e37bde1978bd4b65646da2039dcdad37179f27cb91ed7be4b5b6a9fdeaadf7e80553b9507a974ce451cd9bb5f9be08e8683d5b719ed99eabf3e96b8af26639b276dbef6b80fffadac03ffc673566b5583ff8dfd664c7c03ffccfe6cdda7c1ffcc3ffc8dac03ffc673566b5583ff8dfd664c7c03ffccfe6cdda7c1ffcc3ffc8dac03ffc673566b5583ff8dfd664c7c03ffccfe6cdda7c1ffcc3ffc8dac03ffc673566b5583ff8dfd664c7c03ffccfe6cdda7c1ffcc37f656d4e1fc0aa9ada815942dba7269b2d5ae45bbda0b29aa298de011dcdbd3e2baf2876068a8ab2b51cb9d4fc415dfbf6001b1df68a4f659fb3bc333159fb3a07155be549e51aad7784b1350efee11ffeeb3955fb3a07150bffdb9c95f9c07f2bf56782ff56f21bc9a9dad739a858f8dfe6accc07fe5ba93f13fcb792df484ed5bece41c5c2ff3667653ef0df4afd99e0bf95fc4672aaf6750e2a16feb7392bf381ff56eacf04ffade4379253b5af7350b1f0bfcd59990ffcb7527f26f86f25bf919caa7d9d838a85ff6dceca7ce0bf95fa33c17f2bf98de454edeb1c542cfc6f7356e603ffadd49f09fe5bc96f24a76a5fe7a062e17f9bb3329f3781ffab77df7df7a67280ab07bd0254d63f1a57d1e8c657db55bfdfcc55a380f4941d4e15bbe6cc0e53d5c76aef2f06fbeefd7a179bf7533545edde6f0658dbd7f31c3dcfd55f98235e37c58bb2574f2f6e11fcc37f26f8877ff89f17fc6ffd544df00ffff00fffbeddfbc17f2bd7d38b5b04fff09f09fee11ffee705ff5b3f5513fcc33ffcc3bf6ff77ef0dfcaf5f4e216c13ffc67827ff887ff79c1ffd64fd504fff0ff98f97ffd2708a343e50d8fba387a939d5df4bd076acf3cab60563d2ea123fcb30b49c5f9e7286624ffa2ca055519afe6e5fbab17adca3533bfea45dbfb059379f772fafcd51c95fcf0dff7b884e01ffee11ffe8ff0807ff81f15fcc37f1433927f11fcc37f347e360efeeb1ef00fffa3827ff88f6246f22f827ff88fc6cfc6c17fdd03fee17f54f00fff51cc48fe45f00fffd1f8d938f8af7bc03ffc8f0afecfdb5eff09c211387a93b493a8c44645570f577421542fbc9ebfca551dafe2472fe16c3eb3177d2fa76d5fe421ece5adac8befefedf75e38ad47f43e1b5be9afd6b7a8fa8b6dc6bf7a5ee03fce551d0ffff03f5adf22f8877fff0efff0bf370ffc6fc7f6fc55aeea78f887ffd1fa16c13ffcfb77f887ffbd79e07f3bb6e7af7255c7c33ffc8fd6b708fee1dfbfc33ffcefcd03ffdbb13d7f95ab3a1efee17fb4be45f00ffffe1dfee17f6f1ef8df8eedf9ab5cd5f1bd3324ff04616551aa09b31895a70784cd3d73b9792febd7f3882e8dd14dcf6a9ab9387d4d7b7cab07328a3b621d16f5cee09e8ba9778e663c4754a9bf7719557e49dbb6755ca516f8877ff81ff31c11fcc37fe40fffe73efe59bdf7da559cad1dfee1bfe201fffd38f8ef0bfee13ff287ff731fffacde7bed2aced60efff05ff180ff7e1cfcf705fff01ff9c3ffb98f7f56efbd7615676b877ff8af78c07f3f0efefb827ff88ffce1ffdcc73fabf75ebb8ab3b5c33ffc573c2ec1ffeb3f4118259c9d602ffe3e0f76b5ff923ebd833e9ae7a81af76aef1c67f2ccf4efcd71f4feedc95bf539f297a07a1f1d5fed9b15fcd7fb8f12fcc37f757cb56f56f05fef3f4af00fffd5f1d5be59c17fbdff28c13ffc57c757fb6605fff5fea304fff05f1d5fed9b15fcd7fb8f12fcc37f757cb56f56f05fef3f4af00fffd5f1d5be59c17fbdff28c13ffc57c757fb6605fff5fea304fff05f1d5fed9bd593e43ffb00d643686663eeebb2395297bab88e5e8b4baeed43eedb439f9987caffd85981ff16b68dfa1e21f87f5ef9e1ff7108fee11ffeb782ff16b68dfa1e21f87f5ef9e1ff7108fee11ffeb782ff16b68dfa1e21f87f5ef9e1ff7108fee11ffeb782ff16b68dfa1e21f87f5ef9e1ff7108fee11ffeb77ad3f8bf6ef7ac25f1d15ef6221b1d3b127b64edf6c0a8c3337ba08e3e88973cd8fed3954a47ae7994fb21543db347cfffa1e70dffb7827ff85f04fffbbde07fffb8fbf28bbce15f0bfefb5ef0bf7fdc7df945def0af05ff7d2ff8df3feebefc226ff8d782ffbe17fcef1f775f7e9137fc6bc17fdf0bfef78fbb2fbfc81bfeb5e0bfef05fffbc7dd975fe40dff5af0dff782fffde3eecb2ff2867f2df8ef7b3d65fe1fdd376021841042082184104208218410420821841042082184d053d1d037603df4a7e7d0d3d1c859d973ae3893f727d61a5505ffcf4fac35aa0afe9f9f586b5415fc3f3fb1d6a82af87f7e62ad5155f0fffcc45aa3aae0fff989b54655c1fff3136b8daa82ffe727d6fa785d478bbab45ff2ebec5e7f05d704a88fed201c55cfa5e635ea6bf77ea6a665cc25be0e4ed572c4d7c23d958b2563f5129ef05f13fc6fc7c3fff1827ff8bf0f5ff87f9c827ff8bf0f5ff87f9c827ff8bf0f5ff87f9c827ff8bf0f5ff87f9c827ff8bf0f5ff87f9c827ff8bf0f5ff87f9c827ff89f11fc8ffb3e46c1fff3e3ff3afbdb9bcb3f6bde3becbe904a611580569f51d8d643141da6bd1b19d535eb9bcd6ff6225c7d47e6bfeefd4cae75bc6f1b191f69e4ac8cb48dec9fbf44462ec54b5e1cbd3aa2cb0bfee705ffda0bfee15fbd47797a827ff8b7bea33ef00ffff00fff511bfc6f05ff357f5517fcc33ffcc3ffa8e03f17fcc7827f3d0efee11ffee11ffee13f6a83ffade0bfe6afea827ff8877ff8afe8c55b6fbdf5adea811b296a5447d6309b67664cb6b1d55c47cdcf1eb4d11a32cfec79cd390a87f7ebb58fc4467d0ac4defa3cc4d9cb2ef6283e5affca5ac2fffc18f887ffaae07f6e1cfcd704ff791ffceb36f89f1f03fff05f15fccf8d83ff9ae03fef837fdd06fff363e01ffeab82ffb971f05f13fce77df0afdbe07f7e0cfcc37f55f03f370efe6b82ffbc0ffe751bfccf8fd9cbffe9035823a633ca36bc7a18aa87f2c8cd5afdaa97cb9e75eb5d0e958bc27e6251d51d5dce3397f5cce579f4b98af4182f183f365bbfe813cbd9b82305ffe77ef0aff355dbe17f3b16fefb7dbe1dfef3f1f0af3de17f4cf07fee07ff3a5fb51dfeb763e1bfdfe7dbe13f1f0fffda13fec704ffe77ef0aff355dbe17f3b16fefb7dbe1dfef3f1f0af3de17f4cf07fee07ff3a5fb51dfeb763e1bfdfe7dbe13f1f0fffda13fec704ffe77e97e4ff5a05a984aaadb2786a02f62bea6c9f7df7d0faafc58b36b1f77560bd4552792b5273b1e37b79fdfcec58ffec3d6ceeacde156cfb738fd49cd4fa67b565974d2f5724f5d579d139e8e5affef2a8d4677355e7adeab5ef596cd513fee17f46f00fffd9fbda06fff0affa6c4d33b922c13ffcab3ef51e79c17f5ff00fffd9fbda06fff0affa6c4d33b922c13ffcab3ef51e79c17f5ff00fffd9fbda06fff0affa6c4d33b922c13ffcab3ef51e79c17f5ff00fffd9fbda06fff0affa6c4d33b922c13ffcab3ef51e79dd17ffd751e22cd1aaecc2c8dab2f1d961b035f62ea4e8a0478be417cc8fcb36ce5e6a3e4f065c7658ec1c4655bd0c549b3a07b6167be1f70ebbcd93ad5f05ee998b328b8f724679d5b9f017895fa75eadaa3d3ad7958b60f4e28dd8aa8cf5e3d57bd4968d87fff379cc08feb587f7531ef00fff6a1cfcc3bf7db71ef0afdfa3b66c3cfc9fcf6346f0af3dbc9ff2807ff857e3e01ffeedbbf5807ffd1eb565e3e1ff7c1e33827fede1fd9407fcc3bf1a07fff06fdfad07fcebf7a82d1b0fffe7f39811fc6b0fefa73ce01ffed538f8877ffb6e3de05fbf476dd9f8378dff973e401d2adfef134513c93c6c0efb1c1d74ebe57dbcd422abc55375a85ad418351f1fdb8be979fbbec847f54507ad32677ff022dfa80e5553b496a3a0446d3d70b2da6c3d5e0a7a0f97af5971e263a239abcb3d53ef3252b9b34b08fee11ffedb663cfcc33ffcc3bfcd05fff00fffdb3a544df07f1e13cd19fee17f1de7fbe01ffe557df0dfe43cd51ad876f8877f5f8f17fcc3bf1aa3e6e3637b313d6fdf07fff0afea83ff26e7a9d6c0b6c33ffcfb7abce01ffed518351f1fdb8be979fb3ef8877f551ffc37394fb506b6fd31f07f1d15d1834941d9dbac28873de4bd43e3174af9db76b58851bef5ddcea707c01ae79fed65d6ab618df76d7e4ebd4bccb665f351fbe0f37aff6ccdfc583f171fa3c0f4f27eea8ca9355317a5f589f244eb9f9dbb1ea4d9d98df2282fb557fe3daad9fef3717e3cfcc37fe40ffff00fff5af00fffb60ffee11ffee11ffee11ffee11ffe5b9a03fee1df0bfee1dfcfc5fe54f1f00ffff00ffff00fffca17fee11ffee15fe5817ff87f08fe5fbcf5d65bdff226de48994445fb3eb560ea2250b1bd8bc13e475eea2078af5efeca986c0db2e7c84b8d896a51f3f3ed998f3ab8be3f1bab7cfccf5e7d59ae5e5b658d463c3ca4d939543f6d7f7661a898c83f8a5179a3beec2cc03ffcc3ff36be770ed54fdb0ffff09f3df7ce19fcc37f16a3f2467d57f02f6bece5af8cc9d6207bee9d33f887ff2c46e58dfaaee05fd6d8cb5f1993ad41f6dc3b67f00fff598cca1bf55dc1bfacb197bf32265b83ecb977cee01ffeb3189537eabb827f59632f7f654cb606d973ef9cc13ffc67312a6fd47705ffb2c65efeca986c0db2e7de39837ff8cf6254dea8ef0afe658dbdfc9531d91a64cfbd7306fff09fc5a8bc51df15fccb1a7bf92b63b235c89e5f7f00cb1b2f5210659bd73b906a9c8aed1db88a9ff5e9d5ec173e8a55efd6a737dfc84b1db66c2db3c314d51f5d88ea00fb3c6a5c363fe597ada1ad3d82e9eaaaff0bc4fa44e769640eaa5dbd57f63df3e9b547b1d145a0fca2b5f59ef627fceb36f8cfe707ffadeb3fd21ec5c27fec677de01ffee11ffe7dac8df1ed2a56bdc37f9ecb7b65eff00fff7e3e23827ff8877ff8f73ef00ffff00fff3ed6c6f87615abdee13fcfe5bdb277f8877f3f9f11c13ffcc33ffc7b1ff8877ff8877f1f6b637cbb8a55ef7bf9bfb6836d21ebb39f807f8ffaa2c2fcbb82de2f48b4a1bd09db03e42f900c305fbb87575d46d1a1f6defe5d6d7e0f10bfced1bc7c9b5f0f1ba7724617b3aacd8ef1312aa7ba103df0be76355f9f53f9abfdf23e1578fcb808dc0cc6ca19ce6ab3b9d5e5e2e7adf65e9d33f8877f9f13feb7827ff8877ff857e7d4c744e3b3779f23f2867ff8b735c1bfae03fee11ffee11ffee11ffee1dfd606fff0affce0bf6de27bf3b1637c0cfc3719a7de7d8ec81bfee1dfd604ffba0ef8877ff8877ff887ffc7c8ffd5f7bef7bd1bb50876023d795023a0b2cd5407239b44e5f0f52e892a74914734cf4abff551b554ea5f151d84911aa2fd515275a9bdb775f83e752eb2f589f266f3c8e69abdab5cd90566ebebcdbbf21ec54497e7e879e8ed33fcf705ff2d9d07fcc33ffcc33ffcc33ffcc3bfef837ff8877ff8877ff857cf4af07f3e6615fcb7d457c564ef2a17fcc7827ff8877ff8877ff8877ff8877ff8877ff887ff3e7ba70f60f59264876a8d53fdbd057f5d440074b680954b206baf1eae487e6dac7f6f0d7cfc48be5e5f0f725f7b565bef32526d6a5d220f9b47ad656f5ed125aeeaefed7705689bbf57c3c82550b92447ea3e2a1efe63c13ffcc3ff5d5c3617951bfee1dfe6817ff857b9b3ba54bbf2837ff887fff178f88f05fff00fff7771d95c546ef8877f9b07fee15fe5ceea52edca0ffee11ffec7e3e13f16fcc33ffcdfc5657351b9e11ffe6d1ef8877f953bab4bb52bbfc7ccffcbcac1b4e6d12432607d81aa2d0257d5a2f247feaa7df5e941d7cb1fc5adedbd7a555bb41f57c9851ae5f67bb6b6458057eae8d5171dca756c4fea1cd971d9798cdea3bc23bf48a28bdfe6b80a2ed0688f979f954b2963b4077d8f11f8877f9f23ab03fee15f8d85ff6d3bfcc3bfca03ffba7ef8877ff8dfce03fecf6b51f9237fd50efff0bf7ac13ffcab3ef8877ff8877fd59779db76f8877f9507fe75fdf00ffff0bf9d07fc9fd7a2f247feaa1dfee17ff5827ff8577df7c5ff7596c82f6eb438190c9571be6dfd178d89f2adb97c4ebbe0515e5f93aadbd76663ec73ef50f87a22f07c8c5a4705700f2aefadd483c1e7b33ffdfea976ff8b20bab8d658fb53e58fd6c0fed2b1fdcacfe7ca80f36720fbc5a36af631eaa2577b9eadb9f78f9e6d1bfceb9ae01ffee11ffeb3dae01ffee11ffefd33fcc33ffcc37f940bfee1dfc7c0ff5d1bfceb58f8877ff8877fdf0ffff00ffff00ffff00ffff00ffff09f3ddb36f8d735dd17ffa73f41d82b4c1d705fac5a0c7fc0ec21f5b0a9027dbcf751fe59dd19e851ad597dbeaea8e6a8a69171d9585f4be4dbab39aaa1ba1ed1bea81c190491a21ab3f7de7c7a73cdce9b1f6bdb956f149fb1a56a8afc7ae7ce7af7bc466a847ff8f73e591ffc6fe7d41b0bfff00ffff00ffff0df441d7e1cfcc37f5617fcc33ffcc37f6f5ca546f8877fef93f5c1ff764ebdb1f00ffff00ffff00fff4dd4e1c7c13ffc6775c13ffcc33ffcf7c6556a84ff79fe5f7f002b2a3e03a8b2f023ea8dcd8099d5cc6519c54763ab3566fed9a55155c5a37a1867a0ad1ee628a6079ef5b27ebd8b4d8d55f9aae7df8faff8faf6ca99c92ec3cafa5738867ff8877ff8877ff8877ff887ff16c64763e11ffe7bbebe1dfe75ff5a4b754c25a7f78cbce11ffee11ffea3f8682cfcc37fcfd7b7c3bfee5f6ba98ea9e4f49e9137fcc33ffcc37f141f8d857ff8eff9fa76f8d7fd6b2dd531959cde33f2867ff8877ff88fe2a3b1598da70f60b5c1827b8b34ba8833ca408ce2f66ed68877a59e286e04c2995a47e366ea18b9a047353276f4bccd9ccfea5e8f8c1fe1a872891c7596e0bfe60dfff00fffba3faa754f1df05faf75340efee7ea817fdd1fd5baa70ef8afd73a1a07ff73f5c0bfee8f6add5307fcd76b1d8d83ffb97ae05ff747b5eea903feebb58ec6c1ff5c3df0affba35af7d401fff55a47e3e07fae1ef8d7fd51ad7bea80ff7aada371f03f570ffcebfea8d63d75c07fbdd6d138f89fab07fe757f54eb9e3a9e2affd76d50bd09a883647f2efdf6d0ac3f7d7ca50e1b1bd5350ade489ff556f5a871be1e55dfda76e461e9290223ba1cec4fa5759fa3b819cfa8e6accf9fb79e6cdd590e5befecd9db7319f93a7b733cea2cc1bff686ff73c13ffc2f82ff788c7ab76df09f0bfee1dfbe477115c13ffcdbf85e0ef8aff5c37f3c46bddb36f8cf05fff06fdfa3b88ae01ffe6d7c2f07fcd7fae13f1ea3de6d1bfce7827ff8b7ef515c45f00fff36be9703fe6bfdf01f8f51efb60dfe73c13ffcdbf728aea207e3ffbbdffdee8dead8a3ec50eef5ccbc8fcc5bf57ae87a2eb1d6333aaa8ea3e7b3d72ffa0574a935cf7ee18d9cc70a2fa3de55c1ff98c79e7ae0ffb27ef03f2ef81ff3d8530ffc5fd60ffec705ff631e7bea81ffcbfac1ffb8e07fcc634f3df07f593ff81f17fc8f79eca907fe2feb07ffe382ff318f3df5c0ff65fde07f5cf03fe6b1a71ef8bfac1ffc8f0bfec73cf6d403ff97f583ff71c17fdde33ada6c65d62b645556d0ac56cf6c42476e7ad5eb52f5f8b5aa405959df3d7b9079ae7564fe95dcd535abcea3e7d7f3e95dbe23eb596148d5bbb48d9ec70a2fd965af6233c1ffbc8712fcf7735604fff0bf27df9e38f8dfc6f504fff0af04fff03fea55f1837ff8dfe305fff05f15fcc37f55f0dfcf5911fcc3ff9e7c7be2e07f1bd713fcc3bf12fcc3ffa857c50ffee17f8f17fcc37f55f00fff9b98ef7def7bc713b953d141ecf53d268dd4f990738a7267708ed65a9ddf73dcdb87d253594b25f8bf3fc1ffb8e0ffb282fffb13fc8f0bfe2f2bf8bf3fc1ffb8e0ffb282fffb13fc8f0bfe2f2bf8bf3fc1ffb8e0ffb282fffb13fc8f0bfe2f2bf8bf3fc1ffb8e0ffb282fffb13fc8f0bfe2fab3789ff47f9012c841042082184104208218410420821841042082184107a0aba6e937a8c9f427baa9ff8f37aaaf3f05f957789798c7ace7c459c9fc763d09e757d0cfb701f82ff8715fc5f4ef0df17fc3face0ff7282ffbee0ff6105ff9713fcf705ff0f2bf8bf9ce0bf2ff87f58c1ffe504ff7dc1ffc30afe2f27f8ef0bfe1f56f07f39c17f5ff0ffb082ffcb09fefb7a8adc841fc0ea4d26fb3abaea421cb160d663ade9a9abf7f75fa3b699988a87ba90d677dbbfd46d6bbfc47ef8af76eb5db0590dd9df2dbdd459ca6acfe692ad6b6f3d66e602ff0f27f88f05fff01f79c0ff784cc503fe8f13fc8fc7553de07f3ca6e201ffc709fec7e3aa1ef03f1e53f180ffe304ffe371550ff81f8fa978c0ff7182fff1b8aa07fc8fc7543ce0ff38c1ff785cd503fec7632a1ef07f9ce07f3caeea01ffe331150ff83f4ef03f1e57f5788afc1ffa2708a303df03e1b1e821ebb4b9a3e76ccc887fd5e3bed7a392af378fbd67d0c76580abd8fb3e3fb3fb77895ae1ff98dcf03f1e03ffc7f51d5d0bfc8fe586fff118f83faeefe85ae07f2c37fc8fc7c0ff717d47d702ff63b9e17f3c06fe8feb3bba16f81fcb0dffe331f07f5cdfd1b5c0ff586ef81f8f81ffe3fa8eae05fec772c3ff780cfc1fd777742df03f961bfec763e0ffb8bea36b81ff58d76b625b8452d45e91bd4ceccf51eda9a1e21b81a762559bfd5999ab8db3b9fd73e4d1ab37bb3c2a1e2a7734b7ca19f2e35ba1c6ac2695a777d67afbaad66369f36b612ffcc8cfcf356b9f557661f4f656f5c1ff795b14abdae05ffb44e35ba146f8cf05fffb05ffda03fee1bfd75e11fcc779e11ffef708fefffff6de6d3b92dcc8b6f564d52f77ebd60ff5c1924a9796f498e7847a43b4305f76011c110c32e71aa39aee80d932031c13ec078ed475c1bff6807ff8afc63b82ffb82efcc3ff15c1ff75c1bff6807ff8afc63b82ffb82efcc3ff15c1ff75c1bff6807ff8afc63b82ffb8ee9b2d3c0e9537b131995915d3012c1a1bf91db066a0b320596fff316cacf71863f6e7f054f1763d917c9cbab8a28bd01ea80c783fa72e14dfa3bf8cba7b1d796697988d9d81cf7e53d57f0588bf78b3be7c8e3d27363f3b3fd9bbf2efc45752e710fee11ffee1dff6eb63323ff8877ff887ff48f00fffde13fee1bf3336f2e1ffbe271f0bfff0df11fcc33ffcc3ff4df00fff36de0afee1dfc6647ef00ffff00fff91e01ffebd27fcc37f676ce47f35fe7ffacd6f7ef38b2f14299b5f3d187e43af1e30b5798fc8b179d9c7efe4fb5c7f28545c7671a983141df4aa8ef2ecc0a66a56fd64b97eaedbc3d1e8273a73d179cff6bdbb2fdd5fbe9df12bb9fe1bc33ffc7b1fef09fff09ff9a8f7cc53fd9cf5b1f1f00fff1d4ff887ffd51c9b07ff47da87afa7e6aa5c3f07ff7b72e11ffee11ffec733fcc3bff7f19ef00fff998f7acf3cd5cf591f1b0ffff0dff1847ff85fcdb179f07fa47df87a6aaecaf57357f9b71ef0fffe0cfff0ef7dbce757e0dff603ffefcff0ff5cfedfec40d5447468d4e659d0b2cd1d7163213eafaa59f5787bef421f7964f29e2ab7dbab1f1fbdab35657ba3c6ed41f17b6bebf86fa8a0f3e764e65b8cd8e81748f6dded5cf58b25ebc7f6a0c695d42fb5c8bff2c87ec92956fc1e67392bbf1855fff0df13fcc3bff7af3ce0ff38f50bffe771f88fe7e05fe7c03ffcc33ffc77fac8fa87ff9ee01ffebd7fe501ffc7a95ff83f8fc37f3c07ff3a07fee11ffee1bfd347d63ffcf7f46cfe57cfe10cff9ed11dfc8f7ab3fcabb3be837feb01ff47da979a577ef0cfefffac0f7effc3bfcffbd1f9ffe9bffeebbf7ec936261af787c003ae7cd47c06b0aaaf9ed5bb9ff33d46311d7fbfbe680d6abd915f179eaaef99b9cc73fcf4df75a5c7a846f63d66bd3aeb999d5bc957df75668dfe92f6bfb0aa5f4c2bfb09fff0afe6e07f3e1ffee15fc5451ebebf59c13ffcc33ffcafcc659ee327fcc3bf1a877ff8bfa22be75579cd30da9d5bc9877ff857719187ef6f56f00ffff00fff2b7399e7f809fff0afc6e11ffeafe8ca79555e338c76e756f2e11ffe555ce4e1fb9b15fcc33ffc7f5efefffd2f60f94df29bdffdc0234f6dfa18f7eff667f4817c2fcacbd7527d457951cfaa571b63bdd481b5ebf31fcbae597957cfaa8e9fb7e3febfe81bfbbec7cff19fcf51f976ce8eabb3e0c1edac57c544672ecab37da89e236f1b5b5d5c76df6680ae2e3f9f17f1138d79c13ffcc33ffc6775e01ffee1ffde0bfeb50ffcdfe7c03ffcc33ffcc33ffcfb5abe1efcc33ffc9ffdb3bab6d76e9eed43f51c79db58f8d73ef07f9f03fff00ffff00ffff0ef6bf97af00ffff07ff6cfeada5ebb79b60fd573e46d63e15ffbc0ff7d4ec6ff9b2fe41b880e852aa8bcfc780794eca0f945471f585d462a4f7d647fe1742f82e8d2511fab3ac8515f591feac2f597a88ff3deea2cd8ffec58d69b3d7423c7ae4b81eefb50fbd0d993e81c64672ffb1eea3bfbcbd5ef8d1f8b7af1df28622fea5d7d0b1ba3be43b63edf2ffcc33ffcc33ffcc76bf23ef00ffff01ff753c5c23ffcc33ffcab5cf88ffb80fffbdad11a8f03fea3f5f97ee11ffee11ffee13f5e93f7817ff887ffb89f2a16fee11ffee15fe5c27fdc07fcdfd78ed6781cf01fadcff70bff8fe5ffcd4f8e005f285ab45a9c6d4e6db8f7880eabdf840ac6eaf25087dec6fa0bc28ffb35db9f6a4e8d5b20a21ad1dea93db071fe3bf95a6addb6dec8cbc0501746f64d6c7ff639eac97b29e832d0542fea9254e7d4f7a1cea97df6b9fe975d7441a833e663d437f5ebf4fb1a9dbbac7ff83feec6e01ffe6d0cfcc33ffcdfcfc13ffc5bc1ff21e7d438fcc3bf3a633e06fee11ffee15f7d6fbf06d5bf12fcc33ffcc3bff585ffe36e0cfee1dfc6c03ffcc3fffd1cfcc3bf15fc1f724e8dc33ffcab33e663e0ffc7e0ff67df84fd99c1986da61defc0ed3f6076018c1ad16532e6954f04b79a57176456cbe6fbfad9a1f4eb5317667660a235fbfdcc40f097a28d51bd791f1f13a9f38d7d9c1a571eea7bfa6705baff364add7e3b7daab3a0ce7374e6a26fa72ee46ebf7e1cfee11ffee7d7d5e913fee11ffee11ffe0f390efff00ffff0af04ffbaaeaa09fff0dfedd78fc33ffcc3fffcba3a7dc23ffcc33ffcc3ff21c7e11ffee11ffe95e05fd75535e11ffebbfdfa71f87f1eff6feaa3d9c6fc4f65a20eb18df3b1ca3f5a9cdfa8e803fb3a518ffed96f9aaaa36a45fbe02fb4285601a740f5f5fcfea8cb333a54d181f0b5bbf5b303ac62ad57f4ddb34b578dabefeed7ec81cdf65df5a1be4b7486a3736dcf507609a9ef65fb54176eb647bef78c3ff887ff99faf00fff6acec7c13ffcc33ffcfb1ebc4fd43ffcc3bf17fcc33ffcc33ffcc3bfaa13f5e89fe11ffee1ff28d706fff0ef05fff0afe67c1cfcc33ffcc3bfefc1fb44fdc33ffc7bc13ffc7f05fedfec8b0f521ba2161801551d966c832a40bd6776787dbedd74afce5afcbbafefc7b28f64bdb21a1ee0e882f0efaab6f7f775b21e2b55074ec58f79df83f58bc0f5fbeeeb667d56174de70cf9b5f975a835d8facac3aa3ad7518fbe5fdf9b1f87ffe3e41bd5f5eff07f9c6af81cf8877fef01fff7def00fff5ef0dff384ff43bec37fdc27fcc3bf17fcc3bff7cdd6e8d706fff93cfcc3bf8f1ff3be07eb07ffc79d7ff41ef5eb7bf3e3f07f9c7ca3bafe1dfe8f530d9f03fff0ef3de0ffde1bfee1df0bfe7b9ef07fc877f88ffb847ff8f77a36ff6f519232b01ba38afa389bef9fa346a34db1f17edc6f9292faf03e5e7d98a89fe812ab6afb71fb417cddec108d9f3e37bb34a34b41adcdcf6787d67b5a1ff5cd2208a20b489d0bb55eef97f51aad27cbf13155ac5f5b046947fe17c611f4ee7bed30e5fb817ff887ff738e8f817ff8877ff88f62e11ffe238f310efff0df11fcc3bfea1ffee1dfbec3bf16fcc3bf8d877ff8877ff88f7c558e8f817ff8877ff88f62e11ffe238f310efff0df11fc7f4dfedfa224ffa1fd86fbcdcea0ca0e59049dadd3f5181f5179fa4331e2edcfecc354976854cfc6f99f6a5d599e3afcf6a2ec5ce2d1a5d0b9c8fce5aece859df78af64af5a8eafb71bfde0c02a5ec4cf9387fc60ed19bfd995d9456ea9757d443778fa39ffe1775974bf887ff11af72e11ffee15fd7b371fe27fc9f6ba9bee11ffee11ffee11ffe330ff887ff19c13ffcdb9ff0afe3e01ffee11ffee11ffee1ffdc2bfcc33ffcc3bfaa67e3fc4ff83fd7527dc33ffc7f59fefff8c73f7e1f09d1a510c9e658733f3f9e7d4c7401f9a6b3da2baafaf231eabd5bc77aafd4adfc556c049a3a58d5379ce923f2ea00945d10b3dfa23a5756ab6728d30a4b91c7ec5c673ecb81ff738c7aefd6b1def0dfcb3f0ef8877ff8eff8c37fece7c794e0ff3e07fecf31eabd5bc77ac37f2fff38e01ffee1bfe30fffb19f1f5382fffb1cf83fc7a8f76e1deb0dffbdfce3807ff887ff8e3ffcc77e7e4c09feef73e0ff1ca3debb75ac37fcf7f28f03fee11ffe3bfef01ffbf93125f8bfcf81ff738c7aefd6b1deb2aeff03ac99057473a28dee4269177155573edcac57f411775c768fcadbed7f75bfbb40eeaa37e3bfab96f2ec9e91eebe577ef0bfdf0bfee17fd513fee17fa5e6d53cf8effbc33ffcc37f9e07ff6b9ef00fff2b35afe6c17fdf1ffee11ffef33cf85ff3847ff85fa979350ffefbfef00ffff09fe7c1ff9a27fcc3ff4acdab79f0dff7877ff8bfcaff9b3de05d0d536b36eb536d82ad911d9a59cd1cb2ae5714eb6bd9358dbcd57debe6cd1ee0d5c319f565cf889aef7a57073daa17c5cd4ae576bf45356fd7569d7bbf976a7f23f0d5dec0ffb1ec0dffe73cf8efe52a1ffb0cfff0bf52d30bfe75dcace0ff903dc23ffc1f07fccfe62a1ffb0cfff0bf52d30bfe75dcace0ff903dc23ffc1f07fccfe62a1ffb0cfff0bf52d30bfe75dcace0ff903dc23ffc1f07fccfe62a1ffb0cfff0bf52d30bfe75dcace0ff903dc2ff35fedfb24d8c2e081bef4d559c1acf7cb3b8ce075f05cd831ccd8dbe662e37bf96cc3fcab73ed5c7f7bd75f6e4ca059579da3336fb0ba473a1cefc72b97a610c8fd96f199df928beea438d45fbe12f78cf2ffc9ff3e07f8fe0ff7d0cfe7bbe591cfcc3bff5b69e335ef00fff591efcef11fcbf8fc17fcf378b837ff8b7ded673c60bfee13fcb83ff3d82fff731f8eff96671f00fffd6db7ace78c13ffc6779f0bf47f0ff3e06ff3ddf2c0efee1df7a5bcf192ff887ff2cef59fcffe77f82d006760f749613f9f8cbc3c6ccf874fb985195dfed7db60f0b82fdd9c959f17f863adf76c5271abbe2978d3f523b6bee5a17fcc3ff0ec1ff7a2f3bbde0bf2ff8df27f85fef65a717fcf705fffb04ffebbdecf482ffbee07f9fe07fbd979d5ef0df17fcef13fcaff7b2d30bfefb82ff7d82fff55e767ac17f5ff0bf4ff0bfdecb4e2ff8ef0bfef709fed77bd9e905ff7d7d04ffa73fc07a843ee270a339bdea37e2ec3c4ecfda5bbee1eb0bfe7f3cc13f1a82ff1f4ff08f86e0ffc713fca321f8fff104ff6808fe7f3cc13f1a82ff1f4ff08f86e0ffc713fca321f8fff104ff1fafb76393b20dfe4a9b7f5bcbb3d7f38c7aa386adf5a8ba91af1a07dcc769e7dec2ff636b3eab06fcff3882ff79c1ffbe5a9d71f87f9ce07f5ef0bfaf56671cfe1f27f89f17fcefabd51987ffc709fee705fffb6a75c6e1ff7182ff79c1ffbe5a9d71f87f9ce07f5ef0bfaf56671cfe1f27f89f17fcefabd51987ffc709fee7b59bff6d7f8075fb2bb7a10e485717d1c97fc487bfad73acb5ebaf2e3575e1653567ebaccaff536a5714f513ed9f3f43af00ee4c0fcfecd79e9f2e0b8fec0ffe7b7dc03ffcef10fcc37f566755f07f2ff8ef09fe7b7dc03ffcef10fcc37f566755f07f2ff8ef09fe7b7dc03ffcef10fcc37f566755f07f2ff8ef09fe7b7dc03ffcef10fcc37f566755f07f2ff8ef09fe7b7d9cf8f7ff1384b7890e78ddb847e4cfe45eed73c5ef596b7b557d8535ccea996b7ee4f982ffeb7ef00fffaf5a0bfeaf09fe6bc1ffebd682ff6b82ff5af0ffbab5e0ff9ae0bf16fcbf6e2df8bf26f8af05ffaf5b0bfeaf09fe6bc1ffebd682ff6b82ff5af0ffbab5e0ff9ae0bf16fcbf6e2df8bfa6ddfc9ffe05acea2fd9545c15eba51a9cc9b77fa599e5cd1ca6f1b3f253cfaa3f1be76355ee188bfa9dd99facf618b73fb3e7cc3b925ac355cf959e3b67a4e3b5b2f791aff29ee92f3ad3559da18a07f8877ff8877fef59f9c0ff7d7f360efee17f3cc33ffcc3ffbc27fcc3bfad0dffb5e01ffebde05fc78d9ff00ffff07ff6ac7ce0ffbebf21f8dfe309fff06f6bc37f2df8877f2ff8d771e327fcc33ffc9f3d2b1ff8bfefcfc665e7e9a7fffeeffffec5375135191947f239aac131e663672e2f9b57e55c95af93bdfbb529af286665bdb38a80a8cec26c0dffdce9dde70df8fc9e5928ed98f25a39c3d663f6b2a97aa87ee966316a7ee59286ff39c1ff7c0dff0cfff00ffff03f9307ff3a0efeeffdaa9cab82fff91afe19fee11ffee17f260ffe751cfcdffb55395705fff335fc33fcc33ffcc3ff4c1efceb38f8bff7ab72ae0afee76bf867f8877ff887ff993cf8d771f07fef57e55cd52efefff307589d82d17bf5f1c69c85a75bd3369f41a572ab7e66e7aa0b34ba2cfcbb8aebaa133fb3c73e6f662e826505d2a846557bc5af133ffb8b219bb763d1c5a818da7971cc9e33951fbdc33ffcc37f3c6fc7e01ffee1bf5fa3aabde2d78987ff737ef40efff00ffff1bc1d837ff887ff7e8daaf68a5f271efecff9d13bfcc33ffcc7f3760cfee11ffefb35aada2b7e9d78f83fe747eff00ffff01fcfdb31f8877ff8efd7a86aaff875e2e1ff9c1fbdc3ff3aff3ffde637bff9e5682aba382a383bbe3390f903eb17dff5a8622aa94391813703c1ea05b37ad023bf283ebb4cfc5fa68ee78eaf8f55105e953f3fabc075eaac688685ca23ba3867bfb98f857ff887ffbace8ae0ff4873e1bf570bfee33af00fff554c25f887ffaace8ae0ff4873e1bf570bfee33af00fff554c25f887ffaace8ae0ff4873e1bf570bfee33af00fff554c25f887ffaace8ae0ff4873e1bf570bfee33af00fff554ca51dfcbf65895e36d943a636c5e764be7e11d9c26c7ce5ef0fc9f8a9d6e2eba83cbf465f3feaa703edec41507dfb7d51072d3ac0d15ef91cb5ee6a1d1924518cea439d934c512f33e7d3d7b17dae5c60d177c8bc567e09a93317ad1bfecf75541efcc33ffcc33ffcc3ff98eb0afe635ff8877ff887ff910fffb5e01ffeab7af00fff3e06fee11ffee1dff605ffb907fcc3bfff09fffd75c03ffc67bef00ffff00fff23ff47e1ff4d7d88a889ccb403a4f2890e61b4307529649ba4fa5539be8e7d8fd699d5897c33082dd8d905aa6a7ad90312f957dfcfc7772e3f0f87bae87d9fd9a1b66bf0f5b39e3a20a9b3e4559dc70cde2c27fa85e9fb51fb63e77dfce17aced8cdeaaa58e501fff0ef73e0ff2873e05ff70dfff00ffff00ffff06ffd6caf512cfcc37f560ffee11ffe8f538caaab629507fcc3bfcf81ffa3cc817fdd37fcc33ffcc33ffcc3bff5b3bd46b1f00fff593df8877ff83f4e31aaae8a551ef07f91ff3ffde94fdf7da1ec12b0b1aa806fae1363eb78efecdde7595f7520d4e5131dae2cdff7925d6cbe6ee6a9f63b1b1be36a4ffcdc6caf6a8ddd3d50bdab5ad6cffb77eba9bccef79f1db373d19aab78db9f7ff6ebabd6a26a8c67df4fc487e20afe75af36dff702fff0df89b7fdf967bfbe6a2daac678f6fdc0ffb91efcc3ffcc989d837ff88f7ce0ff90bdc33ffcc33ffcc33ffcfb18f83f4eebe98c8d71f8877ff887ff6c0efee13feac9d7827ff8576becee81ea1dfee11ffee11ffee1dfc7fcd0fcfb3fc0528df80f1c6d587620ab8556872ecaaffcec58e453f5dd399c9d8b48d5af2e38d5a3eac3aab357d1e1edf864b9d958054ea5ea5ba85a91ae5e22d1e55ff51f79f9be66b8e8acbf3a27f00fff5d1ff8877f550bfee11ffef3fe232fdf17fcc37fd423fcc3bff584ffd8cf8e453ef00fff63cc0afee11ffee772e13fef3ff2f27dc13ffc473dc23ffc5b4ff88ffdec58e403fff0ef7d8697cab3756c1cfcc33ffcc37fd67fe4e5fb82ff1f83ff9f7d03d94530c646a16af33b8b8df23b00a9de224fdbb35a8f8daf0ea2f7f4b5d51eaa3d533df87a511fe3395aaf9df77d29a983d481befa85e2e754befa269d035dcd59ffe8a78fc96a772e9bc857eda77d566b51fb1c9d39db837ff65e3647adcbcffb1cf8877ff8877ff8877ff8afd706ff47380efff00ffff00ffff01ff501fff0af7c546f9127fcc33ffcc33ffc6bc13ffcfb7578c13ffc676bf4f9aa77950ffff00ffff01ff501fff0af7c546f91e7cbf2ffc73ffef1bb0f8e0ada39af2856f9f9f8eac0ab8b248325ba98662fa4c847d5f0f36a0d513f9947b6d7d178b597d9b78ad6a8c66d5de593cd4771d14516e545bdaafea27394ad59293a7bd51e65e72f3b6fd145979da36abefa86f00ffff00ffff00ffff07f847ef07fdf3ffcc3bf8f817ff8877ff857827fbd76e56fc7fcbccdb37b04fff0affa807ff8576b52b9f00ffff0affde0ffbe7ff8877f1f03fff00ffff0af04ff7aedcadf8ef9799b67f708fe5f9bff7fff4f10561f353a0091d482fdc222bf6cb33bca0eccd5c3613da277a5ce05d4bd0cbd6f74682b9f6abdcacb7f9b8e77d46bb4bf1d75cf4bb456d577b68733fb149d17dfabefa55a978aab5855357d1debd3f1847ff81f391defa857f8877fe507ffe7be6c9cf7857ff887ff736d3f36fc3afdc23ffcc33ffcc33ffc2b1ff8877ff8877ff8d782ff7bc13ffcfb5e7d2fd5ba541cfcc7b5fdd8f0ebf40bfff00ffff00ffff0af7ce01ffe77f1ffb36ab22aea9bce0aa8dcea52f13d756bd978eb67d7a220c8bcc75875c965176876e0fc418dd6da015cd5b735fc5ac678075a75b145f3bedf6a5dd15a1404d9befb5a997775bea31a6a2cfb5eaa9fac9768ac7351f9ba511f363ffa6505ffeff3f6a7ed47c5f97ee01ffee11ffee11ffee13f1ef373761efee11ffee11ffee11ffedfdfb3b5647dd87cf8877ff8877ff8877f9fa7d6d299f7fd56eb827ff8f773761efee11ffee11ffee11ffedfdfb3b5647dd87cf8ff60fe6fff02d6b14111a051611b178117e545b1d15cb5119dcb2cf25dc9f3cf3379ab9ad9c7accfe8528f6a44df60764dd59eaffacdc6cde40d75ce77e4ebd76dfdaefc12f67d5c15fcf7f3fcf34cdeaae0ff588a87ff9ee0bf9fe79f67f25605ffc7523cfcf704fffd3cff3c93b72af83f96e2e1bf27f8efe7f9e799bc55c1ffb1140fff3dc17f3fcf3fcfe4ad0afe8fa578f8ef09fefb79fe79266f55f07f2cc5c37f4ff0dfcff3cf3379ab82ff63291efe7b82ff7e9e7f9ec95b15fc1f4bf1f0df13fcf7f3ecf3dd1f60558d2aa3aa90f79d69b8bb9847c566105dada13cd5f8958be7d997fc8cdf0c381dbfabfd54b937ad5cd8d15836dfad3773a1dab31c79c33ffcef12fce763d93cfcd782fffbf91d3dc17f9c7b13fcd775b2bee0ff3d5679c2fffd1cfcc37fb7c62362e17f5df09f8f65f3f05f0bfeefe777f404ff71ee4df05fd7c9fa82fff758e509fff773f00fffdd1a8f8885ff75c17f3e96cdc37f2df8bf9fdfd113fcc7b937c17f5d27eb0bfedf638767fa2f60cd34d06d6226e62a34d1a1dee15b1dfee8103fe2a2bb69a767a76667ad555e16b37b9f6674f51ceffcf69dfd78d45ec17fbf37f8877f3507ffeb31f0dfebe726f8df27f87ff71d82ffbc37f8877f3507ffeb31f0dfebe726f8df27f87ff71d82ffbc37f8877f3507ffeb31f0dfebe726f8df27f87ff71d82ffbc37f8877f3507ffeb31f0dfebe726f8df27f87ff71d82ffbcb755fedf6e0376a3bd6ca235573f47633eb6f321558c8fcdfa8c3c94469f3e37da0b3566d7aa3ea8aa5f7df86a7d915f674f677dc7cf68ed76ded6aff6bdaa370348e73ca873db89f5733ea6bbceeede64753bf5ec599c5d33fcc3bfca83fffb3cf8bff7847ff887ffb3e01ffee1ff08f3b27af07faedba907fff0afe65704ffe7391f03fff00ffff0af04fff00fff479897d583ff73dd4e3df8877f35bf22f83fcff918f8877ff8877f25f887ff2effe9bf80d55506ccca5c05e2ec781463ebdc64733ba0756a4435bbb9dd3ad53edfb4b21655afaa9badbfe35f8d557355fd5dfba1bca2dad19e75f66ab5dfe8acef58775467c71cfcc37f2717feebdee03f8e81ff39c1bfeec17ac1fff539f887ff4e2efcd7bdc17f1c03ff73827fdd83f582ffeb73f00fff9d5cf8af7b83ff3806fee704ffba07eb05ffd7e7e01ffe3bb9f05ff706ff710cfccf09fe750fd60bfeafcffd48fc2fff01d6ea877cc401f868ad1ec02b5ebbf77ff652f86adab9c657f37af6a5fb88bc5716fc7f7ec1ff733ce17f8f17fcef15fc3fc713fef778c1ff5ec1ff733ce17f8f17fcef15fc3fc713fef778c1ff5ec1ff733ce17f8f17fcef15fc3fc713fef778c1ff5ec1ff733ce17f8f17fcef15fc3fc713feefb5e55fc042082184104208218410420821841042082184104208a11f516f5ffd2f1bad7ea4b5ce6aeccdcc1e3d723f1fe5ed7d9fbd861df576f60cffe826f87f7cad686c87ef2b78bdbae03f16fc3fbe5634b6c3f715bc5e5df01f0bfe1f5f2b1adbe1fb0a5eaf2ef88f05ff8faf158dedf07d05af5717fcc782ffc7d78ac676f8be82d7ab0bfe63c1ffe36b45633b7c5fc1ebd505ffb1e0fff1b5a2b11dbeafe0f5ea82ff58f0fff85ad1d80edf57f07a75bde25adf6eff4456571fbd801578fc3fa5f72cd9bae93f4156ac219a9f5d7ba5b13733ff449cddcfd5754471c3fbcade29f933b0e34c3cfb52edf4bcf20d77793e4af0dfcfebc60cc1ffbae0ff3982ff7e5e376608fed705ffcf11fcf7f3ba3143f0bf2ef87f8ee0bf9fd78d1982ff75c1ff7304fffdbc6ecc10fcaf0bfe9f23f8efe7756386e07f5df0ff1cc1ff7a0f95aef2bfab17cf7f2776468fe0bf5b2b1adbe1fb0c2ff8ef8bdfff6bbdf0fbbf16bffff57b95f38afcbfd9806a411970aae02c7495a20dccf2af6efaec5a6c5d7b99441fa4ea2f9a1f9eaabf99ff27a6537315bc95c3affef767bbb159fd2bcacec06d6c669fb3ef39fcec775d55d693f787ff58f00ffff07ff684ff5cf00fff4af0affbe908fecffef00fff33756c0dfb6e7f763ce13f17fcc3bf12fceb7e3a82ffb33ffcc3ff4c1d5bc3bedb9f1d4ff8cf05fff0af04ffba9f8ee0ffdcdbabf01fb1117966b567cec32bf05fe5cdd6dac17fa7867db73f23c17f5ffcfee7f7bf12bfff753f1d7d45fedf6c40d5ac074381327c945ff4d166eadab191577d9828df3e47873e82bceb6d7dd47314af3c7d8fb33eb6e79903d581d97efff1d303db013ff2ca7a8be6d5a5d6fd5ed9f84cfdd99ad1998ef62e9a578af8857ff8cf04ffc7727df8d7f9f61dfee1df8ec13ffc6779b64e960fff879c87fff31aa2b55582fff7f71103fff00ffff00ffff73eea398a579ef0af7b837ff8cff2e0ff7e1efecf6b88d65609fedfdf470cfcc33ffcc33ffcdffba8e7285e79c2bfee0dfee13fcb7b65fedfaa42caa4f3b1d5e67bb0d4c655cd57bd64f9dd43efe3a2faf6b065f1b3704407e2e6eb2f35db7b0552f64bc0f6108112ed6de75b47fd447ba8e2544fd5fcf0b7fbe47f11a95ed54512fd62a82e9aea82f1b9d17953dfaf7b89d9187571c13ffcc33ffc77f2e11ffea39e7c5ff00fff2307fe63c13ffcdbba5ef0dfeb05fee1dff70cfff739f0afe7e11ffefd1ae0ff9035e11ffe554fbe2ff887ff9103ffb1e01ffe6d5d2ff8eff502fff0ef7b86fffb1cf8d7f31fc1ff4fbff9cd6f7e892633f06663bb1e1d70559ebac0bab9b38afcab4b20aba5623b504597467528c778671dd5f86a9c8d8ff6275a87eddd9f1bb587d5beaa7d54f37eaedac3ec3baccc557bdbe925ab95e575eac2fff91dfeeb78f8877ff8877f35173dc3ff11d6cd7ac96a65799dbaf07f7e87ff3a1efee11ffee15fcd45cff07f8475b35eb25a595ea72efc9fdfe1bf8e877ff8877ff85773d133fc1f61ddac97ac5696d7a90bffe777f8afe3e11ffee11ffed55cf40cff475837eb25ab95e575eac2fff93d3bdf6f47229fa40a8cb1b1711e846e9336d73f67f9aa57df8bcf8d603a1a3dda7a597fd65b81ef3dfd9cbd48ab3db397ce18ebec6574d83b7bb232efff1be3eaecf8b9ac77bf076abfa33361e7b30b3aea313a63519f6ace3f7b60fd2fd5ec82e9f4def9c56eebdabeace03fee0ffecff3f00ffff0af3de11ffead07fcc33ffcc3ff98837f9daf7a857ff887ffb877f8877f3b0efff01f79c13ffcab989579f8877ff8d79ef00fffd603fee11ffee17fccc1bfce57bd7e26fedf32b36c1163ae3ad05173ea0057102b753e88bfa0c658760165bd5407b8824dc547b1f662f07be6d7e5d7eb0f5cb496ce3788f6caceab9a6aefad5fe7c2f0e3c3d7f6defda5935d28337951adacf70aece8521f637ebd9957b4868a415537aa03ff714ff00fffde17fee13f8a8f62e1ff3c3e7ce13ff68ad600ffef79f00fff51ed2a0ffee13feb09fee1dffbc23ffc47f1512cfc9fc7872ffcc75ed11ae0ff3d0ffee13faa5de5c13ffc673dc13ffc7b5ff887ff283e8a85fff3f8f085ffd82b5a03fcbfe7bd02ff3f8f60fbd327da71b5a9cadcc6671fda7f50e51dd5f0fe51ae1fcb36d87f18ff916c3f7edfd445e87b5cb918a23ebc9fea233b6856de4f7d970816b53eb5de0ae6a8beca1d31be76e752adbe71b4261f1fbd47399d35649eb6b76a8ddedbd6576719feef63d4b3ad07fff01fbd4739f00ffff07f841e517d953b62e03f1e837ff8877ff887fffb31f83f0bfee11ffee11ffee335f9f8e83dca817ff887ff23f488eaabdc1103fff118fcc33ffcc33ffcdf8fc1ff59f00ffff0ff7cfedf5471ffb1aa4d1e31be68b440fb73683469bd549eefcb6fb4cf57ebb19e55dfd1e1cdd6a27acff6c35e3eea59c5da75fb3ea23d1c8a2e459b6ff74dadd3f6d9f98591ed7574706dadea62507dd9187b56d4fad51aecbc5a8bfaaeea9bd91cff1c7def6ccfd519cc9efd3e556711fef59ea87c3f16ad19fee11ffee13f9b837ff8577959aff00ffff07fc8bee0ff3d0ffee17f08fee15f09fee1df7ac33ffcc33ffcc3ff21d702fff0efc7a235c33ffcc33ffc6773f00fff2a2feb15fe3f39ff7ffef39fbfdb0db01a63766ef639f28ee254c3be87aadf6a0d4a9d1ea3fec658a79efa90d13e476baffaaf808be6aaef975d78d9beabba3e6e46554dff9ce564becacfaf6be54cfa7abe66774c9d15dfeb18ef9c4bf887ff281ffee13fda93ec39f28ee26cbf76cdf00fffdd9afe39cbc97ce11ffee11ffeed33fcc33ffc1fe97a7c8eaaebe36604fff00fff799ff00ffff00fffd6cbf7eae7e01ffed518fcc33ffcc37fb427d973e41dc5d97eed9ae11ffebb35fd739693f9c23ffc3f9aff376b349e6f3fedb32aa44c6d8ccdf7ef4a236ef847757cbeaf13c5291f1b97f518c58df7ecb0ab5e55dfaab61f577d2b8fcec5d3bdc4aa71dfa7ea275a8befc5f711c5ab58d54735ef7dd5b7b4f9decffb8cdcecfc588d7a0a700fb41d5375556fd985a8ce1bfcc3bfdfc76adcf7a9fa89d6e27bf17d44f12a56f551cdc33ffcc33ffcdb1ee11ffee11ffec7bbcdf77ede07fedf63e01ffeabb5f85e7c1f51bc8a557d54f3f00ffff00fffb647f8877ff887fff16ef3bd9ff781fff718f887ff6a2dbe17df4714af62551fd53cfcc33ffcc3bfed11fee1ff99fcbfd917bbe8e8a3db86b2cdf21f5c1d7e555b8dfb83e90f94cf8fe254df1e1a0f52b60f7ebf6cbeda033ba63e54b407be66745164397ebd91fc3a3a8728f3aabeab8726da0bf5bdbf0717b9bf48aa4b2383f138f4d9523d45bd7acfa8efee2f43ebe97bc9beb1e221aa0ffff00fffc7a917f8877fb546f83fc25ed59ae01ffe556df887ff31efe3bd37fcc3bfaf07fff00ffff00fffe73ee13f5e07fcc3bfef15fee1ff38e05ff9c03ffcc33ffcfb3c9befebc23ffcc33ffc67b57f38fefff4a73f7d5797c15d50f011b2023e369ab3f37e212aa7d38f5d4bb606553bf2b1f387e853f5aa6a4471aa6eb416e515c5cdd48ad43d133e3eea47e547632327ebdfc774faeb7c63d5af3af71107ca27f255b9d979ea7cbf99b300fff01f09fee13feb61bc6735a2393b6f63e01ffee11ffee11ffee1bfee3f8b837ff8efd48a04fff09ff530deb31ad19c9db731f00ffff00ffff00ffff05ff79fc5c13ffc776a45827ff8cf7a18ef598d68cecedb18f8877ff887ffafceffb73ffff9cfdfa3a4d5055540478aea740fc1959ae37968f5f077e150f1d198da87e850cc5c3ed15c76a975facd34b33fabb53abe3745bff0ec5c5523fb26336776750f577d567f49c13ffcc33ffcc33ffc773cb3faf05f8f67b567e766e2e01ffee1bfae01ff73be9d9ee01ffea379f8877f1f7f45f07fc83ac32feba1e399d587ff7a3cab3d3b371307fff00fff750df89ff3edf404fff01fcdc33ffcfbf82b82ff43d6197e590f1dcfac3efcd7e359edd9b99938f87f0eff6fd1440676d69cfd9949c578df68d3337f3bd7e943d5f4178f8a5797a4f2cbfaab6a64bdfae7ceb7e9ec87f5530752f95fbd7ca3beaafdede4f9b851575d2e9d7c1f937d9319cdc467b11533dd7300ffda03fee1dfc6c0ff7d1dfb3313fc1f658dac57f887ffee1cfcc7827f5d07fe8fb40efc1fb28efd9909fe8fb246d62bfcc37f770efe63c1bfae03ff475a07fe0f59c7fecc04ff475923eb15fee1bf3b07ffb1e05fd781ff23ad03ff87ac637f6682ffa3ac91f50afff0df9d83ff581fcd7ff82f6045c95981ac097bd0d5015f817826a6bb411daf5545175014f32cdfce777dd47c762e66fc3a7bd0f55a953f639d33a7409edd8fccbbaa3feb07ffeb82ff780efeefdfe17fad872806fee1bfebb52af887ffae17fcdfcfc1fffd3bfcaff510c5c03ffc77bd5605fff0dff582fffb39f8bf7f87ffb51ea218f887ffaed7aae01ffebb5ef07f3f07fff7eff0bfd6431403fff0dff55a15fcc37fd76b3c977f8035bb393775e2b30f117dacab174116bf02f095cb72f5c258517501ded4bdc0afecf18cd4597af43e5535aed6dfd5ffca37b869f53bc07f9d3f5b13feeb3cdbd715afd9baf0bf560bfefb35e1bfceb37d5df19aad0bff6bb5e0bf5f13feeb3cdbd715afd9baf0bf560bfefb35e1bfceb37d5df19aad0bff6bb5e0bf5f13feeb3cdbd715afd9baf0bf560bfefb35e1bfceb37d5df19aad0bff6bb5e0bf5f13feeb3cdbd715afd9baf0bf560bfefb35e1bfceb37d5df19aad0bff6bb5e0bf5f73fc7cab1246f048ae62ab0df2be7eccff54f359fd8eaa4ba7baacaab84c572e23ffbc43fe9b45fdf96fd3ed67e5808eefe1732baf1d7b93d5506736ea417d337f99a83c35e7e3ecb9ed6a2567e4c1ff7d5e159709fe73c13ffcc37f5c17fef3dcab82ff731efcdfe7557199e03f17fcc33ffcc775e13fcfbd2af83fe7c1ff7d5e159709fe73c13ffcc37f5c17fef3dcab82ff731efcdfe7557199e03f17fcc33ffcc775e13fcfbd2af83fe7c1ff7d5e1597e974a667fe2708d1f3e40f05eae991fbb6d35b793daa77ced2e713df6c4df0dfab855e5b7cb335c17faf167a6df1cdd604ffbd5ae8b5c5375b13fcf76aa1d716df6c4df0dfab855e5b7cb335c17faf167a6df1cdd604ffbd5ae8b5c5375b13fcf76aa1d790fc17b09efdb13af51ed9d32b1ececf06cc2bf45bfd056694f388d82a478dafeee1ea5f86ce8e3f4af07f16fcaff500ffd77ac8c61f25f83f0bfed77a80ff6b3d64e38f12fc9f05ff6b3dc0ffb51eb2f14709fecf82ffb51ee0ff5a0fd9f8a304ff67c1ff5a0ff07fad876cfc5182ffb3e07fad07f8bfd64336fe28c1ff59f0bfd603fc5feb211b7f94e0ff2cf85feb01feaff5908d3f4a3f32fff20fb03aff4cd9cec3d8a9d789c96a5cf1bef281bab98f80cfcf45ef767cb58feef789b40302f54ffd757376c9d77ee4d9eaf632cb1dfc5ff79ccd857ff8df2df8ef09fee17f97e07f3e07fee11ffef70bfe7b827ff8df25f89fcf817ff887fffd82ff9ee01ffe7709fee773e01ffee17fbfe0bf27f887ff5d82fff99c1f99fff27f827018cc6e808d5ff1580123cb9bede12335d6f1887d78863eba7e579fa5cf5df2ebedac1ffe9f2ff87f8ee01ffe5f51f0ff1cc13ffcbfa2e0ff39827ff87f45c1ff7304fff0ff8a82ffe708fee1ff1505ffcf11fcc3ff2b0afe9f23f887ff5714fc3f47f00fff1fadb75b63a339a55bc3334dabf8cc43d55edd24f5d1fd9c1fcfd63eabe155eda9ca511a97f0ac8fdabfd153d49b1f9fdd179b3ffbfdb25a3bbf8f57d667772f3afd55e7c19e1b355e8d75e51918e70bfef708fee13f8a817ff8cf7294e01ffea3d815c13ffcc3ffbbe01ffee1ff7e5ef975ebc37f1c03fff09fe528c13ffc47b12b827ff887ff77c13ffcc3fffdbcf2ebd687ff3806fee13fcb51827ff88f625704ffafc7ffdbd89cee01890e4b3416cd65073c5b4077dc7fc4db7c760854cd6cad6a7ef8fb3ded42aadeabcbf4306bcbea5520d9f9e87b64dfa5739955877f0586eaa28c7eb1f8fd53e7c77f83eafc4775a38b419d1b3ba7f6b4ba9cb2358d317bf1c23ffcfb79f887ffca13fee1dfe747827ff887ff5cf00ffff07fdf67c7271a87ff7bc1ffb9976ff00fff07fcfb79f887ffca13fee1dfe747827ff887ff5cf00ffff07fdf67c7271a87ff7bc1ffb9976ff00fffc7c7f2ffd36f7ffbdb5f9459b6095123d5bc5da83fe0dd4546bd7a8fcc4f5d5e519ef754ef7eb3edb8f2c8e40151bddf94f5dced5dc5547eb637bfe6ce37506bf26b89f23aebf4b1d9f78ceadbf1eef9eb7e83ca53e577bf91e2ca7ad8777fc666d6a4ea76e63d1bf0affb837ff8576b51b5d55ae01ffe3bbdab98ca0ffee15fd5edcc7b36e05ff707fff0afd6a26aabb5c03ffc777a5731951ffcc3bfaadb99f76cc0bfee0ffee15fad45d5566b817ff8eff4ae622a3ff8877f55b733efd9807fdd1ffcc3bf5a8baaadd602fff0dfe95dc5547ef00fffaa6e67deb301fffffffbafbffefa7d18d8467cb0fa783ecf7f7835e61bf05e9dfcc8cbfbce4aed811af7fb92ed93f7b139de331af775b27d5787b1b3a64aeabb455ed15a7caf2a7fe65c453547accacb7a89facdfa5071599dd91ab3e748f9ae9c3b5b47f503fff7fddbf7c80bfee11ffee11ffee1bf5b03fee1dfd7817ff8cffcbc2ffcf704ffb1e01ffe553cfcc33ffcc3bfea3d5a8f15fcdfd7807ff8eff494f9795ff8ef09fe63c13ffcab78f8877ff8877fd57bb41eab1f9dffbb7f016bfc670dd422d526d878eba39a56e395a78fe9cad655cf597d35de3d00d99cf5b2f5b33d389235a9c391f55979da7eb2fe552f5dade4ab6fa6623208949f8af5dfc38fdbe7a80feb3bc343d57374496471d97aec7cd5b7f786ffba6e340ffff09ff53f330fff47ea1f3d57bcc07fdcbfeaa52bf8877f951f79fa98aee01ffe2b3f150bfff0afc6bf7d837f5f27f2c804fff0aff2234f1fd315fcc37fe5a762e11ffed5f8b76ff0efeb441e99e01ffe557ee4e963ba827ff8affc542cfcc3bf1afff60dfe7d9dc82313fcc3bfca8f3c7d4c57afceffcfc34c411c25da589f1f6d98f7571ed1fbc851ef59ec6d2e9a1f7daabe236520a935740e4c34eff7cdd6f57ba7f6398af1873a5b77d6bbaf5beddd8cb7fa3e36479dd5ea97948d89f6329af363ca2f3ac31157d5f9a82ecbac4ef46e2f90f113fee11ffe751efcd77dc13ffcfbfe3a827ff8f71ed1fbc851eff00fffca0ffee11ffecf31f00ffff00ffff07f9479f05ff705fff0effbeb08fee1df7b44ef2347bdc33ffc2b3ff8877ff83fc7c03ffcc3ffebf0ff363ecaf8cf9af98f6437d6c667f287461da2683cfa48515f55edaceea8917db411537de04e6ddfbbf7f279b637bff76a1fa26fead76a9fab6faace4a27cec6fb3c5bbb03a2f7c8be818d559765b466efa9deadbfba28fd9afd7efb3c75beecb8bab41497f6a7edc5f767d70effef35e01ffee11ffe232ff8877ff8877f1bbf521bfe8f536df8877ff83f641efcdffbfbfcac17eb05fff0af62460df8877ff887ffc80bfee11ffee1dfc6afd486ffe3541bfee11ffe0f9907fff7fe3e3febc57ac13ffcab9851e3e9fcfffaebafdfef06ccc7b1c1eae3a83c5fc8ca1e3c15a3c6a3d831a7d4391855cde1a3e05271f65df5e3a1cbd6a9be45d447f4ada2cb5c29fb6ed5b9f0bd789fce25a3eaf99c282fba0cd55e476075be8deac7cf479046cf6a7d9dfa9d7df0f52ac1fffdd8f0817ff8877ff857def00fff761efee13fea07fee11ffee11ffee15fe54463c701fff00fff511efceb185faf12fcdf8f0d1ff8877ff8877fe50dfff06fe7e11ffea37ee01ffee11ffe7dceb73ffff9cfdf3380b38d8a0e5e7418fc58e4991da0ea232a559e2a6e36378abfc97f806a4d9d83ea7dedb8d5ca1e457da9f56587b7ea39eabd7301a9b8ac7ed5afcdc918a8ce71d4cbcc79f5b9beff998bac7381c1ff51d6ab72a3f89be01ffee11ffee11ffebb827ff887ff43cec13ffcc33ffcc33ffc7704ffb5e01ffe7d0d3f57c5c03ffcc3bfee05fee11ffee1bf2bf8877ff83fe41cfcc3ff57e1ff3fff025664e4a1ce0ea6fa10dd03ef95017f65232a45876aa55607e4cec5a6dead6661f0bd7b0fd56bf742cbe6bad0cc1cfa9575fabcee85333b3ed37b76d1da71bbc68ac32b827ff88fea57e3d11cfcf7e2e01ffee11ffee11ffea3772bf887ffaaa759c13ffc47f5abf1680efe7b71f00ffff00ffff00fffd1bb15fcc37fd5d3ace01ffea3fad5783407ffbd38f8877ff8877ff8ff01f8b77f803503a03ac09dcdeeaa7378a33ea385abcbc6f71e1db8ecc3571b9d5d909157a6eab2aefa8ebe5d771d99a7ef73e8ca5aad47b58f9d779b9fd5cda0cae27768959de88ccfe4fa1cf8877ff8877f1f0bffe7ba594fbe17f887ffacfe10fcebf9a84ff887ffecdde66775e1ff7d1cfee11ffee1df8fc13ffc477dc3ffbae01ffeb3b5c33ffc6779f00ffff00fff33f9f00ffff07fce837ff87f14ff6f7e93c67fd9a255e3518ef5f38b8bbc0fb188284ee5d85cbf601b639f6ff3d941cb7ab67ba1d6aa7a8ed6677b51b5546defabc686a7fa66eac0280fbfd6ec5bf87eaaf5546bf4df2ddadfac2fb5b6ccc7afc1facf9cd76aadead9d78bf2bdbfdd9fce79847ff8877fede3d760fde11ffee11ffe553cfc1f274f9f9f797b3ff8877ff8d7f9f07fce817ff85782ffbc36fcc33ffcbffbf835587ff8877ff8877f150fffc7c9d3e767dede0ffee11ffe753efc9f73e01ffe95e03faf0dffcfe7ff4d99ab8fea1bed2882b8133b2b7b20c74ff5213245d0a979eb9f6db63dd43ede7f407f08ec5e47872a5abf8f8b2e5bdf87aa6de52f5fd5b3adaffaf7b554bc8a51fb11d5ad9efd9afc7c756946dc78a93564f9aadfec3c7c4b2e4c25ffade05ff700fff01fc5455ef07f2ff8877ff887ffe8bd3bd711fcc3bfcdab9efd9afc3cfcc33ffcc3bf8a8bbce0ff5ef00ffff00fffd17b77ae23f8877f9b573dfb35f979f8877ff8877f151779c1ffbde01ffee11ffea3f7ee5c479f95ffbbff09c24ad9e1b92a0b7676b8abda51fe4c7cc76326c6c76635bbfd7740ea7acfe6abf5dcd4d9c799beb35e56f377682707d91aaa3a9defe93daeec19fcafc5c0ff5adf592fabf93b04fff00ffff00ffffb04fff0df11fcd7b9f00fff33def03f27f8877ff8877ff8df27f887ff8ee0bfce857ff89ff186ff39c13ffcc33ffcc3ff3efda8fc4ffd0156a75877fcd1aa0ef6cc65b7bbfe47e9152eaf1f55cfd8efce2ffa67d5aec63faa1f3b07fff0ff2cc1ff7305ff67c1ffc709fe9f2bf83f0bfe3f4ef0ff5cc1ff59f0ff7182ffe70afecf82ff8f13fc3f57f07f16fc7f9ce0ffb982ffb3e0ffe304ffcf15fc9f05ff1f27f87fae3e0bff4b7f80f5ac4dfdc8039589cb0b597560ffe83ebe421df847af28f87f4e1df847af28f87f4e1df847af28f87f4e1df847af28f87f4e1df847af28f87f4e1df847af28f87f4e1df847af28f87f4e1df847af28f87f4e9dafc8ffdbb1a099e2571af5b9af72e93d7afd57d7f9d1f9bbb4bb8f47ad2bf37de65e3eab16fcc3ff3304ff7382ffe708fe9f23f89f13fc3f47f0ff1cc1ff9ce0ff3982ffe708fee704ffcf11fc3f47f03f27f87f8ee0ff3982ff39c1ff7304ffcf11fccf09fe9f23f87f8ee07f4ef0ff1ced5cff5b377975f1b7bf16abf42a1b3bab4edf76fddd75fa3dabf2fcbcdaf3198fe89b8d98ce3a662ea51ddf7ff65b747356eaccc47563a39847b203ffb9e0bfe757cdc13ffc7f46c17fcfaf9a837ff8ff8c82ff9e5f3507fff0ff1905ff3dbf6a0efee1ff330afe7b7ed51cfcc3ff6714fcf7fcaa39f887ffcf28f8eff95573f00fff9f51f0dff3abe6e01ffe3fa376f0ff9699d8e46a233d9ceae3451fd4fe935eddc338fecb6af89c595539634fa25ea2f8d99ad1471ccf9d8bc5ee71d4dbcca569c7667a8e6a77633375bf7db46f954f96a3e2a2ba9d5f983e2e8ac9fa51f5677ef9c13ffc679ef07fa471f07f9463c31bfee35cf8ef09fee3fcf10eff7dc17fee09ff471a07ff473936bce13fce85ff9ee03fce1feff0df17fce79ef07fa471f07f9463c31bfee35cf8ef09fee3fcf10eff7dc17fee09ff471a07ff473936bce13fce85ff9ee03fce1feff0dfd733f8ffe937bff9cd2f9da257c0e8ea56a3f3a1fde1b6636aee4a2fd9c5b3e2d9a979c5a38a99d91b7ba8bb174f4733974027bf3b77558f6463e5bb3e62adf01fd7b173ab9e9d9a573caa18f85f17fc3f56f00fff2bf9ddb9ab82ffc70afee17f25bf3b7755f0ff58c13ffcafe477e7ae0afe1f2bf887ff95fceedc55c1ff6305fff0bf92df9dbb2af87face01ffe57f2bb735705ff8f15fcc3ff4a7e77eeaae0ffb1fa11f9ffe9b7bffded2f0ab499225d90d5f3ce058edc99bf0a5cadf1c8f89d5207f511353ee2e2537133ebdbdd737641aae76cdf66d8d8f50b0dfefb351e19bf53f09fc7ef14fcd7f3f0ff5cc17f1ebf53f05fcfc3ff7305ff79fc4ec17f3d0fffcf15fce7f13b05fff53cfc3f57f09fc7ef14fcd7f3f0ff5cc17f1ebf53f05fcfc3ff7305ff79fc4ec17f3d0fffcf15fce7f13b05fff53cfcdfebedf67f46c3ea9f1c9b39fcfe7d8c29dfa88edfc0e883f99f91bf97ad377370af7cb8ec9fa3cb14ad718c4507dbbfabefd051f742b9f97aefd1dfec05e3bd877f3417c555b12a3ecbed7cafe86c47cf6adf6c8c62f0ea2f50b50ef83fa45777ae12fc6b6ff8877ff83fca9af00fff518d59c13ffc7705fff17bc70ffee17fd484ff43ce477356f00fffde13fee1df0bfe6bc13ffc7705fff17bc70ffee17fd484ff43ce477356f00fffde13fee1df0bfe6bc1ff6bf1ffedd75f7f4d3bb69ba836d48fa9789f33e2468c8af53e33072fba70b2f1e8a3cfd68df6215a47d5ab9a5335550dd58ff28cbe61d4ffcc853eb3e62cb73a03d91edb185f37022bdac3ac46f6fdbdef4cef5deeba7dce08fee11ffee11ffe7b75e15ff7e7c73af9f07f9cbca231f8bff78e6a66bdf838155bd5857fdd9f1febe4c3ff71f28ac6e0ffde3baa99f5e2e3546c5517fe757f7eac930fffc7c92b1a83ff7befa866d68b8f53b1555df8d7fdf9b14e3efc1f27af680cfeefbda39a592f3e4ec55675e15ff7e7c73af9f07f9cbca231f8bff78e6a66bdf838155bd5857fdd9f1febe4c3ff71f28ac6e0ffde3baa99f5e2e3546c5517fe757f7eac930fffc7c92b1a83ff7befa866d68b8f53b155dd59fedf6cb037f309b767d590dfc0a839a5e1177dd4a837ffac2e98b106eb67c7479eda3455cffa590f1f3b3c6dffde43f5ecc7672e28bf6ebf0776def7a1f2a28baff39dfdde463d449efeec75f64e9d239b13ad63acd5ffa7d6a92e8ce8a7ef45b1e3bfb35fafaa9df110419f9dd7a81efc1fb206fcc33ffcebde32c13ffcfb1e224ff887ffcc17fe8f53ef7edd7e0fecbcef43e5c13ffcc3ffb906fcc33ffcebde32c13ffcfb1e224ff887ffcc17fe8f53ef7edd7e0fecbcef43e5c13ffcc3ffb906fcc33ffcebde32c13ffcfb1e224ff887ffccf72bf1ffed2f7ff9cb77ffc114c4b6b1e8dd2ede37a56aa8457af9f8ecd2a89ea31ea3febf352e8dce3a540fd13e785d59d3ecb78bfaf4fdccaea73a53ea7254bf00a2f3537dbfa85e56c77ba93a6afdd19cafe13d23f66c4ce6157d8f6a4cd5867ff8579ecab7b31ef8877ff8877ff8877ff8d7eb8a04fff00ffff00ffff09f79c13ffcc3ff11ae15fee13fea11fef33e7d3ff07f16fcdffbf935c23ffcc33ffcc3bf5e5724f8877ff8ffbafcbf1d853238acd9185b39c4d10628986dadace7113bfaf5ffddc6c77fded78e5b1fef6d63bd97fde97bcff6cad6a90eb69aebe64507c8f6e6f769a61f5bc33ffb5c753ed43efbef30fe5367221b8f7aeb7c3fd57b755efdb33a5f2adfae6fccfbdce8e2f2bd75fbf5827ff887ff231c877ff8877f2df8877f5f07fee1dfe7c23ffc7b3f9fa76a653d8f58f8877fd513fcc33ffcc37f5613fee1dffa786f1b0bff4758c33ffb5cf8877fefe7f354adace7110bfff0af7a827ff8877ff8cf6ac23ffc5b1fef6d6357f97fab8a4487305ab41fcb163a627c63ea235b3fdf6fb448e5a32e1eebe5fb8b0e9f3a34aa6e7469780ff5ec3d6d3f11d87eadfea0649748b49ffea7ed3fba347c6ce71251df46e52bd815743e3fdab30874ebedfb8d00ccbe8b82d47aaafa598f3eb7b32f911ffcc33ffc1f721efeefe3b21efc18fcc3bfca877ff8b7bdc03ffcc3ffb97ed6a3cf857ff8f76b857ff81f63f00ffff07fde33f8877ff8877fb56ef8877fdfabf5a9eac23ffcc33ffcfbfdf01ef00fff87a8efeb29c1ff7d7f9f89ff370b44b6b9ea405b53d558046f7460ed42ba90dbb1ce7cb40e3b567de4e852f17df8fefd41cb2ec30c10ef9b1d441bebd7aafcaafebdb7ef3f53741ed43ef8da6abc034d762eb3d8e8bbab3d55df2c5b4bc6597489db7755cbeea5f551b5fc1ae0ff3e06fee3febdb7ef3f13fcc33ffcc33ffcc33ffcc33ffc9ffde11ffee1ff5c5b8dc3fffd18fcd7f3f0ff9e0ffff00ffff06f63e01ffeab757b0ff887ff8ee01ffee11ffee11ffee1ffffe5fdfaebafdf6d60d4a897ffe06a61ca4b2d5879f9bad166643dcde4457b50ad618c657dfab168cfb21c35af7aee78cc7ce7ca4fed97ffc6d16514e575bfb3cfcd7c55dfb61fefebe3d4e5ebd5a9db59937a8fd66aeb4671d1d818b76b827ff8cf6ac23ffc777a81ff231c837ff8cfd6a4dea3b5daba515c3436c6ed9ae01ffeb39af00fff9d5ee0ff08c7e01ffeb335a9f768adb66e14178d8d71bb26f887ffac26fcc37fa717f83fc231f887ff6c4dea3d5aabad1bc5456363dcae09fee13fab09fff0dfe905fe8f700cfee13f5b937a8fd66aeb4671d1d818b76b82ffe7f2ffed2f7ff9cbf76352d9875a397c917f76587cac7faee6b2de552f9dbccea5d9bd2833afd5bdbdfa6d3a87b3f35e79757f19f81ad17752719d9a33d01fcd9ea2bcd9ef915d0e333daf08fee11ffef53aaa9ea23cf83ffbc33ffc67bdf81af00ffff07f94f5e11ffee11ffe57f3d5f34df00ffff00ffff00ffff01fafab330eff752fbe06fcc33ffc1f657df8877ff887ffd57cf57c13fcc33ffccff19ffe0156b6a0cefb18b3cdcea87b98bcff55a0863a1b3fde672fd1ac66b667b3077b65cdd95aaeac7325a703e38e5f2855bd2a6f66bedb97bae86e8ace453417f9ceae03fee11ffee1bffb3ec6aabebaf5a318ef0fff73f1f05fd7abf266e6bb7dc13ffcafac61361efeeb7a55decc7cb72ff887ff9535ccc6c37f5dafca9b99eff605fff0bfb286d978f8afeb557933f3ddbee01ffe57d6301b0fff75bd2a6f66bedb17fcc3ffca1a66e3e1bfae57e5cdcc77fb827ff85f59c36c3cfcd7f5aabc99f96e5ff0fff5f96ffd0b58ea205c81f0ca47d9913f3c3a8771e7c5d33918b317d395ba1fa995cbef9117fd6afed5efd0e5a97b79ecf0c97ce1ff3c375b331b877ff857b1f0bf2f7f78c0ffc709fedfc76e82ffb97eaee40f0ff8ff38c1fffbd84df03fd7cf95fce101ff1f27f87f1fbb09fee7fab9923f3ce0ffe304ffef6337c1ff5c3f57f28707fc7f9ce0ff7dec26f89febe74afef080ff8f13fcbf8fdd04ff73fd5cc91f1ef0ff7182fff7b19be07fae9f2bf9c3e315f8fff6ebafbf7e7f6590b39a570ed30c0c36cf3fafaafb417d4f3b00b8dae74cacba006e5adde347ecfdeebdabf661f5325d8debeef98e5abb05fff05fd59815fccfe5afc4ee12fcc37f556356f03f97bf12bb4bf00fff558d59c1ff5cfe4aec2ec13ffc57356605ff73f92bb1bb04fff05fd59815fccfe5afc4ee12fcc37f556356f03f97bf12bb4bf00fff558d59c1ff5cfe4aec2ec13ffc57356605ff73f92bb1bbf4d5f96ffd0b5851f255ed3c64b3756f5af5acfa99ed7727903b6a4579373d1bc02bda75be767ccf479c898f12fcc3ff6710fc3f46f00fff9f41f0ff18c13ffc7f06c1ff6304fff0ff1904ff8f11fcc3ff6710fc3f46f00fff9f41f0ff18c13ffc7f06c1ff6304fff0ff1904ff8f11fcef3b2f4b7f808510420821841042082184104208218410420821841042e838deba81affad778edbf340be266d7f599fe02f4d9aaf686bddbaf67ed29fcafc5ff4882ffe70bfee1ff5504ffcf17fcc3ffab08fe9f2ff887ff5711fc3f5ff00fffaf22f87fbee01ffe5f45f0ff7cc13ffcbf8ae0fff9827ff87f15c1fff305ff9f9bffb76ea1ddff04dd2e9fd1d76a9c1f1f7d45fd75eb3d43af065fb637dee3f6ae7cafae69e79ea89e67e21f217f3e1ff54f43c23ffccfd6817ff85ff55f15fcbf4e1df887ff55ff55c1ffebd4817ff85ff55f15fcbf4e1df887ff55ff55c1ffebd4817ff85ff55f15fcbf4e1df887ff55ff55c1ffebd4817ff85ff55f15fcbf4e1df887ff55ff557d76fec33fc0ca1ade01c9332eb24e9fd1415b3d70bb2f90675db24a16f2a88f99cbd9ee6974f02bffaa9e3a57dd1ea3b310bd776aafd4cde6fdf9f4e32bfe4af00ffff00fff6a1cfefb7eb382ffdc1ffee17fa6ce6c0cfc9f6bc3bf7eefd45ea99bcdc3ff5c9dd918f83fd7867ffddea9bd52379b87ffb93ab331f07fae0dfffabd537ba56e360fff73756663e0ff5c1bfef57ba7f64add6c1efee7eaccc6c0ffb936fcebf74eed95bad93cfccfd5998d81ff736df8d7ef9dda2b75b379f89fab331bf36cfedf32136bd63d24f6a782676cbe9a577ed97c056d04b9fde937ba82b052273edba30aac91b3e3a28ebeafff4eaa8fd53a56ca335abf5df3cc2f030ffbd55f24d5be473da89f43dd8b3dcacfdeb37e22c1ffbbe01ffeb338f8d739f01f0bfe8fd013fecf827f3d0fff67cf19c1ff518e5771f0af73e03f16fc1fa127fc9f05ff7a1efecf9e3382ffa31cafe2e05fe7c07f2cf83f424ff83f0bfef53cfc9f3d6704ff47395ec5c1bfce81ff58f07f849ef07f16fcebf9afc0ff9b5f886dca37e74d54d35903b7393bef3f84f2892e1cefe5fb573d2bb8edc7f2b9ea52ea48ad497d74bb86ea30dac33b72ab1eab39eb61bf85edcbefa55f4f74c8b3b57400f4ca7ab23159ddec92f0e7293b9759dfeafcf86fe6fda377ff6ccf6826bfbfd92f20f8877ff8877ffb0efff00ffff00ffff00ffff00ffff0ef3de13fee13fee3bee01ffee11ffe559ff09ffbc13ffcc3bff6f2fdc33ffcfbbaf00ffffe1dfee1dffb47f1596d350fffe7bee0ff7e7fbffde52f7ff97e4cc837aca4c056a0fa83e1f354cde8bd3a9451dde3a80f78b48eec630f65f1d59ed871efd93df4ca67759dca278aa9726dfeccf988f62e923f57912a3fe5d3b910a3739d79a8ef30736eabefe7736704fff00ffff00ffff00ffff0af04ffef827ff8877ff857f1f00ffff07f7e87ff73cfde13fecfb594e01ffeb3bac701ffaa3ff8af7de01ffee15fc754b9361ffee11ffee1bfca837ff88fde3f2bff6faa116f68c7ade1ed79fc67dffd06f926acbf3a087e036c8dd1838a8ffa8eea783fbb4ee5356294afea35eac3d6b3ff6597aff2f563aa67dbafff56362783c54bf5a2149d275bdb8397edbdafadbe4356cbbefbb3ab72a3f55417aedad3889d4ad12f3beb535dd459aeaa05fff09f09fee1dfae01fe75af511ff00ffff01faf0ffe0fd937fcc3bf1d1be3f00fffaa17f887ff2a57d5827ff8cf04fff06fd700ffbad7a80ff8877ff88fd707ff87ec1bfee1df8e8d71f8877fd50bfcc37f95ab6ac1ff13f9f7ff02567611441fdb2ece3f675ebec1e8406687507d705f436d92efdde6cf5c46d9becc7af9dc99b58ce7d55e3bdfd7d69bfd7ed90591cd67fb9741e5fbf5cf6a2dea3c477d668ad650c577fd7d8eed355abb7fef7cabaa47f8877fdb13fc1fe91aaa78f8877ff887ff8ee01ffeed5ac6f36aaff00fffd6b7d323fcc3bfed09fe8f740d553cfcc33ffcc37f47f00fff762de379b557f8877febdbe911fee1dff604ff47ba862a1efee11ffee1bf23f8ff1afcbf5923db9c6dcc373be6adb1351fcf7e03fc46f84df131d5e1b2cfbe6fb519518f36dfc7a97e7c6f6a5dd541f439ca3b8a51fb6dd71ff5ec7b8d4050ca0ea2daffe85bd95ad939eb5eae6ace7ed3a8bef5c82e80a89eea3df2892e02c591dfc3e85c78f6b24b469d97ec6cc13ffc7bc13ffcc33ffc4773f00fff7ebdf00fff512efcc3bfefd7c7c03ffcc33ffcc3ffd913fee11ffedf9fe11ffee1ffbc2ef8877f1b07ffe7dee11ffee11ffee11ffe7ddca3f9ff594daa829d79ebe53f9a6fa6f256befe608cb9e1ad16e93720bb70aabed4a18ffca27d5097a05a4b676fd5dad49ad59e7b286cac1f8b7c7d5fd13aa3f3e5a15697447586a24b205ab3f2553546bccd8d9401e8d7eb7bf139b3fc44e747ad23ba04e0ffbebf48f00fff4af0bfde7f47f00fffd138fcc3fff0867ff8f77dc13ffcc33ffcfb31f8877ff8877fdf87df0b9b1b09fed7fbef08fee13f1a877ff81fdef00fffbe2ff8877ff8877f3f06fff0ff12fcfff5af7ffd5e15f4861d658b890e46b4216a01d94fef15f592f9ce6af6c376c69467b61655bbca5110762ea8aa874e3f9d6f177967bdd97544ebccf6b3db7be5a3faf6be1527d9a536bb7fb3e71afefb827ff8877f9dabe6ac17fceb31f88fbde11ffee1ff5c1ffee11ffee1bfaae5c7e15fd7ca7aaf7c54dfde17fee13f1a83ffd81bfee11ffecff5e11ffee11ffeab5a7e1cfe75adacf7ca47f5ed7de11ffea331f88fbde11ffebf02ff6f99e1ca07b34ddcdeed987fb7b176f3ecb8fd19cda9bc31a6ea8d71eb9bd552bd2bff08e44ac3dbe78f7da9e0b2b1d11e8f3af66756cb7af93d8e2ebc313772d4b7b1f99defe4fbb3f9fed98fa93db33e1e40bbde6c9fa36f95f5ee633a179acdab2ee9ae5777bfe1ff1c0fffefb1ded3cec1bfee07fee13ff386ff58f00fffd11cfcc3bff287ffb317fcbff702ff712df8877ff83fc2dee1ff905ef0af737c0cfcdff706fff09f09fee13f9a837ff857fef07ff682fff75ee03fae05fff00fff47d8fb2efebffde52f7ff95e811235e463fc8156cf516ea5a8de18f3cf7eacdb7716d7edb393b323ceaed3efb5daf32a268abd29bb84ab7e7c7e94ebbd2320b37a6a2eca8fea477d647b1679ae9c715fdf7a559eddf52a5f2ff8877ff887ff282fea317b8e723b7daa7a630cfee13f9a8bf2a3fa511ff07f9f17f5983d47b99d3e55bd3106fff01fcd45f951fda80ff8bfcf8b7acc9ea3dc4e9faade18837ff88fe6a2fca87ed407fcdfe7453d66cf516ea74f556f8cc13ffc4773517e543fea03feeff3a21eb3e728b7d3a7aa37c6e01ffea3b9283faa1ff501fff779518fd97394dbe953d51b63f00fffd15c941fd58ffa80fffbbca8c7ec39caedf4a9ea8d31f887ff682eca8fea477dc0ff7d5ed463f61ce576fa54f5c6d867e5ffe7eea1f2312adecfab8f5acd8d5a36eef633abd739447e43a383eaeb65975c34af62fc7aa2f1cec1f53d57a044eb51711d886ecf9d0bceef7975d9676b1defdd78abe80c78a9b3e1e315d8d13e45ebcde4f7f838e26f9d9d0755a7732623c13ffcc33ffcab7a333d5473a396ef0dfee17f26de0afe0fd9a78d85ffe3ce0bfee15ff9c1ffb9de4c0fd5dca8e57b837ff89f89b782ff43f66963e1ffb8f3827ff8577ef07fae37d34335376af9dee01ffe67e2ade0ff907dda58f83feebce01ffe951ffc9febcdf450cd8d5abe37f887ff99782bf83f649f3616fe8f3baf67f17ffa17b0d465901db88eb203ad62554defa5c6ab0b21cacf362fea7178441f20f3f6b9917727a7da2f9b3f14ed5d3596f946b1d5a5e76b45cf517c34b772a964b99d5e3a7d7595f510fdecfa449e37c13ffc4763992ffcc37f56c30afee13faa05ff713cfcc33ffcc33ffcc3bff787fffb39f88f7d22cf9be01ffea3b1cc17fee13fab6105fff01fd582ff381efee11ffee11ffee1dffbc3fffd1cfcc73e91e74df0ff64fefffad7bf7e3f2e6ae600743fdeec479ecd9dbdc87600a46aadee45f7a2b2cae23b1765b7b76edf9db1956f59c5deb463dfbbf5562fae8ef74dddcbe25182ffda5fcdc17f3d06ffb9f74df03f9f0bfff0df8985ff5e1ff0dfef01fee17f87e07f3d17fee1bf130bffbd3ee0bfdf03fcc3ff0ec1ff7a2efcc37f2716fe7b7dc07fbf07f887ff1d82fff55cf887ff4e2cfcf7fa80ff7e0f77677bc71f60bd8a761f70e5b9f34364f1ea02ca607ec405ba53cfa8e3f7e7a61ddfea597b94a9bb9e57e8f5a304fff00ffff0ff484ff85f13fc5f13fcd7827ff8877ff87fa427fcaf09feaf09fe6bc13ffcc33ffc3fd213fed704ffd704ffb5e01ffee11ffe1fe909ff6b82ff6b82ff5a9f91ffd31f60fdc81f10ddebeae17b45ed5ec3ea2fbe5715fca321f8eff9dd04ffe8ab09fe7b7e37c13ffa6a82ff9edf4df08fbe9ae0bfe77713fca3af26f8eff9dd04ffe8ab09fe7b7e37c13ffa6a82ff9edf4df08fbe9ae0bfe77713fca315bdf981b1f9aff6115ea99f1fe580fa75becaba67fbb0f1bbd770f3539e9ff58cc07f2df8ff58c1ffe304ffb5e0ff6305ff8f13fcd782ff8f15fc3f4ef05f0bfe3f56f0ff38c17f2df8ff58c1ffe304ffb5e0ff6305ff8f13fcd782ff8f15fc3f4ef05f0bfe3f56f0ff38c17fad9dbdbc4513e32ffb76155c8560c4da7e56eb5673ddbe567b99d1a34098d98f99b828f711e0d8bfd2ac7a59ad7fb5efea2f63a38bbb52779faf7e0ff88f05fff05f09fee3baf05f0bfe63c17f3d0efff7f99d39f887ffab7943f01fd785ff5af01f0bfeeb71f8bfcfefccc13ffc5fcd1b82ffb82efcd782ff58f05f8fc3ff7d7e670efee1ff6ade10fcc775e1bf16fcc782ff7a1cfeeff33b731fc1ff5be74355ff2c59a771dbf4cc0266173b0eb6fa70be4f3f3753cb5e00ea398aade67cdf6aefab6fd1f9a62a26faced9be6563767c06c0995f3ebeaeff8eb7e7ee77ed5c8ad1b7ee9e01fb9cf515f954fb3c72a373efbf31fcbfcfc13ffcc37f9d93c546827ff88ffca27af00ffff00fff99e0ff3c0efff01fcd651ef00fff9d9c2c3612fcc37fe417d5837ff8877ff8cf04ffe771f887ff682ef3807ff8efe464b191e01ffe23bfa81efcc3ff57e7ffa7dffef6b7bf1c135a01a91a5bada1169f6db81a9bddc8cee5e00f41a6ce1e5a00a283121d3a5ba3d34fd4837acee256fdd5dcaa6fe4677f4631158c2b17b53a7bd13953bfb8d47be565e71423b3827ff8efc4adfaabb955dfc8cffe8c62e0ff08bdd47315dbcd99a901ffe79a37c17f5d0bfed704fff0df895bf55773abbe919ffd19c5c0ff117aa9e72ab69b335303fecf356f82ffba16fcaf09fee1bf13b7eaafe6567d233ffb338a81ff23f452cf556c3767a606fc9f6bde04ff752df85f13fcc37f276ed55fcdadfa467ef6671403ff47e8a59eabd86ece4c0df83fd7bc09feeb5af0bf26f8dfcbff4fbffffdef7f894c66c6577256c0ca2e22bf39d18645877127e81dbf6f8dcbb40bd54dd17ed85a996fb47f2b7fa9a9e6abdad965d401a28228bbd8a2deb3f72bcad655e5ccd6198ace5a95d719dfed55d5807ff887ff5ece10fc1f77e3f07fc878f83f640cfcc3ff4aafd93cfce73ef07fce99ad3304ffc7dd38fc1f321efe0f1903fff0bfd26b360fffb90ffc9f7366eb0cc1ff71370eff878c87ff43c6c03ffcaff49acdc37fee03ffe79cd93a43f07fdc8dc3ff21e3e1ff9031f00fff2bbd66f36faa0175507dc3ca3c5a4c7631d8b108d40c727501d97a7e6cac61fc37a3997ed4c7f5ebeaeea58ab1fd77f6d7c39e5d6ad637daa32e88fe5bd88bc79f3755cbf612ed81faa6d5f7f57ed937c860f56761f6d97b29557b6dfff3f16a1fa2ef03ffb9e03fafa1e6e03f7ff65e4af00ffff00fffd11cfcc37f163b62e0ff38f5a27af6b5e11ffe67fb81fff73abe26fcc37fb40e5fd3cf6579f07fa47ef07f9c7a513dfbdaf00fffb3fdc0ff7b1d5f13fee13f5a87afe9e7b23cf83f523ff83f4ebda89e7d6df887ffd97ee0ffbd8eaf09fff01fadc3d7f47359de67e4ffa7dffdee77bf5486aa40b4d95971b580e86354aafacd36a0e313c5a8b5aaba3e4e5d20d9bbdd479f5fed9ff78bf6c4c7aa1ad177ebd6cb6afab576341b1701e163b2fdedee99f78dde55edc83f923a93119395e0bf5f4bad55d5857ff88fde55edc83f12fcc3bff2507ef00fff99e0bf5f4bad55d5857ff88fde55edc83f12fcc3bff2507ef00fff99e0bf5f4bad55d5857ff88fde55edc83f12fcc3bff2507ef00fff99e0bf5f4bad55d5857ff88fde55edc83f12fcc3bff2507ef00fff99e0bf5f4bad55d5edf2ffedaf7ffdeb77dbac873e6b4a994663b6495fcbfb6631d6dbfb7a457567c66c0f51bf6aef94b7ea27dac7680d7edccf45bd643999afefd1ef835a97f5cace42b41ff639da1bd5c791acc1d751f356d95a323eba2c557d45bd65672c8acbbed94ccf591f43f00fffd60bfee11ffee13feacfe6c13ffcc37fbe063feee7e01ffe556df8877ff8877fe50bfff198ed01fee15fd5f679f00fffca1bfee13f8af37d45bdc13ffc2b6f150ffff00ffff05fe564bef07fa4bda93cf8877fe52dcffbdffef6b7efaa695fc4c7641b116daacdcd0e919a571ed17b47d9818b2e81990357016ceb74e3ba97915f57e61fed75e782cb2ecece7a6ecaf67c28f3b6cace40e7dc0d8fea3c1fc7d15a63d73febd7befb9ebe05bf64abfdca7a807ff8877ff8f73dcff616e5c33ffcc33ffcc33ffc675ef00ffff00fff511d9bebe394e01ffefd1a7c2df8877ff8877ff8877f5f57c579efe3807fe501fff0aff2322ff8877ff887ffa88ecdf5714af00fff7e0dbed647f0ff767b19ff8dc1f19f6f66482db68acb166c63b30b41bd2bd9d8f1d3ffe7637dbf76ded7f7fb15c5dbfffc7aaac3a73ea29d8bfab3e3d11aa36f117d8fe8db6640abfdb1736a5f7c4f59be1dcbf6d3d68940f3eb8ceafa5a1d2996fcb33aafb6f7e82cf8358e71bfe6a82f5f43ad11fee11ffee11ffee1bff2b179f07ff6857ff8877ff8f7cff00fffb607f8877fdb3bfcc3bf9f8bfab3e3d11ae11ffee1ff3ed6f76be7e11ffe55ad8ee0ffb81b877ff8877ff8877ff887fff71ea39ff0ffb5f9ffcfbf80d539a4bee90866d54c7668b30fa2eafa79d58ff298e9a3fa181d1f9fe37b546bb0bd57391d656b51f5aa9ed4b8cf8f7edadce87d763d9d33d739a33ed7afcdfad831e5efe7a35eb3fad15876062376b3ef9ad5c9d6a1d60bffe73af00fff51af597df8877ff8afd703ff875cbbef1bfee1df8fab7af00ffff00fff518c1af7f9f00fffd17a94e01ffee11ffee1ffb8eb1dfeebf5c0ff21d7eefb867ff8f7e3aa1efcc33ffcc37f14a3c67d3efcc37fb41ea51f99ff9f154c637218f844fb6e1b89008d0e955a546791d178b531d98119e3dd3efd58d49bda1ffb3c3ca28f14d551eb50ebef8211e57a45176847d97e65bd44974db43e1563eb75d66fc7954fb4e7634c31a0147d577b2e948fda473b17d5f66b867ff8b76351efaaae7def08fecf827ff8877ff8f73dc33ffcfb75c1ffbd57b43e1563ebc13ffcc33ffcc33ffcc33ffcab1af00ffff00ffff00fffaa4f3f16f506ff87aceb6bc0ff7dcff00fff7e5df07fef15ad4fc5d87af00fffafc2ffb7bffef5afdfab05fb66c6bb5f989af78b56f32a3e022adb840852ab6c8dddde6d8cea395b9b5f4bb44635e63f78277fa58e7acfbee1d0b7e41254b99d6fa8e23bca4089409c012e3beb1dd0239f0e8bd13aabbeabbd81ff5eef3646f50cff878c817ff857f32a1efecf63f00fffd93aabbeabbd81ff5eef3646f50cff878c817ff857f32a1efecf63f00fffd93aabbeabbd81ff5eef3646f50cff878c817ff857f32a1efecf63f00fffd93aabbeabbd81ff5eef3646f50cff878c817ff857f32a1efecf63f00fffd93aabbeabbd81ff5eef3646f53cc3ffdb18f4c155e3b7773ba6e6a358b55936662c68f4925d38be7fe5d9f930d145a17af7ded545d4597fb4ef235fd5f063d58599e557b9d9c1b5ebb3dfcdce55e7c3fef47d55f5abb312f947908fb9687fbcd4fafcbcf598f9f6953a6bb26bf1eb80fff318fcc33ffceb3af61dfee17fccc13ffc47f1f00ffff00fff43f00ffff07faeb922f8877fe50bfff06f63e0ffb8abef05fff0efe7e0ff5cc7bec33ffc8f39f887ff281efee11ffee17f08fedfd7ffed6f7ffbdbf7ec7075353c32af4e9d6e2f1d3033efd935afeed18ebd9da971b55ef4cb41f9571759e6b3dacbeedc6c6dd5ba76acf119aa7a82ffb9dc19c17f3e36dbcbee5cf887ffd9dc19c17f3e36dbcbee5cf887ffd9dc19c17f3e36dbcbee5cf887ffd9dc19c17f3e36dbcbee5cf887ffd9dc19c17f3e36dbcbee5cf887ffd9dc19c17f3e36dbcbee5cf887ffd9dc19c17f3e36dbcbee5cf887ffd9dc19c17f3e36dbcbee5cf887ffd9dc195579fffe03ace383e59bdc79889f0dc4230f7be7906779ddf99da054b92b17e595988f54e752ff1105ff73def0bf1ef391827f2df89ff386fff5988f14fc6bc1ff9c37fcafc77ca4e05f0bfee7bce17f3de62305ff5af03fe70dffeb311f29f8d782ff396ff85f8ff948c1bf16fccf79c3ff7acc470afeb5e07fce1bfed7633e52f0aff523f3ff9f3fc0ba0a4596f3a31f30946bf7f9583d8b9ff5025f8d8b72e01f3d53f0df13fca3af28f8ef09fed15714fcf704ffe82b0afe7b827ff41505ff3dc13ffa8a82ff9ee01f7d45c17f4ff08fbea2e0bf27f8ff7a7a897f01ababf1d79fafa447f6f48aebad647bfe6cfddbbf2eeef4fd19bfcf6716fcbfbee01f3d4af0fffa827ff428c1ffeb0bfed1a304ffaf2ff8478f12fcbfbee01f3d4af0fffa827ff428c1ffeb0bfed1a304ffaf2ff8478fd257e4ffcd9b5d69c4fed789afe67ccccc4257d6d2e9c9ab0b6a34a6f6cbfe5561c77766ad57bf7125dbf3cac19c5d53b5b755acd5e8d7f69d79cdae6ff53b5df96633827ff8bf9a0bff73bd7662e1ffff04ffb14757f01f0bfee17f760efeef05ff73bd7662e1ffff04ffb14757f01f0bfee17f760efeef05ff73bd7662e1ffff04ffb14757f01f0bfee17f760efeef05ff73bd7662e1ffff04ffb14757f01f0bfee17f76ee2aff3ffdee77bffb65b6d91d9b7c2576257e5515082b1f6da5f64ccecac5dcf155876da6963f372b7b377ba9ce786735fcdaa3b350b17175fddd585f27ab0bffbd3af00fff9907fc9fe3663c677b7886e0ffdd17fe730ff83fc7cd78cef6f00cc1ffbb2ffce71ef07f8e9bf19cede11982ff775ff8cf3de0ff1c37e339dbc33304ffefbef09f7bc0ff396ec673b6876708fedf7de13ff780ff73dc8ce76c0fcf10fcbffbc27fee01ffe7b819cfd91e9e21f87ff785ffdc03fecf71339eb33d3c438fe6ffdfff0296ff8bbf2af1169ffd9560171eef397390ba7f1dda95eadd3fab9a511f19b4dd8ba8f35dfc98ff36516fdd8bc2efb71a1f6badbe7ba737bb8eceb8edabaaafce4e2727faeeea7c64e76156dd8bd27f1bd59fead7e78f71f83fe433fcc3bfad0bfff75ef07fa4f1f00fff33827ff8b731f00fff4af03f572b8b87ff7b5ff8877f550ffedffb8efce0ff38c5c27f5ff00fff3606fee15f09fee76a65f1f07fef0bfff0afeac1ff7bdf911ffc1fa758f8ef0bfebf3efffff917b03a7047632b311d458bae726c5ee7a25bedadf2cbf64fadcb5e8cfebfce3e648727cbe91cbe2c375a4ba7b76c6cd6275a4bc7b3f33d3b5eded3c7ac5e0cd1bb1def5efed9be6571d1d84a4c47f00ffff00fffb339f00fffd59a551efce779d1d84a4c47f00ffff00fffb339f00fffd59a551efce779d1d84a4c47f00ffff00fffb339f00fffd59afd3bfcc3ff4a9d2a261a83ff3c7ec507fee11ffef3e7a8e64a3efcc37fa7deaa7e04fe7ffafdef7fffcbf8ebb399c3db3d18b6d9eca0fbf1ee26663d5fd9f0ce1e447f49191d820a92e822537f49d7b9a4d5beadee49e7c08d38fb53cda9e7596066e1f4fb54ad67f6e2cf6ade74e597d4552eab5ff6f0af7baee6e05fc7d99f6a4e3dc33ffcc33ffc67dfda7bc1ff51c6c0ff9ae0ff90353b71f6a79a53cff00ffff00fffd9b7f65ef07f9431f0bf26f83f64cd4e9cfda9e6d433fcc33ffcc37ff6adbd17fc1f650cfcaf09fe0f59b313677faa39f50cfff00ffff09f7d6bef05ff471903ff6b7a16ffdffef6b7bf7df7c1917115337bb9a8f9ca2bf2531be67b5439d9a5b73aaffc6dcee8adfa9076cee7abb94cdd8b40ad2bdadb28affaa9fa51eb8ad616ed9b7a8fceb6aa9fed75b757bf3733bf54557d55d3d6cebcaa352a0ff8877ff8877feb65eb6531f07f84fef00fff51af597df8eff9c13ffcc3ff79edc701ff590efcc3bf9a83ffe388d60afff00fffba57bf37f00fff3e67f406fff0afc6e01ffefd1a76f31f9dad47f16fe3a27e7c8ceff308d6a2e2a21e2afed5f7897a573df8dc8affec9eaaf8573df8fca81ff8e7f7bf9fb37dede63f3acfcfe2dfc754f7e1b3f857635dfea33398dd79f09ffbd8f167f0fff318a80eb58ff3f1d565101d6adb8cf7b2b93e3ef28e362683c21fb20a5cbf8e19b07d7dd5975f8b5aa7ea3dbba4b37ea2793f6773a24ba823df67d45374a98edcec1780aa17ad2bbab07c4cc6487446aab3ebfbb7be59ef7e7daa467601455cc33ffcc33ffcfb1cf8877f9b03ffe7b5c03ffcc37f2df83f0bfee11ffee1dffa66bdfbf5a91af07fae51f9c33ffcc3ff3907fee1dfe6c0ff792d5f817feffb55f98f185ae55fd550b9beff2bfc573d784feba1e2c679557b989d9788b948d13aa2f5faf755fefdf347f2efb95ee5bfba63aa67f8ff31f957eb7e24ff9d753c9affe89c7d24ffbe7febeb7b8fd4e1bffb3dd5da54dc67e73ff25fe53f62bce2ff671f9c1d28dba45abc5da82aaa2e06e5dfd9f068aed38307b23b9779ab0394e5456b8fd6636bd9dcccdbe7f8ef59411ff5e86b561e516c7609ab6f10eda73a2fd55ea97ebd6fa4ee1aa29ea2e76c4e5d322a3e5b8bcaf579f00ffff00ffff00fff518fbe26fcc33ffcc37fe401fff00ffff00ffff09f79d877bf7756f07fc875c1ffd937da2bd5aff78d04fff00ffff05fad21eae995f9b76bfceafc6773b3fcab7e1fcd7fb45eefaf7c14ff514f554dc593add7e1dfd6fd51f8aff604fecfb17e1efee11ffef7f1efebd83cdba3dd8bcfca7ff64d66f9b76353fcdffe2708fd0222d0ad79e732e8fef49ba4362d3a68d97cb61eb52115dcd5c1b43e1558597f9d3d56f5a377553702b3b3ff554f362f53b4f76a4c1ede89cb28f2b67333e7245b5f764eaa3d51fd64e7b0c389ed5b7978c13ffcc3ff11d6867ff8b7f5bde01ffee11ffee11ffe6dbcaa1b09fee11ffee1df3f7b7ff8877f5f37ea03fee15f7978c13ffcc3ff11d6867ff8b7f5bde01ffee11ffee11ffe6dbcaa1b09fe7f0cfedf8e60136dd0781f26b6b0fd698bf826fc26d8f7111b7d5cbb70df97fa98d65779a918eb9f791ec15aa2757bcf687d3ed6beabfa192411b8aa47f52dedb3da031557cd45df4eadc58f4567c88f45dfcbaea503a38232db6fbbbeaa7e34efff8b3cfc37b57d46fbab545db2f00fff3656f5eb9fb339f887ffa83efcc37fb63ef8877ff8d7f594e0ff903dc1ff7d7c24f8877ff8af6b7b0ff8877ff8877f3557f507fff00fff7a7df00ffff0afeb29c1ff217b82fffbf848f00ffff05fd7f61ef00fffb3fc7ffbfbdffffedd2fc69b471fddcb1fb4e8b07b6ffb9e79471fa6fb7cb81ea33abe9f6e4ea77f05a3ffe0d1c7aafcc6b8f78cf2ed3ab3efe5bdada2039ead25da836a7d7e8fa2b8ca4729faee516ef5bd3ae3d9f9afd69a9d25b5b668ff548c15fcc33ffcc33ffcc33ffcc37f26f8877ff8877ff88ffb877ff887ff7b6ffb9e79c37fee37c6bd67946fd709fff00ffff01fd5f1393657f9c23ffcc3ffb957f8877ff8bff7b6ef9937fce77e63dc7b46f9769df00ffff0ff7cfefffd0758d5e2aea8ba043a739dc3d8f1ac623a10da39955b5d62b33d756267f6a082a4739866e366cf51055374a9cdf88ef7a155b8a2b33972a25f74d9a53ebb868a8399cb2baab32af8877ff83feedee17f6e0efef358f8877ff8af3dab18f8877f9553cdc37fbfdf9539f8cf63e11ffee1bff6ac62e01ffe554e350ffffd7e57e6e03f8f857ff887ffdab38a817ff85739d53cfcf7fb5d9983ff3c16fee11ffe6bcf2ae623f83ffd01d6cc22af2e76b6d9cec5f2ef453de860ed3a9499ef954bb1aa313c560ff2ee5f24577c67cf4975deaa1e562ff95deaaca973762a4ee03f16fcc37f25f8affb5e19efcc65ebea7854827ff8af04ff75df2be39db96c5d1d8f4af00fff95e0bfee7b65bc3397adabe35109fee1bf12fcd77daf8c77e6b275753c2ac13ffc5782ffbaef95f1ce5cb6ae8e4725f887ff4af05ff7bd32de99cbd6d5f1a804fff05f09feebbe57c63b73d9ba3a1e95e01ffe2bc17fddf7ca78672e5b57c7a3d223beed7ffe2708bb45d5e6af6ce62ead80317b98af5c8059eea32edbac97994be69997c947f83ee37c76ea3ff29b67755762e17fbebf2817fe3fd617fee763e17fbebf2817fe3fd617fee763e17fbebf2817fe3fd617fee763e17fbebf2817fe3fd617fee763e17fbebf2817fe3fd617fee763e17fbebf2817fe3fd617fee763e17fbebf2817fe3fd617fee763e17fbebf2817fe3fd617fe8fe958f89fef2fcafd770dfb07589dc53c6253570ec447c3b3a2ace7ceba5e65cd3bfbf88cdfd1ea5997cf337e39c2ff6305ff8ff5fa08c1ff73eafa58f8ff38c1ffbbe0ff39757d2cfc7f9ce0ff5df0ff9cba3e16fe3f4ef0ff2ef87f4e5d1f0bff1f27f87f17fc3fa7ae8f85ff8f13fcbf0bfe9f53d7c7c2ffc709fedf05ffcfa9eb63e1ffe304ffef82ffe7d4f5b15f85fff25fc04208218410420821841042082184104208218410420821a4f536fe226be5afc9b29cdd7eddf81d1e57e246ece92fdd3ef15f790e5d392b234fedcd98dba1477ccf1d79cfd2eaf987ff3d712316fe753efc3f56f00fff8f12fcc3bfca9d9ddb9503ffcf15fcc3bfca9d9ddb9503ffcf15fcc3bfca9d9ddb9503ffcf15fcc3bfca9d9ddb9503ffcf15fcc3bfca9d9ddb9503ffcf15fcc3bfca9d9ddb9503ffcf15fcc3bfca9d9ddb95f315f87fb3ffa45727c16ae4a858ebd76ddef750f5a3e2338f6ecd6eadccd3afbf5b27d347c33fd6a0beaded2deb73ec8d8fe9ee6f54673c473e1e8ad57fc22ecb5bf9a5dbddb72cd7af6b46f00fff5dc13ffcfbdc2816fee13ff2807f1d03fff09f09fee15fe5c27fcfa35b13feb5e01ffe7d6e140bfff01f79c0bf8e817ff8cf04fff0af72e1bfe7d1ad09ff5af00fff3e378a857ff88f3ce05fc7c0ff8fc7ffdb48f0133340548b98693e3bc4bb2ea00a940ce2f133eb65cc5797420473d45f76e0558f95ef5545df385b6f1613adbbb31fd519537b370babcdeb9ccbea1c6531dd3eeccf2e2b7e5fe11ffe5704fff7fda83825f8cffda277f8877ffb0efff0bf22f8df23f8bfef47c529c17fee17bdc33ffcdb77f887ff15c1ff1ec1ff7d3f2a4e09fe73bfe81dfee1dfbec33ffcaf08fef708feeffb51714af09ffb45eff00ffff61dfee17f45afc4ff4fbffffdef7fe92cacf341a3b8ec52f071d1c7cc3e72b5a1d1d88c460faa97ce07a9bcaff495bd5f51f52dab5f08bb2f7c5f77b76ff79cda1cf5bec2937adfb1af1d46e0bf16fcdf8fc13ffc77bde01ffe77f604fff09ff9c2ff1ec17f9da3dee1bfd7838a83fff9beb2f72b82ff3a47bdc37faf071507fff37d65ef5704ff758e7a87ff5e0f2a0efee7fbcadeaf08feeb1cf50effbd1e541cfccff795bd5f11fcd739ea1dfe7b3da838f89fef2b7bbf22f8af73d43bfcf77a50713f1aff6f1148d181548badfeb2d1ffa59ecff73ebe861d57f16363ecb87d9f397cd11e647f6d68c7d55cd7bbba04d458b4a7d5a13892beacb7f751fb90fd859fede7d99ab9346ebdaaf314e5661c54bf986c8ccf55fd547d44fff97ad118fcc773f00fff2a17fe75eff00fff3e16fecf82ff7b3ff887ffdd82fffb7af07f1c9df5d867f8877f950bffba77f8877f1f0bff67c1ffbd1ffcc3ff6ec1ff7d3df83f8ece7aec33fcc3bfca857fdd3bfcc3bf8f85ffb3e0ffdeef47e5ffdbdffffef7efb6882fe817eb8da2b8084cbb412ad66fe08a47165bc5d9772bbff1aadfe890cfd650527dab9a6a8dd15e46fbaef6bfda3fd55337379a575076d75b5d5e55ff6adc2baa91ed7fe7ccfa7a2b6b896a76fa1bcf557c1607fff779333594e01ffe6704ff3a16feb527fce7b9f00fff761cfef33ee11ffee11ffe3bb9f0af3daa3a99e05fc7c2bff684ff3c17fee1df8ec37fde27fcc33ffcc37f2717feb547552713fceb58f8d79ef09fe7c23ffcdbf15dfc7ffbdffffddff2eb66c66ac32270a205670720f28f7aac36bbbb5191a7ea3dbb70a2b564075aad4ff9f875457be1c73a1e51dd953efc73c7b3fb2d6dae5775695667d0be47e7a3f289cedab7c62f9b4811632a4ef5342bf8877fbf2f337df8e78e27fcc7827ff8877ff887ff738ff00fffaa175fd7f70fff753d3f66fb867ff88ffaf0cf1d4ff88f05fff00ffff00fffe71ee11ffe552fbeaeef1ffeeb7a7eccf60dfff01ff5e19f3b9ef01f0bfee11ffee11ffecf3d7e36feeffe052cbf98686cf543668afc6780e9f4d5f920d1e6753cb30fab62aabdae7aca2e329b975d2c570e77e7b07aaf680fa2bdcabe41d573e4df015d8d5bffe3a87ff145bd2865fb9bd5e8c4fbb86a6df00fff512cfcc33ffc6b4ff8877ff89f17fc1fed31f83ff7968dc33ffcdb3a56f00fffb60efcc33ffc1fb236fcdffb1f07fcef12fc1fed31f83ff7968dc33ffcdb3a56f00fffb60efcc33ffc1fb236fcdffb1f07fcefd28fcaffbfff05ace830d931d564641a41a82e0e351ff9aa3ebcd4e1ea7cb46813673e6a770f1404aa76b4261b3b036996a3f26d9eafbfd2a7ea7be64289d66a7dfd989f53bd46bd473d2875fcbb7b1e8d777e112aafce2566df3bfdc3bfaee9f3e01ffea3de7d3cfcc33ffcc37f340efff00ffff00ffff0efd7e8e3e1ff5ef07fdcad693cab5e3bfdc3bfaee9f3e01ffea3de7d3cfcc33ffcc37f340efff00ffff00ffff0efd7e8e3e1ff5ef07fdcad693cab5e3bfdc3bfaee9f34afec7bf809581a08aa8c6fd023a50596fbf717e31dd9cac96f7f8165c544ad5a1b77151afb3bede235bab9d5735b2f8e8f05460471782add9f1f57d76d79f79647d453d649792f2cceaf8fcc8335aabf588ce687591cc3200fff00ffff06fe78e03fe7d1df8877ff887ffa89fa8d7595fef01fff00ffff00ffff07f1cf0af72e01ffea3feb235fbdce3807ff83f641df8877ff887ffa89fa8d7595fef01fff00ffff00ffff07f1cf0af723e13ff77ff02962a1699fa4564739da66dcd2eb8bebe3ae0ddcb21f39a013e7bf7357c7fd13e44cf4a9dbea231ef93ed51671f330fdb63e7fbfaefe173a3be3bdfb182bd923ac7aa77356fdf956774067cdef7e28256de631efe732ff83fc27ee11ffee11ffea318f8877ff8877f3f6f7b847ff8b735e11ffeb37af00fffaa3efcc33ffcc33ffc1fad3efc98f781ff9ee01ffe557df8877ff8877ff83fcfdb1ee11ffe6dcd1f89fff45fc052ea1ed0e840f886babeddfeaa1e663dbcdff088ea54792beba83cabb8998b7ce6728cfac90e7b7641467edd75441746b5ee68cd59cfab8cccfc92c9c09ef9ee6a2cba1ce13ff71b1e519d2a0ffee11ffee11ffecf79915f771df0dfabe36bc27f4ff00fff9187f7837ff85779f00ffff00ffff07fce8bfcbaeb80ff5e1d5f13fe7b827ff88f3cbc1ffcc3bfca837ff8877ff887ff735ee4d75d07fcf7eaf89af0dfd323f8fff7bf80654dae5c0c19d4d561180dced4b87a8955feb3f3ddf557fb7145d95ece7c9f95ba3bf62ddaab47f6de51e78cae9effaaee0a97abf1f00fffab75e11ffee11ffe67e7e1ff283de15fe7c1ffb9d64df0bf57f0dff7e88cc33ffcab39f8d7b9f0aff3e0ff5ceb26f8df2bf8ef7b74c6e11ffed51cfceb5cf8d779f07fae7513fcef15fcf73d3ae3f00fff6aee99fccfe4c27fdfa3330efffff7fe9f3fc05ad5ec41baeab3539d8bf18adf8e1c0fc6cc61f5fe3b217ee4657ea5af1d79d5b9d8f19d57f67625f6d1df03fe63bf1d39f0dfafbd2b0ffefb82ffd86f470efcf76befca83ffbee03ff6db9103fffddabbf2e0bf2ff88ffd76e4c07fbff6ae3cf8ef0bfe63bf1d39f0dfafbd2b0ffefb82ffd86f470efcf76befca83ffbee03ff6db9103fffddabbf2e0bf2ff88ffd76e4c07fbff6ae3cf8ef0bfe63bf8e96ff006bc7865587f0a6677f14d587bd00abb8d5f9abfd75c767e33ef2a29dd1abf5a3b4bbc78f5c33fcebb8d5f9abfd75c767e3e07f9fe0bfef01ff73fd75c767e3e07f9fe0bfef01ff73fd75c767e3e07f9fe0bfef01ff73fd75c767e3e07f9fe0bfef01ff73fd75c767e3e07f9fe0bfef01ff73fd75c767e3e07f9fe0bfef01ff73fd75c767e3e07f9fe0bfef01ff73fd75c767e3e07f9fe0bfeff1a3f07ff707589fe1107f663d6b7f7fb4eff84aebb5bdeceaeb59bfdce1ffb182ffc708fee1ff3308fe1f23f887ffcf20f87f8ce01ffe3f83e0ff31827ff8ff0c82ffc708fee1ff3308fe1f23f887ffcf20f87f8ce01ffe3f83e0ff31827ff8ff0c52fbfb665fec64f49c8dbda25ea9cf47f5e27d57ebdcf23e6abfaed48d723fe2dc56dca8d86adfa3383bbe43f0ff58c17f5e7b772efccf09fe1f2bf8cf6befce85ff39c1ff6305ff79edddb9f03f27f87face03fafbd3b17fee704ff8f15fce7b577e7c2ff9ce0ffb182ffbcf6ee5cf89f13fc3f56f09fd7de9d0bff7382ffc70afef3dabb73e17f4ef0ff58a95edea2e0f14f80f9e76cac5bf8999b32d3e78cba6b588564265ead71e652ca7caa9c28b6fbcdc778b686d9b5784f1ba7c62af9cb68656fb3fc5b4f9d73eae3ae9cedaa57f8cf05ff471a0bffb117fcc37f37af23f8cf3de15ff751d552cfd958b716fcf7f33a82ffdc13fe751f552df59c8d756bc17f3faf23f8cf3de15ff751d552cfd958b716fcf7f33a82ffdc13fe751f552df59c8d756bc17f3faf23f8cf3de15ff751d552cfd958b716fcf7f33a82ffdc13fe751f552df59c8d756bc17f3faf23f8cf3de15ff751d552cfd958b716fcbfeb6dd6b02ad451f7c288c63c10b3bd5d0168a8f3616f7e236ef672f3f19dcb485d34594e5437f3ead4b5eb3e8a7e326fbb77d1c595e56571d9f788eaa875753d6d8cdfa3eafbf86f9fc577bff5e86136ef6add51bb930fffe738f83fca5cf8af05ff755c26f88febc2ffd9d3c6c03ffcdb988e3ffcc33ffcc33ffc1f652efcd782ff3a2e13fcc775e1ffec6963e01ffe6d4cc71ffee11ffee11ffe8f3217fe6bc17f1d9709fee3baf07ff6b431f00fff433ffdfef7bfff254aae9ed57be653c5cf6cc0ea81c92052f0457133f12a77b576b746e7bb64bd740ed855efe842993d53bb62777b74f3ae9c27ef11bdabf12e8bf07f8e9b8957b9abb5bb35e07f3e76b707fcc37f94bb5abb5b03fee763777bc03ffc47b9abb5bb35e07f3e76b707fcc37f94bb5abb5b03fee763777bc03ffc47b9abb5bb35e07f3e76b707fcc37f94bb5abb5b03fee763777bc03ffc47b9abb5bb35e07f3e76b707fcc37f94bb5abb5b03fee763777bc03ffc47b943fffe03ac61b4627605d4e191bd8fb108d6d95afe2f31bd6ff4578955fdd9fdf37bae7a8bf2a2183f17ed6515d3f1cf72ae9e09e5d9895985bb9a9b594ff43d55cc6cadea7b567da95f7e760efee13f8a55ef510efcc3ff8c47f63ec6e0ff3c0efff731f09ff705fff00ffff373f01f7bc2ffd9abdb0bfcf704ff711efccfd5827ff89f9d83ffd813fecf5edd5ee0bf27f88ff3e07fae16fcc3ffec1cfcc79ef07ff6eaf602ff3dc17f9c07ff73b55e8dffb7d194fda7d14680fd691387467c05bb9a8f2e826883fc3fe5552dd6f6e56b5959df6af37d5c5657e5463e2a46d550bdabef64bf69b64fd95e66df3ffaae5dc0d57ab31efc3a54acdfaf2ade5f94518c3de37e2e03383b23d19cddbb6a5fb20bd5c6797fb50ef887ffa816fcc37fe409ff715d950bfff09fcdc13ffcab7d50f92306feefd7a262e0ffdd1ffee1dfe62b5f3b06fff01f79c27f5c57e5c23ffc6773f00fff6a1f54fe8881fffbb5a818f87ff7877ff8b7f9cad78ec13ffc479ef01fd755b9f00fffd91cfcff78fcbf656623306b68c4d8717f387c8c6a24fa987e2ef2f3b90a846aadd121f09bae2e21bb0ebb67514db596c8dff7e1c722dfe11d5d16d177b5befefbfb7a51ef6adf7defaa4ef48dfd7c067d06a70762f8aaefa42e7efb5dfdf7f03119f8d9983d6f6a0fb24b5e7de3e82c451ed91e581f1ba36ac23ffcdb18f88f7bf13dc1fffd3cfcc33ffcc37fe407ff67dfe10dfff7f3f619fee1dfaf0bfee1dff7ebbde1ff3e46d5847ff8b731f01ff7e27b82fffb79f8877ff887ffc80ffecfbec31bfeefe7ed33fcc3bf5f17fcc3bfefd77bc3ff7d8caa09ff35ff3ffb01fbac0e6b75018ca211eceac0471fd4d7565efe80fa79df4f245f233bdc598fead9f75a495d36fe20cdc26df73d8bf735fd9afd4510cd7b6f5557ed913a7bd919cb40b7b5fd73b46e750622f8a29aca375b5374e6a2bd565c46fed977f2b5aadee11ffee11ffee11ffee13f5e87f2cdd604fff06f05fff06ffb857ff887ff43ae07fee1ff38e03febd5e6c03ffcc3ff59f00fffbe07f8877f1f53f5a89e7daf95e01ffe6dbff00ffff07fc8f5c03ffc1fc7e7e5ffdb3ffef18fefea234770441f362aae0e875d4474c8a2cbaa037374118c795b5badcdd7f2b951ef516eb4f6d5b9ec52c92e99283faa711c47b97eef191df4ea40db986c7d36373b7f6a3e9ab3f3be976ead6a4faa6f12ad4ff550f56a1571ec63e01ffe7dcc71c07fb756b527f00fff51bfb373f00ffff00fff518ff00fff7e0efedff36d6db5365fcbe746bd47b9f00ffff00fff51bfb373f00ffff00fff518ff00fff7e0efedff36d6db5365fcbe746bd47b9f00ffff00fff51bfb373f00ffff0ff35f87f5306d987b8fd549b30c65541ebdf01b19af71ba13c6ddfb637bbbe686dea236507cdf6a50e90edd1f7e5fb8de4fd54dd689f6c6dffcd466ff6bf0a56bb7fd1deabf5da9f3edef7ace6ec5a8fe308cfa8f7caf6365a8fdacf116f1988a472d5f7aef88a7cd4f78a6215dffe4cc03ffcc33ffcdbb14e7fbe8eea03fee11ffee1dffaf9f94cf00fff7eadc701fff00fffd11ac7b88d87ff23ec15fee11ffee11ffee11ffee11ffee11ffee15fc5c33ffcc33ffcc33ffcfbbe66f9ffcfbf80654d55731e1275e86dae8f89fc7c7cb430751978757ca23c55a7ba1495b20ba473087c3fdecb1f8e6acdd6275a5b677fb36fe273a3fd54f3d17bf5add57aaafc68cd59ff333d44b99dde7c6cb547aa9e3a6bd9398b3ce01ffebde0bfee21ca857ff8f773f00fff991ffc1fa77af00fffde07fee1bfaa6f7da23c5507feef6b7b4ff8877ff83fd21af00ffff00fff7e0efee13ff383ffe3540ffee1dffbc03ffc57f5ad4f94a7eac0ff7d6def09fff00fff475aa3e23ffc03ac59ad1c32151b355c7958cd40d9550675e760d8bea279abea90fbe76e7d75e9546b53dfa83a689def13f55afd92a82edeaa5e0545b4eeaccf2c7ec62fbb00aff059d51cb5e0bfaed719f731b6af68de0afee1dff607ffb98715fcc7f5e11ffe55cd510bfeeb7a9d711f63fb8ae6ade01ffe6d7ff09f7b58c17f5c1ffee15fd51cb5e0bfaed719f731b6af68de0afee1dff607ffb98715fcc7f5e11ffe55cd510bfeeb7a9d711f63fb8ae6ade01ffe6d7ff09f7b58c17f5c1ffee15fd51cb5e0bfaed719f731b6afbb5e6f7f80756c68e44a7e07f08ecfd53e66e3d587efac6fa6ceea07ce7cb2c3647d6640cac63a7963dcd6efe4cefc2279a4bab5572f85950bb8fb8b6146f07f3f6605fff05f8dc33ffccfd481ff3c17fed773e01ffe331ff8ef0bfed7f3e13f8e85ff3c17fed773e01ffe331ff8ef0bfed7f3e13f8e85ff3c17fed773e01ffe331ff8ef0bfed7f3e13f8e85ff3c17fed773e0ffe3f9977f80757503669ac80e4934d7b9186eda01dd951e660efd954b7a36b67b09747a5db918329f67ea232fec8fd2aecb0dfee1df3ec3ffe710fccf09fee1ff2b09fee704fff0ff9504ff73827ff8ff4a82ff39c13ffc7f25c1ff9ce01ffebf92e07f4ef00fff5f49f03f27f887ffaf24f89fd3a3f93ffd01d6d5c3f7ecc3bb73b37769e5e3cc7ee0476a472fbb405f89edc43f7bbf9f714eb35f68ddf5c1ff75c13ffcab7a37c1ff5ec1ff7ec1ff7ec1ff6304fffb05fffb05ff8f11fcef17fcef17fc3f46f0bf5ff0bf5ff0ff18c1ff7ec1ff7ec1ff6304fffb05fffb05ff8f11fcef17fcef17fc3f469f9dffe9ff09c2cfa2db02c7c7f98a5a599fcdd9b53f8fdae78eef88f9e86ffdd5cfda6714fce739f0bf4ff0ff7a82ff3c07fef709fe5f4ff09fe7c0ff3ec1ffeb09fef31cf8df27f87f3dc17f9e03fffb04ffaf27f8cf73e07f9fe0fff504ff790efcef13fcbf9ee03fcf81ff7d82ffd7d367f9266ff6c5fed5d6edf93f7fa5f52dfe6b2e1b93c5a9f847c8fe056034f70c457bb9e2a372af1eae6efea81ff53f2ec0ddeaf4d7bd7ca3fe779dc599bd54cf558e8fbd7296baf3f0bfa78ff10cff7382fff738f8ef0bfe7b827ff8cfe6e17f4f1fe319fee704ffef71f0df17fcf704fff09fcdc3ff9e3ec633fccf09fedfe3e0bf2ff8ef09fee13f9b87ff3d7d8c67f89f13fcbfc7c17f5ff0df13fcc37f360fff7bfa18cf51ed9ffef0873ffc1225fb03ee37af73f857b47a50fdbb3f44b6d799c3722546ed5fe59141b7e3003fe22066175c34967de7d91eb3fcee25addeaff6e17b583dcf1d9f5ddf15fef7c5c07f3c06ff7b7de0fffe1dfee39e760bfe8f341efee7ebfbe76ece7887ffb8a7dd82ff238d87fff9fafeb99b33dee13fee69b7e0ff48e3e17fbebe7feee68c77f88f7bda2df83fd278f89fafef9fbb39e31dfee39e760bfe8f341efee7ebfbe76ece7887ffb8a7dd82ff238d87fff9fafeb99b33dee13fee69b7e0ff48e3e17fbebe7feee68cf7cfc0ffbfff006be6b0559b9e1d9cec12511b95d5ca36d7c77595c5aa4b715633978cba402af83228d5feab5a91e74cefaaf65545808de7ecfb47004467aeaaaff6b4ca897ca25fb6f63b6775562f283507fff01f79cef4ae6a5f15fcc37fe607fff01fe5c13ffc7704ffb1e74cefaaf655c13ffc677ef00fff511efcc37f47f01f7bcef4ae6a5f15fcc37fe607fff01fe5c13ffc7704ffb1e74cefaaf655c13ffc677ef00fff511efcc37f471fc1ff4ffff33ffff34b1410254531d14664e3bbe50fad3ac8d11a2a881ff101ba87bf3af8aac623f67787aacbc47f23f51d6fb2bf946c6ce7b27ad6de64ebb06336365a7fe4abdea3b1aa4ee501ffd77bb3eff07f1e877ff85f15fcc33ffcc763559dca03feaff766dfe1ff3c0efff0bf2af887ff6ff01f8e55752a8f6ff07fb937fbfe0dfe4fe3dfe01ffe1705fff00ffff15855a7f280ffebbdd977f83f8fc33ffcaf0afee11ffee3b1aa4ee501ffd77bb3efdffef9cf7f7e8f0ef2f7e22fddfc5faaa94623ef3197355a7db499fe54bcefdbe7755479fb679b63d5d9fbec1b557b7a24fdceae5fed5b3776767f95d76a5e751164e763a8cb88f7bcda7bf4eefb54f5fc3ad4f7807ff85735220ff8877ff8877ff8877ff887ff6e2cfcaff51ebdfb3ee15ff7a7e2e1bfee35f2807ff8877ff8877ff8877ff8efc6c2ff5aefd1bbef13fe757f2a1efeeb5e230ff8877ff8877ff8877ff85fe7ff6d4c5407add388ffc8d6cb7b28ffdb4f1f67dfadb7f2f4f13e478d4787d3ff17d5f11f39ea57cd8f7578d91eac97f7b171d97e7a2fdfb3cf8bdebdf711acb77b8e3a5e56d137cffcd4a5a07c555dcb4694a7ceeea8e91950bda9baeaddfaaa3e321fc5a25f1bfcc37ff70caa9fd118fcdf7bc17fec0fffc7a907eb05fff00ffff0efdfe13ff6837ff8f7f57c4df83f5aefdefb38e01ffeefebc03ffcc33ffcc33ffcdb5a997fe66505fff03f72e11ffe3bbdc13ffcfb7abe26fc1fad77ef7d1cf00ffff775e07f8eff6ffff8c73fbefb049ba43635fa207e53a3dcece3461fb10228dac8eca7f78efa57ebce0e5556471d842826dbe7acb61a9f99eb7c2bb5d73e37f2ebec8fda87ea3c64b5bd4f96affcabb3a5f2d4a5965d549167a71f9f1ff51ce546bdc33ffc4731f00fffbe27b516d5a3aad9ed4dd5857ff8877ff8877ff8877ff887fffbf19939f8877ff8877ff83fd2f5c2ff71ea49ad45f5a86a767b5375e11ffee11ffee11ffee11ffee1ff7e7c660efee11ffe9fc3ffcfbe78f431a3857be3ec908f771be337263a24b31fc3cbd7569beed7a2f2a377351601a8545d0edea3fad86a3fabba7eccfb4752df2f8bad7cfd37b2fd65974e77ffa36fa2cea5f78bbcbbdfcff6ec73ed73f43d558e7af71ed1c504fff0efe7e11ffee15fe747ef6a0cfee37af00ffff07fce857ff85731f00fff761efee1df7b6675e01ffeabba7eccfb47827ff8877ff8877ff887ffb817f8877fdbb3cfb5cff00fff4af00fff3e47bd7b0ff87f4dfedf5483d1465923151b6d5ee6e51b8e1af57dfa03a30e68f6116d8eefd17bf81c5bd3ef4bb457919ff78eded54151bed9feaa7ed4b35775a1a86f31ebe1f3563d6c7e068dbf64a37d53fbaece7a7519f9fad945eb6bf95c15db6154fdb251fdc0ffbd87cf817fed09fff0efebc03ffc773c7c1efcc3bf12fcbf7b295ff87fcf877ff887ffb806fcc3bfea07feef3d7c0efc6b4ff8877f5f07fee1bfe3e1f3e01ffe95e0ffdd4bf9c2ff7b3efcc33ffcc735e01ffe553ff07fefe17376f2ffed9ffffce777d5947f56f3d5bb5fe068c22bdbf0ec6047f3aa5604b4edd17f34ef9ff9f8b5447d658734f2f4f1592daf6c2ef38ee27c8fd101cbd61de56771516eb59fd137cdde676a55ef33739df948b3be766dab3eddf5c13ffcc37f3dd7998f04fff00ffff0dff5b56b5bf5e9ae0ffee11ffeebb9ce7c24f8877ff887ffaeaf5ddbaa4f777df00ffff05fcf75e623c13ffcc33ffc777dedda567dbaeb837ff887ff7aae331f09fee11ffee1bfeb6bd7b6ead35d1ffcc7fcff9c99a90fe1cdfdc6551be11b8da416aee2238fce61cb7a1b73763d7e53bb7078ef68df6c4c956f7bea5cd699ba00f983eaf3a3b8eca28efae8bc4773d9f7f1fbd6dd3f7501447ba5d6a7bebbaa1f3d2b45975f74ceb3791be79fe11ffe471cfcc33ffcc33ffcc33ffcc3bfcd877ff887ff5cf00fff599ff0ff3e1ff55079fbbc2c16fee11ffeefe3fc33fcc3ff88837ff8877ff8877ff8877ff8b7f9f00ffff09fcb7f837fff0b589999fac8a30915171d8accd3cf59ff6c032bc8ba3daa7c75a9647191ff4cad2c26eaa113db5db7f219ea7edbd9fea28babfad63300cff659fd9251bdd9183faee6aada513fd95c5627db3ff83f425ff83ffee307fff00fff3a2ef287ffbc3ff8d7b5a37ee0bff6817ff887ffdc17fee1bfe33304fff03fde551df887ff195ff8cf6b47fdc07fed03fff00fffb92ffcc37fc76708fee17fbcab3af00fff33bef09fd78efa81ffdae733f17ffa9f20ec244531d5d80ab095ffcc7c276ee660de3473b83a3dcdc0d0f1bb1a63636fea5c82bb2eead5b3f28873367bf1ce9cafd55f105774f59cc13ffcc33ffccf8cc17fdd13fcafe7ecc88df2e1bf8e817ff857e3f0afe7e07f4ef00fff8fce8df2e1bf8e817ff857e3f0afe7e07f4ef00fff8fce8df2e1bf8e817ff857e3f0afe7e07f4ef00fff8fce8df2e1bf8e81fff9357efbc73ffef17d7683b226af5e12bb2f992bda75195ec97984765fce8ff4e8f8becabe3e4a8f5cdfca0509fff0bfabce0e5ff8dfe30dff7d7ff8df5367872ffceff186ffbe3ffcefa9b3c317fef778c37fdf1ffef7d4d9e10bff7bbce1bfef0fff7beaecf085ff3ddef0dff787ff3d7576f8c2ff1e6ff8effbc3ff9e3a3b7ce17f8f37fcf7fde17f4f9d1dbef0bfc71bfefbfebbf897ff021642082184104208218410420821841042082184104208a15a6fdd40fb975baff8d786afd2d357e96325ffd5ce45d4cf4c9f2a76c7deeef2781697f0df13fcbf8ee07f9fe0bf27f87f1dc1ff3ec17f4ff0ff3a82ff7d82ff9ee0ff7504fffb04ff3dc1ffeb08fef709fe7b82ffd711fcef13fcf704ffaf23f8df27f8ef09fe5f47f0bf4ff0dfd36c1f6f5d93db3f9fa59eab82ab876de444f9765ef5e473aaf7ac872cdef791c566f3ddcb42bd57df6b46c36ff59f58abbca3e7ee9eecb808549f916f771fbbe769ecedf05d5dcff0f05ceeb888e0ff1c03ffb5e0bf8e81ffb91ee01ffe3b1e3382ffba4f554f3dc33ffc776bc23ffccff600fff0dff19811fcd77daa7aea19fee1bf5b13fee17fb607f887ff8ec78ce0bfee53d553cff00fffdd9af00fffb33dc03ffc773c6604ff759faa9e7a86ff6bfcfff4873ffce1173fe90fc758902f6663b32259d3aa71ef9b2dbe53df5e283b0f68e760cc7a66f32bfb7de5305ecdcde6a378fb9e5d28d119b90a9ef25d8137ab3f735977e266545d14f05f0bfe7bb9d97c146fdfe11ffe332ff8aff3e11ffea378f85f13fcf772b3f928debec33ffc675ef05fe7c33ffc47f1f0bf26f8efe566f351bc7d877ff8cfbce0bfce877ff88fe2e17f4df0dfcbcde6a378fb0efff09f79c17f9d0ffff01fc5c3ff9a76f2ffa61af586636c2cc2bf8fb8ccc7cea978e53b622a102b59dfa8afacbfcccfc7aabf4cac1441eff7a093affaf6fb5779d99ad577ecacd3af45c9fba83336bcecbcea2303a3ead7fba8785b3bba40bafeea79c4656bea5eec11975d6ee13ff7f3b1f01f7bc23ffcc3bfae09fff0efe3e01ffee11ffed53cfc1f279f48f00ffff00ffff07fb4bce0ff3d07fee13ff3f3b1f01f7bc23ffcc3bfae09fff0efe3e01ffee11ffed53cfc1f279f48afccff9b328ec65473be19a5ac41ff1ec11f1dfae8e2a9fa8e0eb45a87ba10fda1a900519ed9dad4878c0ebc82461deaa8be1d53e0aff8f9bea3f8e8e250bda9bad5999dd1cc37b3b5fcded858f5cdd41e76f75379447be37bf2179b5a4b3606fff00fffe75af00ffff00ffff0af05ff87f4ccd606ff878c877ff8877ff85775b2b18ee01ffee11ffe67e6e11ffe33bf48f00ffff00fffd1dae0ff90f1f00ffff00fffaa4e36d611fcc3ffb3f93ffd4f10568ac08de6d5e2fd7c27bf1af3b5b20da8364e8d8dc3bb0277e782e9f465fb53074cf965176d94ab2e8cec1bf9b1ee856af7b5a30e4855ee6c5ed54306aecfb1ef2bf5b3f3179d595fd38fcd9e67f8877ff887ff680cfee3be3a71f09fe7d89ffeb9933b9b57f500fff05f09fee3dee07fae36fcc33ffcc3bf9acb62e1ff3e0efee17f45f00fffb382ffb837f89fab0dfff00ffff0afe6b258f8bf8f837ff85f11fcc3ffacb25ede8e49551ff5f6ec9b1c63d106d9390fa4f752007898550f99bc87fa38d5078d0e92ea53cddbb5ab03a2d6d381df7afa780564e46df7c57ada3a6abd7eccc67b3f1f93f5e2ebaa5cb5669b577db3c8d7abea61e64c47174047d62762d0d659b94ce03fcf3f1a7daa79bb76f8cfd709ff5af00fff5ef00fffa326fc6bc1ffbd9ff286ff58f00ffff00fff512efcc33ffcc3bf8af775e11ffee13f5f27fc6bc13ffc7bc13ffc8f9af0af05fff77eca1bfe63c13ffc2ff3ffcf7ffef3bb07de2e24da28fbae7efa0695b283ebe7544fbe5e76a1f8dea37eaaf547fdfa1a95879a531ebe5eb40eb517be66e6d3eda58a89ce43766e548fd519aacec7f7c62facc8b373ce325571558fd519b01e91b7cff535a25ee01ffeb35ee01ffe552efcc33ffc1fa9bf9d877ff8877ff8573dc23ffc47f5e0ff08e3e11ffee11ffee11ffea37566827ff8f77ebe5f95e705fff00ffff00fffe77abed72806fee1df0afee1ff4bf2ffaf7ffdeb7bb740a7f96cfeae70b08028660688ec635d59cbcc47b4ef43aa97e843656b5173c3bf0b8defcd2aead3ce1d477d69faf8683d9d8b3baaafc09ff976dd73abd619ad2d7bf6be299ccdf3307bb6a31af07fa4b1f00ffff01ffbc23ffc475ed97ae0ff3c06fff05fcdc33ffc47fed57ae07f7e2df00ffff01ffbc23ffc475ed97ae0ff3c06fff05fcdc33ffc47fed57ae07f7e2df00ffff01ffbc23ffc475ed97ae0ff3c06ff3f06fffff917b0bc895775f0b30d8d8adb98cee67c1730ccaafb2166fa52dedf8b4bb61bfb3db978fd9855e712edaef17b03862867e63077bc46bf33fb92ed67f41e415e9d9fecbb5a9fa3586fb64e1ba37aabf65ec546debec64c1df8877ff8877fd5a35a6b25f83fd7b16356f0affb807fbdcec8dbd798a903fff00ffff0af7a546bad04ffe73a76cc0afe751ff0afd71979fb1a3375e01ffee11ffe558f6aad95e0ff5cc78e59c1bfee03fef53a236f5f63a60efcc33ffcc3bfea51adb512fc9febd8312bf8d77dc0bf5e67e4ed6bccd481ff9a7ff90758957915b333b602468defec23fa10b3f5fc61ec5e4cd18550f9443d44dec3afebd1fd2e555e373f8ba9f668a646a5ce65e3eb56fd3e4b33bff43ab9510cfc1fa1cff0827f3ddfa95d79c37f5eb71aebe64631f07f843ec30bfef57ca776e50dff79dd6aac9b1bc5c0ff11fa0c2ff8d7f39dda9537fce775abb16e6e1403ff47e833bce05fcf776a57def09fd7adc6bab9510cfc1fa1cff0827f3ddfa95d79c37f5eb71aebe64631f07f843ec30bfef57ca776e50dff79dd6aac9b1bc5c0ff11fa0c2ff8d7f39dda9537fce775abb16e6e1403ff47e833bce05fcf776a57def09fd7adc6bab9510cfc1fa1cff01ab16fc784fc21f3efbeb1a368c2fe1cf3d141561ba116e563bcb7cdb1bd463ddfc63dd42a26f388bcd47ac77854438d473e331ed1be461edeabba943afed51aaa3db03fd59aec59b2dfa0d3873f37aa075bb3b387debfaaa1beb33ad747d0b3efad7366223ff5eeeb64f9f00fff597c565b8dc33ffcc3ff51d6cffacaea7b0ff83ff700fff09f09fecfe3517c565b8dc33ffcc3ff51d6cffacaea7b0ff83ff700fff09f09fecfe3517c565b8dc33ffcc3ff51d6cffacaea7b0ff83ff700fff09f09fecfe3517c565b8dc33ffcc3ff51d6cffacaea7b0ff83ff700fff09f49f1ffed5ffffad7d457c83678561daf5df5aa8f931d9a999cacce8ef9aafe4adcca1e573957fbf3e3d5fb55d90bfca6d97332e655ee6cef336bf37dabfa3b05fff5785467c77c557f250efee1bf2bf8afc7a33a3be6abfa2b71f00fff5dc17f3d1ed5d9315fd55f89837ff8ef0afeebf1a8ce8ef9aafe4a1cfcc37f57f05f8f477576cc57f557e2e01ffebb82ff7a3caab363beaabf1207fff0df15fcd7e3519d1df355fd9538f887ffaee0bf1e8feaec98afeaafc4c13ffc7705fff5785447cdff7fa204b6a9ad3ab44e0000000049454e44ae426082','image/png',490462,2,'','','{}'); +INSERT INTO reaction VALUES(1,1722097094,1,'memos/4','👍'); +INSERT INTO reaction VALUES(2,1722097100,1,'memos/4','🔥'); +INSERT INTO reaction VALUES(3,1722097101,1,'memos/4','+1'); +INSERT INTO system_setting VALUES ('MEMO_RELATED', '{"contentLengthLimit":8192,"enableAutoCompact":true,"enableComment":true,"enableLocation":true,"defaultVisibility":"PUBLIC","reactions":["👍","💛","🔥","👏","😂","👌","🚀","👀","🤔","🤡","❓","+1"]}', ''); diff --git a/store/store.go b/store/store.go new file mode 100644 index 0000000..6b7e826 --- /dev/null +++ b/store/store.go @@ -0,0 +1,57 @@ +package store + +import ( + "time" + + "github.com/usememos/memos/internal/profile" + "github.com/usememos/memos/store/cache" +) + +// Store provides database access to all raw objects. +type Store struct { + profile *profile.Profile + driver Driver + + // Cache settings + cacheConfig cache.Config + + // Caches + workspaceSettingCache *cache.Cache // cache for workspace settings + userCache *cache.Cache // cache for users + userSettingCache *cache.Cache // cache for user settings +} + +// New creates a new instance of Store. +func New(driver Driver, profile *profile.Profile) *Store { + // Default cache settings + cacheConfig := cache.Config{ + DefaultTTL: 10 * time.Minute, + CleanupInterval: 5 * time.Minute, + MaxItems: 1000, + OnEviction: nil, + } + + store := &Store{ + driver: driver, + profile: profile, + cacheConfig: cacheConfig, + workspaceSettingCache: cache.New(cacheConfig), + userCache: cache.New(cacheConfig), + userSettingCache: cache.New(cacheConfig), + } + + return store +} + +func (s *Store) GetDriver() Driver { + return s.driver +} + +func (s *Store) Close() error { + // Stop all cache cleanup goroutines + s.workspaceSettingCache.Close() + s.userCache.Close() + s.userSettingCache.Close() + + return s.driver.Close() +} diff --git a/store/test/README.md b/store/test/README.md new file mode 100644 index 0000000..988c631 --- /dev/null +++ b/store/test/README.md @@ -0,0 +1,13 @@ +# Store tests + +## How to test store with MySQL? + +1. Create a database in your MySQL server. +2. Run the following command with two environment variables set: + +```go +DRIVER=mysql DSN=root@/memos_test go test -v ./test/store/... +``` + +- `DRIVER` should be set to `mysql`. +- `DSN` should be set to the DSN of your MySQL server. diff --git a/store/test/activity_test.go b/store/test/activity_test.go new file mode 100644 index 0000000..3ac3d8c --- /dev/null +++ b/store/test/activity_test.go @@ -0,0 +1,34 @@ +package teststore + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func TestActivityStore(t *testing.T) { + ctx := context.Background() + ts := NewTestingStore(ctx, t) + user, err := createTestingHostUser(ctx, ts) + require.NoError(t, err) + create := &store.Activity{ + CreatorID: user.ID, + Type: store.ActivityTypeMemoComment, + Level: store.ActivityLevelInfo, + Payload: &storepb.ActivityPayload{}, + } + activity, err := ts.CreateActivity(ctx, create) + require.NoError(t, err) + require.NotNil(t, activity) + activities, err := ts.ListActivities(ctx, &store.FindActivity{ + ID: &activity.ID, + }) + require.NoError(t, err) + require.Equal(t, 1, len(activities)) + require.Equal(t, activity, activities[0]) + ts.Close() +} diff --git a/store/test/attachment_test.go b/store/test/attachment_test.go new file mode 100644 index 0000000..a653a0d --- /dev/null +++ b/store/test/attachment_test.go @@ -0,0 +1,63 @@ +package teststore + +import ( + "context" + "testing" + + "github.com/lithammer/shortuuid/v4" + "github.com/stretchr/testify/require" + + "github.com/usememos/memos/store" +) + +func TestAttachmentStore(t *testing.T) { + ctx := context.Background() + ts := NewTestingStore(ctx, t) + _, err := ts.CreateAttachment(ctx, &store.Attachment{ + UID: shortuuid.New(), + CreatorID: 101, + Filename: "test.epub", + Blob: []byte("test"), + Type: "application/epub+zip", + Size: 637607, + }) + require.NoError(t, err) + + correctFilename := "test.epub" + incorrectFilename := "test.png" + attachment, err := ts.GetAttachment(ctx, &store.FindAttachment{ + Filename: &correctFilename, + }) + require.NoError(t, err) + require.Equal(t, correctFilename, attachment.Filename) + require.Equal(t, int32(1), attachment.ID) + + notFoundAttachment, err := ts.GetAttachment(ctx, &store.FindAttachment{ + Filename: &incorrectFilename, + }) + require.NoError(t, err) + require.Nil(t, notFoundAttachment) + + var correctCreatorID int32 = 101 + var incorrectCreatorID int32 = 102 + _, err = ts.GetAttachment(ctx, &store.FindAttachment{ + CreatorID: &correctCreatorID, + }) + require.NoError(t, err) + + notFoundAttachment, err = ts.GetAttachment(ctx, &store.FindAttachment{ + CreatorID: &incorrectCreatorID, + }) + require.NoError(t, err) + require.Nil(t, notFoundAttachment) + + err = ts.DeleteAttachment(ctx, &store.DeleteAttachment{ + ID: 1, + }) + require.NoError(t, err) + err = ts.DeleteAttachment(ctx, &store.DeleteAttachment{ + ID: 2, + }) + require.ErrorContains(t, err, "attachment not found") + ts.Close() +} diff --git a/store/test/idp_test.go b/store/test/idp_test.go new file mode 100644 index 0000000..a204f33 --- /dev/null +++ b/store/test/idp_test.go @@ -0,0 +1,60 @@ +package teststore + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func TestIdentityProviderStore(t *testing.T) { + ctx := context.Background() + ts := NewTestingStore(ctx, t) + createdIDP, err := ts.CreateIdentityProvider(ctx, &storepb.IdentityProvider{ + Name: "GitHub OAuth", + Type: storepb.IdentityProvider_OAUTH2, + IdentifierFilter: "", + Config: &storepb.IdentityProviderConfig{ + Config: &storepb.IdentityProviderConfig_Oauth2Config{ + Oauth2Config: &storepb.OAuth2Config{ + ClientId: "client_id", + ClientSecret: "client_secret", + AuthUrl: "https://github.com/auth", + TokenUrl: "https://github.com/token", + UserInfoUrl: "https://github.com/user", + Scopes: []string{"login"}, + FieldMapping: &storepb.FieldMapping{ + Identifier: "login", + DisplayName: "name", + Email: "email", + }, + }, + }, + }, + }) + require.NoError(t, err) + idp, err := ts.GetIdentityProvider(ctx, &store.FindIdentityProvider{ + ID: &createdIDP.Id, + }) + require.NoError(t, err) + require.NotNil(t, idp) + require.Equal(t, createdIDP, idp) + newName := "My GitHub OAuth" + updatedIdp, err := ts.UpdateIdentityProvider(ctx, &store.UpdateIdentityProviderV1{ + ID: idp.Id, + Name: &newName, + }) + require.NoError(t, err) + require.Equal(t, newName, updatedIdp.Name) + err = ts.DeleteIdentityProvider(ctx, &store.DeleteIdentityProvider{ + ID: idp.Id, + }) + require.NoError(t, err) + idpList, err := ts.ListIdentityProviders(ctx, &store.FindIdentityProvider{}) + require.NoError(t, err) + require.Equal(t, 0, len(idpList)) + ts.Close() +} diff --git a/store/test/inbox_test.go b/store/test/inbox_test.go new file mode 100644 index 0000000..da79c50 --- /dev/null +++ b/store/test/inbox_test.go @@ -0,0 +1,54 @@ +package teststore + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func TestInboxStore(t *testing.T) { + ctx := context.Background() + ts := NewTestingStore(ctx, t) + user, err := createTestingHostUser(ctx, ts) + require.NoError(t, err) + const systemBotID int32 = 0 + create := &store.Inbox{ + SenderID: systemBotID, + ReceiverID: user.ID, + Status: store.UNREAD, + Message: &storepb.InboxMessage{ + Type: storepb.InboxMessage_MEMO_COMMENT, + }, + } + inbox, err := ts.CreateInbox(ctx, create) + require.NoError(t, err) + require.NotNil(t, inbox) + require.Equal(t, create.Message, inbox.Message) + inboxes, err := ts.ListInboxes(ctx, &store.FindInbox{ + ReceiverID: &user.ID, + }) + require.NoError(t, err) + require.Equal(t, 1, len(inboxes)) + require.Equal(t, inbox, inboxes[0]) + updatedInbox, err := ts.UpdateInbox(ctx, &store.UpdateInbox{ + ID: inbox.ID, + Status: store.ARCHIVED, + }) + require.NoError(t, err) + require.NotNil(t, updatedInbox) + require.Equal(t, store.ARCHIVED, updatedInbox.Status) + err = ts.DeleteInbox(ctx, &store.DeleteInbox{ + ID: inbox.ID, + }) + require.NoError(t, err) + inboxes, err = ts.ListInboxes(ctx, &store.FindInbox{ + ReceiverID: &user.ID, + }) + require.NoError(t, err) + require.Equal(t, 0, len(inboxes)) + ts.Close() +} diff --git a/store/test/memo_relation_test.go b/store/test/memo_relation_test.go new file mode 100644 index 0000000..56565bf --- /dev/null +++ b/store/test/memo_relation_test.go @@ -0,0 +1,62 @@ +package teststore + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/usememos/memos/store" +) + +func TestMemoRelationStore(t *testing.T) { + ctx := context.Background() + ts := NewTestingStore(ctx, t) + user, err := createTestingHostUser(ctx, ts) + require.NoError(t, err) + memoCreate := &store.Memo{ + UID: "main-memo", + CreatorID: user.ID, + Content: "main memo content", + Visibility: store.Public, + } + memo, err := ts.CreateMemo(ctx, memoCreate) + require.NoError(t, err) + require.Equal(t, memoCreate.Content, memo.Content) + relatedMemoCreate := &store.Memo{ + UID: "related-memo", + CreatorID: user.ID, + Content: "related memo content", + Visibility: store.Public, + } + relatedMemo, err := ts.CreateMemo(ctx, relatedMemoCreate) + require.NoError(t, err) + require.Equal(t, relatedMemoCreate.Content, relatedMemo.Content) + commentMemoCreate := &store.Memo{ + UID: "comment-memo", + CreatorID: user.ID, + Content: "comment memo content", + Visibility: store.Public, + } + commentMemo, err := ts.CreateMemo(ctx, commentMemoCreate) + require.NoError(t, err) + require.Equal(t, commentMemoCreate.Content, commentMemo.Content) + + // Reference relation. + referenceRelation := &store.MemoRelation{ + MemoID: memo.ID, + RelatedMemoID: relatedMemo.ID, + Type: store.MemoRelationReference, + } + _, err = ts.UpsertMemoRelation(ctx, referenceRelation) + require.NoError(t, err) + // Comment relation. + commentRelation := &store.MemoRelation{ + MemoID: memo.ID, + RelatedMemoID: commentMemo.ID, + Type: store.MemoRelationComment, + } + _, err = ts.UpsertMemoRelation(ctx, commentRelation) + require.NoError(t, err) + ts.Close() +} diff --git a/store/test/memo_test.go b/store/test/memo_test.go new file mode 100644 index 0000000..e401387 --- /dev/null +++ b/store/test/memo_test.go @@ -0,0 +1,118 @@ +package teststore + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/usememos/memos/store" + + storepb "github.com/usememos/memos/proto/gen/store" +) + +func TestMemoStore(t *testing.T) { + ctx := context.Background() + ts := NewTestingStore(ctx, t) + user, err := createTestingHostUser(ctx, ts) + require.NoError(t, err) + memoCreate := &store.Memo{ + UID: "test-resource-name", + CreatorID: user.ID, + Content: "test_content", + Visibility: store.Public, + } + memo, err := ts.CreateMemo(ctx, memoCreate) + require.NoError(t, err) + require.Equal(t, memoCreate.Content, memo.Content) + memoPatchContent := "test_content_2" + memoPatch := &store.UpdateMemo{ + ID: memo.ID, + Content: &memoPatchContent, + } + err = ts.UpdateMemo(ctx, memoPatch) + require.NoError(t, err) + memo, err = ts.GetMemo(ctx, &store.FindMemo{ + ID: &memo.ID, + }) + require.NoError(t, err) + require.NotNil(t, memo) + memoList, err := ts.ListMemos(ctx, &store.FindMemo{ + CreatorID: &user.ID, + }) + require.NoError(t, err) + require.Equal(t, 1, len(memoList)) + require.Equal(t, memo, memoList[0]) + err = ts.DeleteMemo(ctx, &store.DeleteMemo{ + ID: memo.ID, + }) + require.NoError(t, err) + memoList, err = ts.ListMemos(ctx, &store.FindMemo{ + CreatorID: &user.ID, + }) + require.NoError(t, err) + require.Equal(t, 0, len(memoList)) + + memoList, err = ts.ListMemos(ctx, &store.FindMemo{ + CreatorID: &user.ID, + VisibilityList: []store.Visibility{store.Public}, + }) + require.NoError(t, err) + require.Equal(t, 0, len(memoList)) + ts.Close() +} + +func TestMemoListByTags(t *testing.T) { + ctx := context.Background() + ts := NewTestingStore(ctx, t) + user, err := createTestingHostUser(ctx, ts) + require.NoError(t, err) + memoCreate := &store.Memo{ + UID: "test-resource-name", + CreatorID: user.ID, + Content: "test_content", + Visibility: store.Public, + Payload: &storepb.MemoPayload{ + Tags: []string{"test_tag"}, + }, + } + memo, err := ts.CreateMemo(ctx, memoCreate) + require.NoError(t, err) + require.Equal(t, memoCreate.Content, memo.Content) + memo, err = ts.GetMemo(ctx, &store.FindMemo{ + ID: &memo.ID, + }) + require.NoError(t, err) + require.NotNil(t, memo) + + memoList, err := ts.ListMemos(ctx, &store.FindMemo{ + PayloadFind: &store.FindMemoPayload{ + TagSearch: []string{"test_tag"}, + }, + }) + require.NoError(t, err) + require.Equal(t, 1, len(memoList)) + require.Equal(t, memo, memoList[0]) + ts.Close() +} + +func TestDeleteMemoStore(t *testing.T) { + ctx := context.Background() + ts := NewTestingStore(ctx, t) + user, err := createTestingHostUser(ctx, ts) + require.NoError(t, err) + memoCreate := &store.Memo{ + UID: "test-resource-name", + CreatorID: user.ID, + Content: "test_content", + Visibility: store.Public, + } + memo, err := ts.CreateMemo(ctx, memoCreate) + require.NoError(t, err) + require.Equal(t, memoCreate.Content, memo.Content) + err = ts.DeleteMemo(ctx, &store.DeleteMemo{ + ID: memo.ID, + }) + require.NoError(t, err) + ts.Close() +} diff --git a/store/test/migrator_test.go b/store/test/migrator_test.go new file mode 100644 index 0000000..8b22eb2 --- /dev/null +++ b/store/test/migrator_test.go @@ -0,0 +1,17 @@ +package teststore + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetCurrentSchemaVersion(t *testing.T) { + ctx := context.Background() + ts := NewTestingStore(ctx, t) + + currentSchemaVersion, err := ts.GetCurrentSchemaVersion() + require.NoError(t, err) + require.Equal(t, "0.25.1", currentSchemaVersion) +} diff --git a/store/test/reaction_test.go b/store/test/reaction_test.go new file mode 100644 index 0000000..142c6a8 --- /dev/null +++ b/store/test/reaction_test.go @@ -0,0 +1,48 @@ +package teststore + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/usememos/memos/store" +) + +func TestReactionStore(t *testing.T) { + ctx := context.Background() + ts := NewTestingStore(ctx, t) + + user, err := createTestingHostUser(ctx, ts) + require.NoError(t, err) + + contentID := "test_content_id" + reaction, err := ts.UpsertReaction(ctx, &store.Reaction{ + CreatorID: user.ID, + ContentID: contentID, + ReactionType: "💗", + }) + require.NoError(t, err) + require.NotNil(t, reaction) + require.NotEmpty(t, reaction.ID) + + reactions, err := ts.ListReactions(ctx, &store.FindReaction{ + ContentID: &contentID, + }) + require.NoError(t, err) + require.Len(t, reactions, 1) + require.Equal(t, reaction, reactions[0]) + + err = ts.DeleteReaction(ctx, &store.DeleteReaction{ + ID: reaction.ID, + }) + require.NoError(t, err) + + reactions, err = ts.ListReactions(ctx, &store.FindReaction{ + ContentID: &contentID, + }) + require.NoError(t, err) + require.Len(t, reactions, 0) + + ts.Close() +} diff --git a/store/test/store.go b/store/test/store.go new file mode 100644 index 0000000..5ae05e5 --- /dev/null +++ b/store/test/store.go @@ -0,0 +1,124 @@ +package teststore + +import ( + "context" + "fmt" + "log/slog" + "net" + "os" + "testing" + + // sqlite driver. + _ "modernc.org/sqlite" + + "github.com/joho/godotenv" + + "github.com/usememos/memos/internal/profile" + "github.com/usememos/memos/internal/version" + "github.com/usememos/memos/store" + "github.com/usememos/memos/store/db" +) + +func NewTestingStore(ctx context.Context, t *testing.T) *store.Store { + profile := getTestingProfile(t) + dbDriver, err := db.NewDBDriver(profile) + if err != nil { + slog.Error("failed to create db driver", slog.String("error", err.Error())) + } + resetTestingDB(ctx, profile, dbDriver) + + store := store.New(dbDriver, profile) + if err := store.Migrate(ctx); err != nil { + slog.Error("failed to migrate db", slog.String("error", err.Error())) + } + return store +} + +func resetTestingDB(ctx context.Context, profile *profile.Profile, dbDriver store.Driver) { + if profile.Driver == "mysql" { + _, err := dbDriver.GetDB().ExecContext(ctx, ` + DROP TABLE IF EXISTS migration_history; + DROP TABLE IF EXISTS system_setting; + DROP TABLE IF EXISTS user; + DROP TABLE IF EXISTS user_setting; + DROP TABLE IF EXISTS memo; + DROP TABLE IF EXISTS memo_organizer; + DROP TABLE IF EXISTS memo_relation; + DROP TABLE IF EXISTS resource; + DROP TABLE IF EXISTS tag; + DROP TABLE IF EXISTS activity; + DROP TABLE IF EXISTS storage; + DROP TABLE IF EXISTS idp; + DROP TABLE IF EXISTS inbox; + DROP TABLE IF EXISTS reaction;`) + if err != nil { + slog.Error("failed to reset testing db", slog.String("error", err.Error())) + panic(err) + } + } else if profile.Driver == "postgres" { + _, err := dbDriver.GetDB().ExecContext(ctx, ` + DROP TABLE IF EXISTS migration_history CASCADE; + DROP TABLE IF EXISTS system_setting CASCADE; + DROP TABLE IF EXISTS "user" CASCADE; + DROP TABLE IF EXISTS user_setting CASCADE; + DROP TABLE IF EXISTS memo CASCADE; + DROP TABLE IF EXISTS memo_organizer CASCADE; + DROP TABLE IF EXISTS memo_relation CASCADE; + DROP TABLE IF EXISTS resource CASCADE; + DROP TABLE IF EXISTS tag CASCADE; + DROP TABLE IF EXISTS activity CASCADE; + DROP TABLE IF EXISTS storage CASCADE; + DROP TABLE IF EXISTS idp CASCADE; + DROP TABLE IF EXISTS inbox CASCADE; + DROP TABLE IF EXISTS reaction CASCADE;`) + if err != nil { + slog.Error("failed to reset testing db", slog.String("error", err.Error())) + panic(err) + } + } +} + +func getUnusedPort() int { + // Get a random unused port + listener, err := net.Listen("tcp", "localhost:0") + if err != nil { + panic(err) + } + defer listener.Close() + + // Get the port number + port := listener.Addr().(*net.TCPAddr).Port + return port +} + +func getTestingProfile(t *testing.T) *profile.Profile { + if err := godotenv.Load(".env"); err != nil { + t.Log("failed to load .env file, but it's ok") + } + + // Get a temporary directory for the test data. + dir := t.TempDir() + mode := "prod" + port := getUnusedPort() + driver := getDriverFromEnv() + dsn := os.Getenv("DSN") + if driver == "sqlite" { + dsn = fmt.Sprintf("%s/memos_%s.db", dir, mode) + } + return &profile.Profile{ + Mode: mode, + Port: port, + Data: dir, + DSN: dsn, + Driver: driver, + Version: version.GetCurrentVersion(mode), + } +} + +func getDriverFromEnv() string { + driver := os.Getenv("DRIVER") + if driver == "" { + driver = "sqlite" + } + return driver +} diff --git a/store/test/user_setting_test.go b/store/test/user_setting_test.go new file mode 100644 index 0000000..ce954ac --- /dev/null +++ b/store/test/user_setting_test.go @@ -0,0 +1,28 @@ +package teststore + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func TestUserSettingStore(t *testing.T) { + ctx := context.Background() + ts := NewTestingStore(ctx, t) + user, err := createTestingHostUser(ctx, ts) + require.NoError(t, err) + _, err = ts.UpsertUserSetting(ctx, &storepb.UserSetting{ + UserId: user.ID, + Key: storepb.UserSetting_GENERAL, + Value: &storepb.UserSetting_General{General: &storepb.GeneralUserSetting{Locale: "en"}}, + }) + require.NoError(t, err) + list, err := ts.ListUserSettings(ctx, &store.FindUserSetting{}) + require.NoError(t, err) + require.Equal(t, 1, len(list)) + ts.Close() +} diff --git a/store/test/user_test.go b/store/test/user_test.go new file mode 100644 index 0000000..14131b4 --- /dev/null +++ b/store/test/user_test.go @@ -0,0 +1,56 @@ +package teststore + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + "golang.org/x/crypto/bcrypt" + + "github.com/usememos/memos/store" +) + +func TestUserStore(t *testing.T) { + ctx := context.Background() + ts := NewTestingStore(ctx, t) + user, err := createTestingHostUser(ctx, ts) + require.NoError(t, err) + users, err := ts.ListUsers(ctx, &store.FindUser{}) + require.NoError(t, err) + require.Equal(t, 1, len(users)) + require.Equal(t, store.RoleHost, users[0].Role) + require.Equal(t, user, users[0]) + userPatchNickname := "test_nickname_2" + userPatch := &store.UpdateUser{ + ID: user.ID, + Nickname: &userPatchNickname, + } + user, err = ts.UpdateUser(ctx, userPatch) + require.NoError(t, err) + require.Equal(t, userPatchNickname, user.Nickname) + err = ts.DeleteUser(ctx, &store.DeleteUser{ + ID: user.ID, + }) + require.NoError(t, err) + users, err = ts.ListUsers(ctx, &store.FindUser{}) + require.NoError(t, err) + require.Equal(t, 0, len(users)) + ts.Close() +} + +func createTestingHostUser(ctx context.Context, ts *store.Store) (*store.User, error) { + userCreate := &store.User{ + Username: "test", + Role: store.RoleHost, + Email: "test@test.com", + Nickname: "test_nickname", + Description: "test_description", + } + passwordHash, err := bcrypt.GenerateFromPassword([]byte("test_password"), bcrypt.DefaultCost) + if err != nil { + return nil, err + } + userCreate.PasswordHash = string(passwordHash) + user, err := ts.CreateUser(ctx, userCreate) + return user, err +} diff --git a/store/test/workspace_setting_test.go b/store/test/workspace_setting_test.go new file mode 100644 index 0000000..18939fd --- /dev/null +++ b/store/test/workspace_setting_test.go @@ -0,0 +1,31 @@ +package teststore + +import ( + "context" + "testing" + + "github.com/stretchr/testify/require" + + storepb "github.com/usememos/memos/proto/gen/store" + "github.com/usememos/memos/store" +) + +func TestWorkspaceSettingV1Store(t *testing.T) { + ctx := context.Background() + ts := NewTestingStore(ctx, t) + workspaceSetting, err := ts.UpsertWorkspaceSetting(ctx, &storepb.WorkspaceSetting{ + Key: storepb.WorkspaceSettingKey_GENERAL, + Value: &storepb.WorkspaceSetting_GeneralSetting{ + GeneralSetting: &storepb.WorkspaceGeneralSetting{ + AdditionalScript: "", + }, + }, + }) + require.NoError(t, err) + setting, err := ts.GetWorkspaceSetting(ctx, &store.FindWorkspaceSetting{ + Name: storepb.WorkspaceSettingKey_GENERAL.String(), + }) + require.NoError(t, err) + require.Equal(t, workspaceSetting, setting) + ts.Close() +} diff --git a/store/user.go b/store/user.go new file mode 100644 index 0000000..8a2a940 --- /dev/null +++ b/store/user.go @@ -0,0 +1,159 @@ +package store + +import ( + "context" +) + +// Role is the type of a role. +type Role string + +const ( + // RoleHost is the HOST role. + RoleHost Role = "HOST" + // RoleAdmin is the ADMIN role. + RoleAdmin Role = "ADMIN" + // RoleUser is the USER role. + RoleUser Role = "USER" +) + +func (e Role) String() string { + switch e { + case RoleHost: + return "HOST" + case RoleAdmin: + return "ADMIN" + case RoleUser: + return "USER" + } + return "USER" +} + +const ( + SystemBotID int32 = 0 +) + +var ( + SystemBot = &User{ + ID: SystemBotID, + Username: "system_bot", + Role: RoleAdmin, + Email: "", + Nickname: "Bot", + } +) + +type User struct { + ID int32 + + // Standard fields + RowStatus RowStatus + CreatedTs int64 + UpdatedTs int64 + + // Domain specific fields + Username string + Role Role + Email string + Nickname string + PasswordHash string + AvatarURL string + Description string +} + +type UpdateUser struct { + ID int32 + + UpdatedTs *int64 + RowStatus *RowStatus + Username *string + Role *Role + Email *string + Nickname *string + Password *string + AvatarURL *string + PasswordHash *string + Description *string +} + +type FindUser struct { + ID *int32 + RowStatus *RowStatus + Username *string + Role *Role + Email *string + Nickname *string + + // The maximum number of users to return. + Limit *int +} + +type DeleteUser struct { + ID int32 +} + +func (s *Store) CreateUser(ctx context.Context, create *User) (*User, error) { + user, err := s.driver.CreateUser(ctx, create) + if err != nil { + return nil, err + } + + s.userCache.Set(ctx, string(user.ID), user) + return user, nil +} + +func (s *Store) UpdateUser(ctx context.Context, update *UpdateUser) (*User, error) { + user, err := s.driver.UpdateUser(ctx, update) + if err != nil { + return nil, err + } + + s.userCache.Set(ctx, string(user.ID), user) + return user, nil +} + +func (s *Store) ListUsers(ctx context.Context, find *FindUser) ([]*User, error) { + list, err := s.driver.ListUsers(ctx, find) + if err != nil { + return nil, err + } + + for _, user := range list { + s.userCache.Set(ctx, string(user.ID), user) + } + return list, nil +} + +func (s *Store) GetUser(ctx context.Context, find *FindUser) (*User, error) { + if find.ID != nil { + if *find.ID == SystemBotID { + return SystemBot, nil + } + if cache, ok := s.userCache.Get(ctx, string(*find.ID)); ok { + user, ok := cache.(*User) + if ok { + return user, nil + } + } + } + + list, err := s.ListUsers(ctx, find) + if err != nil { + return nil, err + } + if len(list) == 0 { + return nil, nil + } + + user := list[0] + s.userCache.Set(ctx, string(user.ID), user) + return user, nil +} + +func (s *Store) DeleteUser(ctx context.Context, delete *DeleteUser) error { + err := s.driver.DeleteUser(ctx, delete) + if err != nil { + return err + } + s.userCache.Delete(ctx, string(delete.ID)) + return nil +} diff --git a/store/user_setting.go b/store/user_setting.go new file mode 100644 index 0000000..3c48b97 --- /dev/null +++ b/store/user_setting.go @@ -0,0 +1,441 @@ +package store + +import ( + "context" + + "github.com/pkg/errors" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/types/known/timestamppb" + + storepb "github.com/usememos/memos/proto/gen/store" +) + +type UserSetting struct { + UserID int32 + Key storepb.UserSetting_Key + Value string +} + +type FindUserSetting struct { + UserID *int32 + Key storepb.UserSetting_Key +} + +func (s *Store) UpsertUserSetting(ctx context.Context, upsert *storepb.UserSetting) (*storepb.UserSetting, error) { + userSettingRaw, err := convertUserSettingToRaw(upsert) + if err != nil { + return nil, err + } + userSettingRaw, err = s.driver.UpsertUserSetting(ctx, userSettingRaw) + if err != nil { + return nil, err + } + + userSetting, err := convertUserSettingFromRaw(userSettingRaw) + if err != nil { + return nil, err + } + if userSetting == nil { + return nil, errors.New("unexpected nil user setting") + } + s.userSettingCache.Set(ctx, getUserSettingCacheKey(userSetting.UserId, userSetting.Key.String()), userSetting) + return userSetting, nil +} + +func (s *Store) ListUserSettings(ctx context.Context, find *FindUserSetting) ([]*storepb.UserSetting, error) { + userSettingRawList, err := s.driver.ListUserSettings(ctx, find) + if err != nil { + return nil, err + } + + userSettings := []*storepb.UserSetting{} + for _, userSettingRaw := range userSettingRawList { + userSetting, err := convertUserSettingFromRaw(userSettingRaw) + if err != nil { + return nil, err + } + if userSetting == nil { + continue + } + s.userSettingCache.Set(ctx, getUserSettingCacheKey(userSetting.UserId, userSetting.Key.String()), userSetting) + userSettings = append(userSettings, userSetting) + } + return userSettings, nil +} + +func (s *Store) GetUserSetting(ctx context.Context, find *FindUserSetting) (*storepb.UserSetting, error) { + if find.UserID != nil { + if cache, ok := s.userSettingCache.Get(ctx, getUserSettingCacheKey(*find.UserID, find.Key.String())); ok { + userSetting, ok := cache.(*storepb.UserSetting) + if ok { + return userSetting, nil + } + } + } + + list, err := s.ListUserSettings(ctx, find) + if err != nil { + return nil, err + } + if len(list) == 0 { + return nil, nil + } + if len(list) > 1 { + return nil, errors.Errorf("expected 1 user setting, but got %d", len(list)) + } + + userSetting := list[0] + s.userSettingCache.Set(ctx, getUserSettingCacheKey(userSetting.UserId, userSetting.Key.String()), userSetting) + return userSetting, nil +} + +// GetUserAccessTokens returns the access tokens of the user. +func (s *Store) GetUserAccessTokens(ctx context.Context, userID int32) ([]*storepb.AccessTokensUserSetting_AccessToken, error) { + userSetting, err := s.GetUserSetting(ctx, &FindUserSetting{ + UserID: &userID, + Key: storepb.UserSetting_ACCESS_TOKENS, + }) + if err != nil { + return nil, err + } + if userSetting == nil { + return []*storepb.AccessTokensUserSetting_AccessToken{}, nil + } + + accessTokensUserSetting := userSetting.GetAccessTokens() + return accessTokensUserSetting.AccessTokens, nil +} + +// RemoveUserAccessToken remove the access token of the user. +func (s *Store) RemoveUserAccessToken(ctx context.Context, userID int32, token string) error { + oldAccessTokens, err := s.GetUserAccessTokens(ctx, userID) + if err != nil { + return err + } + + newAccessTokens := make([]*storepb.AccessTokensUserSetting_AccessToken, 0, len(oldAccessTokens)) + for _, t := range oldAccessTokens { + if token != t.AccessToken { + newAccessTokens = append(newAccessTokens, t) + } + } + + _, err = s.UpsertUserSetting(ctx, &storepb.UserSetting{ + UserId: userID, + Key: storepb.UserSetting_ACCESS_TOKENS, + Value: &storepb.UserSetting_AccessTokens{ + AccessTokens: &storepb.AccessTokensUserSetting{ + AccessTokens: newAccessTokens, + }, + }, + }) + + return err +} + +// GetUserSessions returns the sessions of the user. +func (s *Store) GetUserSessions(ctx context.Context, userID int32) ([]*storepb.SessionsUserSetting_Session, error) { + userSetting, err := s.GetUserSetting(ctx, &FindUserSetting{ + UserID: &userID, + Key: storepb.UserSetting_SESSIONS, + }) + if err != nil { + return nil, err + } + if userSetting == nil { + return []*storepb.SessionsUserSetting_Session{}, nil + } + + sessionsUserSetting := userSetting.GetSessions() + return sessionsUserSetting.Sessions, nil +} + +// RemoveUserSession removes the session of the user. +func (s *Store) RemoveUserSession(ctx context.Context, userID int32, sessionID string) error { + oldSessions, err := s.GetUserSessions(ctx, userID) + if err != nil { + return err + } + + newSessions := make([]*storepb.SessionsUserSetting_Session, 0, len(oldSessions)) + for _, session := range oldSessions { + if sessionID != session.SessionId { + newSessions = append(newSessions, session) + } + } + + _, err = s.UpsertUserSetting(ctx, &storepb.UserSetting{ + UserId: userID, + Key: storepb.UserSetting_SESSIONS, + Value: &storepb.UserSetting_Sessions{ + Sessions: &storepb.SessionsUserSetting{ + Sessions: newSessions, + }, + }, + }) + + return err +} + +// AddUserSession adds a new session for the user. +func (s *Store) AddUserSession(ctx context.Context, userID int32, session *storepb.SessionsUserSetting_Session) error { + existingSessions, err := s.GetUserSessions(ctx, userID) + if err != nil { + return err + } + + // Check if session already exists, update if it does + var updatedSessions []*storepb.SessionsUserSetting_Session + sessionExists := false + for _, existing := range existingSessions { + if existing.SessionId == session.SessionId { + updatedSessions = append(updatedSessions, session) + sessionExists = true + } else { + updatedSessions = append(updatedSessions, existing) + } + } + + // If session doesn't exist, add it + if !sessionExists { + updatedSessions = append(updatedSessions, session) + } + + _, err = s.UpsertUserSetting(ctx, &storepb.UserSetting{ + UserId: userID, + Key: storepb.UserSetting_SESSIONS, + Value: &storepb.UserSetting_Sessions{ + Sessions: &storepb.SessionsUserSetting{ + Sessions: updatedSessions, + }, + }, + }) + + return err +} + +// UpdateUserSessionLastAccessed updates the last accessed time of a session. +func (s *Store) UpdateUserSessionLastAccessed(ctx context.Context, userID int32, sessionID string, lastAccessedTime *timestamppb.Timestamp) error { + sessions, err := s.GetUserSessions(ctx, userID) + if err != nil { + return err + } + + for _, session := range sessions { + if session.SessionId == sessionID { + session.LastAccessedTime = lastAccessedTime + break + } + } + + _, err = s.UpsertUserSetting(ctx, &storepb.UserSetting{ + UserId: userID, + Key: storepb.UserSetting_SESSIONS, + Value: &storepb.UserSetting_Sessions{ + Sessions: &storepb.SessionsUserSetting{ + Sessions: sessions, + }, + }, + }) + + return err +} + +// GetUserWebhooks returns the webhooks of the user. +func (s *Store) GetUserWebhooks(ctx context.Context, userID int32) ([]*storepb.WebhooksUserSetting_Webhook, error) { + userSetting, err := s.GetUserSetting(ctx, &FindUserSetting{ + UserID: &userID, + Key: storepb.UserSetting_WEBHOOKS, + }) + if err != nil { + return nil, err + } + if userSetting == nil { + return []*storepb.WebhooksUserSetting_Webhook{}, nil + } + + webhooksUserSetting := userSetting.GetWebhooks() + return webhooksUserSetting.Webhooks, nil +} + +// AddUserWebhook adds a new webhook for the user. +func (s *Store) AddUserWebhook(ctx context.Context, userID int32, webhook *storepb.WebhooksUserSetting_Webhook) error { + existingWebhooks, err := s.GetUserWebhooks(ctx, userID) + if err != nil { + return err + } + + // Check if webhook already exists, update if it does + var updatedWebhooks []*storepb.WebhooksUserSetting_Webhook + webhookExists := false + for _, existing := range existingWebhooks { + if existing.Id == webhook.Id { + updatedWebhooks = append(updatedWebhooks, webhook) + webhookExists = true + } else { + updatedWebhooks = append(updatedWebhooks, existing) + } + } + + // If webhook doesn't exist, add it + if !webhookExists { + updatedWebhooks = append(updatedWebhooks, webhook) + } + + _, err = s.UpsertUserSetting(ctx, &storepb.UserSetting{ + UserId: userID, + Key: storepb.UserSetting_WEBHOOKS, + Value: &storepb.UserSetting_Webhooks{ + Webhooks: &storepb.WebhooksUserSetting{ + Webhooks: updatedWebhooks, + }, + }, + }) + + return err +} + +// RemoveUserWebhook removes the webhook of the user. +func (s *Store) RemoveUserWebhook(ctx context.Context, userID int32, webhookID string) error { + oldWebhooks, err := s.GetUserWebhooks(ctx, userID) + if err != nil { + return err + } + + newWebhooks := make([]*storepb.WebhooksUserSetting_Webhook, 0, len(oldWebhooks)) + for _, webhook := range oldWebhooks { + if webhookID != webhook.Id { + newWebhooks = append(newWebhooks, webhook) + } + } + + _, err = s.UpsertUserSetting(ctx, &storepb.UserSetting{ + UserId: userID, + Key: storepb.UserSetting_WEBHOOKS, + Value: &storepb.UserSetting_Webhooks{ + Webhooks: &storepb.WebhooksUserSetting{ + Webhooks: newWebhooks, + }, + }, + }) + + return err +} + +// UpdateUserWebhook updates an existing webhook for the user. +func (s *Store) UpdateUserWebhook(ctx context.Context, userID int32, webhook *storepb.WebhooksUserSetting_Webhook) error { + webhooks, err := s.GetUserWebhooks(ctx, userID) + if err != nil { + return err + } + + for i, existing := range webhooks { + if existing.Id == webhook.Id { + webhooks[i] = webhook + break + } + } + + _, err = s.UpsertUserSetting(ctx, &storepb.UserSetting{ + UserId: userID, + Key: storepb.UserSetting_WEBHOOKS, + Value: &storepb.UserSetting_Webhooks{ + Webhooks: &storepb.WebhooksUserSetting{ + Webhooks: webhooks, + }, + }, + }) + + return err +} + +func convertUserSettingFromRaw(raw *UserSetting) (*storepb.UserSetting, error) { + userSetting := &storepb.UserSetting{ + UserId: raw.UserID, + Key: raw.Key, + } + + switch raw.Key { + case storepb.UserSetting_ACCESS_TOKENS: + accessTokensUserSetting := &storepb.AccessTokensUserSetting{} + if err := protojsonUnmarshaler.Unmarshal([]byte(raw.Value), accessTokensUserSetting); err != nil { + return nil, err + } + userSetting.Value = &storepb.UserSetting_AccessTokens{AccessTokens: accessTokensUserSetting} + case storepb.UserSetting_SESSIONS: + sessionsUserSetting := &storepb.SessionsUserSetting{} + if err := protojsonUnmarshaler.Unmarshal([]byte(raw.Value), sessionsUserSetting); err != nil { + return nil, err + } + userSetting.Value = &storepb.UserSetting_Sessions{Sessions: sessionsUserSetting} + case storepb.UserSetting_SHORTCUTS: + shortcutsUserSetting := &storepb.ShortcutsUserSetting{} + if err := protojsonUnmarshaler.Unmarshal([]byte(raw.Value), shortcutsUserSetting); err != nil { + return nil, err + } + userSetting.Value = &storepb.UserSetting_Shortcuts{Shortcuts: shortcutsUserSetting} + case storepb.UserSetting_GENERAL: + generalUserSetting := &storepb.GeneralUserSetting{} + if err := protojsonUnmarshaler.Unmarshal([]byte(raw.Value), generalUserSetting); err != nil { + return nil, err + } + userSetting.Value = &storepb.UserSetting_General{General: generalUserSetting} + case storepb.UserSetting_WEBHOOKS: + webhooksUserSetting := &storepb.WebhooksUserSetting{} + if err := protojsonUnmarshaler.Unmarshal([]byte(raw.Value), webhooksUserSetting); err != nil { + return nil, err + } + userSetting.Value = &storepb.UserSetting_Webhooks{Webhooks: webhooksUserSetting} + default: + return nil, nil + } + return userSetting, nil +} + +func convertUserSettingToRaw(userSetting *storepb.UserSetting) (*UserSetting, error) { + raw := &UserSetting{ + UserID: userSetting.UserId, + Key: userSetting.Key, + } + + switch userSetting.Key { + case storepb.UserSetting_ACCESS_TOKENS: + accessTokensUserSetting := userSetting.GetAccessTokens() + value, err := protojson.Marshal(accessTokensUserSetting) + if err != nil { + return nil, err + } + raw.Value = string(value) + case storepb.UserSetting_SESSIONS: + sessionsUserSetting := userSetting.GetSessions() + value, err := protojson.Marshal(sessionsUserSetting) + if err != nil { + return nil, err + } + raw.Value = string(value) + case storepb.UserSetting_SHORTCUTS: + shortcutsUserSetting := userSetting.GetShortcuts() + value, err := protojson.Marshal(shortcutsUserSetting) + if err != nil { + return nil, err + } + raw.Value = string(value) + case storepb.UserSetting_GENERAL: + generalUserSetting := userSetting.GetGeneral() + value, err := protojson.Marshal(generalUserSetting) + if err != nil { + return nil, err + } + raw.Value = string(value) + case storepb.UserSetting_WEBHOOKS: + webhooksUserSetting := userSetting.GetWebhooks() + value, err := protojson.Marshal(webhooksUserSetting) + if err != nil { + return nil, err + } + raw.Value = string(value) + default: + return nil, errors.Errorf("unsupported user setting key: %v", userSetting.Key) + } + return raw, nil +} diff --git a/store/workspace_setting.go b/store/workspace_setting.go new file mode 100644 index 0000000..9f9054e --- /dev/null +++ b/store/workspace_setting.go @@ -0,0 +1,245 @@ +package store + +import ( + "context" + + "github.com/pkg/errors" + "google.golang.org/protobuf/encoding/protojson" + + storepb "github.com/usememos/memos/proto/gen/store" +) + +type WorkspaceSetting struct { + Name string + Value string + Description string +} + +type FindWorkspaceSetting struct { + Name string +} + +type DeleteWorkspaceSetting struct { + Name string +} + +func (s *Store) UpsertWorkspaceSetting(ctx context.Context, upsert *storepb.WorkspaceSetting) (*storepb.WorkspaceSetting, error) { + workspaceSettingRaw := &WorkspaceSetting{ + Name: upsert.Key.String(), + } + var valueBytes []byte + var err error + if upsert.Key == storepb.WorkspaceSettingKey_BASIC { + valueBytes, err = protojson.Marshal(upsert.GetBasicSetting()) + } else if upsert.Key == storepb.WorkspaceSettingKey_GENERAL { + valueBytes, err = protojson.Marshal(upsert.GetGeneralSetting()) + } else if upsert.Key == storepb.WorkspaceSettingKey_STORAGE { + valueBytes, err = protojson.Marshal(upsert.GetStorageSetting()) + } else if upsert.Key == storepb.WorkspaceSettingKey_MEMO_RELATED { + valueBytes, err = protojson.Marshal(upsert.GetMemoRelatedSetting()) + } else { + return nil, errors.Errorf("unsupported workspace setting key: %v", upsert.Key) + } + if err != nil { + return nil, errors.Wrap(err, "failed to marshal workspace setting value") + } + valueString := string(valueBytes) + workspaceSettingRaw.Value = valueString + workspaceSettingRaw, err = s.driver.UpsertWorkspaceSetting(ctx, workspaceSettingRaw) + if err != nil { + return nil, errors.Wrap(err, "Failed to upsert workspace setting") + } + workspaceSetting, err := convertWorkspaceSettingFromRaw(workspaceSettingRaw) + if err != nil { + return nil, errors.Wrap(err, "Failed to convert workspace setting") + } + s.workspaceSettingCache.Set(ctx, workspaceSetting.Key.String(), workspaceSetting) + return workspaceSetting, nil +} + +func (s *Store) ListWorkspaceSettings(ctx context.Context, find *FindWorkspaceSetting) ([]*storepb.WorkspaceSetting, error) { + list, err := s.driver.ListWorkspaceSettings(ctx, find) + if err != nil { + return nil, err + } + + workspaceSettings := []*storepb.WorkspaceSetting{} + for _, workspaceSettingRaw := range list { + workspaceSetting, err := convertWorkspaceSettingFromRaw(workspaceSettingRaw) + if err != nil { + return nil, errors.Wrap(err, "Failed to convert workspace setting") + } + if workspaceSetting == nil { + continue + } + s.workspaceSettingCache.Set(ctx, workspaceSetting.Key.String(), workspaceSetting) + workspaceSettings = append(workspaceSettings, workspaceSetting) + } + return workspaceSettings, nil +} + +func (s *Store) GetWorkspaceSetting(ctx context.Context, find *FindWorkspaceSetting) (*storepb.WorkspaceSetting, error) { + if cache, ok := s.workspaceSettingCache.Get(ctx, find.Name); ok { + workspaceSetting, ok := cache.(*storepb.WorkspaceSetting) + if ok { + return workspaceSetting, nil + } + } + + list, err := s.ListWorkspaceSettings(ctx, find) + if err != nil { + return nil, err + } + if len(list) == 0 { + return nil, nil + } + if len(list) > 1 { + return nil, errors.Errorf("found multiple workspace settings with key %s", find.Name) + } + return list[0], nil +} + +func (s *Store) GetWorkspaceBasicSetting(ctx context.Context) (*storepb.WorkspaceBasicSetting, error) { + workspaceSetting, err := s.GetWorkspaceSetting(ctx, &FindWorkspaceSetting{ + Name: storepb.WorkspaceSettingKey_BASIC.String(), + }) + if err != nil { + return nil, errors.Wrap(err, "failed to get workspace basic setting") + } + + workspaceBasicSetting := &storepb.WorkspaceBasicSetting{} + if workspaceSetting != nil { + workspaceBasicSetting = workspaceSetting.GetBasicSetting() + } + s.workspaceSettingCache.Set(ctx, storepb.WorkspaceSettingKey_BASIC.String(), &storepb.WorkspaceSetting{ + Key: storepb.WorkspaceSettingKey_BASIC, + Value: &storepb.WorkspaceSetting_BasicSetting{BasicSetting: workspaceBasicSetting}, + }) + return workspaceBasicSetting, nil +} + +func (s *Store) GetWorkspaceGeneralSetting(ctx context.Context) (*storepb.WorkspaceGeneralSetting, error) { + workspaceSetting, err := s.GetWorkspaceSetting(ctx, &FindWorkspaceSetting{ + Name: storepb.WorkspaceSettingKey_GENERAL.String(), + }) + if err != nil { + return nil, errors.Wrap(err, "failed to get workspace general setting") + } + + workspaceGeneralSetting := &storepb.WorkspaceGeneralSetting{} + if workspaceSetting != nil { + workspaceGeneralSetting = workspaceSetting.GetGeneralSetting() + } + s.workspaceSettingCache.Set(ctx, storepb.WorkspaceSettingKey_GENERAL.String(), &storepb.WorkspaceSetting{ + Key: storepb.WorkspaceSettingKey_GENERAL, + Value: &storepb.WorkspaceSetting_GeneralSetting{GeneralSetting: workspaceGeneralSetting}, + }) + return workspaceGeneralSetting, nil +} + +// DefaultContentLengthLimit is the default limit of content length in bytes. 8KB. +const DefaultContentLengthLimit = 8 * 1024 + +// DefaultReactions is the default reactions for memo related setting. +var DefaultReactions = []string{"👍", "👎", "❤️", "🎉", "😄", "😕", "😢", "😡"} + +// DefaultNsfwTags is the default tags that mark content as NSFW for blurring. +var DefaultNsfwTags = []string{"nsfw"} + +func (s *Store) GetWorkspaceMemoRelatedSetting(ctx context.Context) (*storepb.WorkspaceMemoRelatedSetting, error) { + workspaceSetting, err := s.GetWorkspaceSetting(ctx, &FindWorkspaceSetting{ + Name: storepb.WorkspaceSettingKey_MEMO_RELATED.String(), + }) + if err != nil { + return nil, errors.Wrap(err, "failed to get workspace general setting") + } + + workspaceMemoRelatedSetting := &storepb.WorkspaceMemoRelatedSetting{} + if workspaceSetting != nil { + workspaceMemoRelatedSetting = workspaceSetting.GetMemoRelatedSetting() + } + if workspaceMemoRelatedSetting.ContentLengthLimit < DefaultContentLengthLimit { + workspaceMemoRelatedSetting.ContentLengthLimit = DefaultContentLengthLimit + } + if len(workspaceMemoRelatedSetting.Reactions) == 0 { + workspaceMemoRelatedSetting.Reactions = append(workspaceMemoRelatedSetting.Reactions, DefaultReactions...) + } + if len(workspaceMemoRelatedSetting.NsfwTags) == 0 { + workspaceMemoRelatedSetting.NsfwTags = append(workspaceMemoRelatedSetting.NsfwTags, DefaultNsfwTags...) + } + s.workspaceSettingCache.Set(ctx, storepb.WorkspaceSettingKey_MEMO_RELATED.String(), &storepb.WorkspaceSetting{ + Key: storepb.WorkspaceSettingKey_MEMO_RELATED, + Value: &storepb.WorkspaceSetting_MemoRelatedSetting{MemoRelatedSetting: workspaceMemoRelatedSetting}, + }) + return workspaceMemoRelatedSetting, nil +} + +const ( + defaultWorkspaceStorageType = storepb.WorkspaceStorageSetting_DATABASE + defaultWorkspaceUploadSizeLimitMb = 30 + defaultWorkspaceFilepathTemplate = "assets/{timestamp}_{filename}" +) + +func (s *Store) GetWorkspaceStorageSetting(ctx context.Context) (*storepb.WorkspaceStorageSetting, error) { + workspaceSetting, err := s.GetWorkspaceSetting(ctx, &FindWorkspaceSetting{ + Name: storepb.WorkspaceSettingKey_STORAGE.String(), + }) + if err != nil { + return nil, errors.Wrap(err, "failed to get workspace storage setting") + } + + workspaceStorageSetting := &storepb.WorkspaceStorageSetting{} + if workspaceSetting != nil { + workspaceStorageSetting = workspaceSetting.GetStorageSetting() + } + if workspaceStorageSetting.StorageType == storepb.WorkspaceStorageSetting_STORAGE_TYPE_UNSPECIFIED { + workspaceStorageSetting.StorageType = defaultWorkspaceStorageType + } + if workspaceStorageSetting.UploadSizeLimitMb == 0 { + workspaceStorageSetting.UploadSizeLimitMb = defaultWorkspaceUploadSizeLimitMb + } + if workspaceStorageSetting.FilepathTemplate == "" { + workspaceStorageSetting.FilepathTemplate = defaultWorkspaceFilepathTemplate + } + s.workspaceSettingCache.Set(ctx, storepb.WorkspaceSettingKey_STORAGE.String(), &storepb.WorkspaceSetting{ + Key: storepb.WorkspaceSettingKey_STORAGE, + Value: &storepb.WorkspaceSetting_StorageSetting{StorageSetting: workspaceStorageSetting}, + }) + return workspaceStorageSetting, nil +} + +func convertWorkspaceSettingFromRaw(workspaceSettingRaw *WorkspaceSetting) (*storepb.WorkspaceSetting, error) { + workspaceSetting := &storepb.WorkspaceSetting{ + Key: storepb.WorkspaceSettingKey(storepb.WorkspaceSettingKey_value[workspaceSettingRaw.Name]), + } + switch workspaceSettingRaw.Name { + case storepb.WorkspaceSettingKey_BASIC.String(): + basicSetting := &storepb.WorkspaceBasicSetting{} + if err := protojsonUnmarshaler.Unmarshal([]byte(workspaceSettingRaw.Value), basicSetting); err != nil { + return nil, err + } + workspaceSetting.Value = &storepb.WorkspaceSetting_BasicSetting{BasicSetting: basicSetting} + case storepb.WorkspaceSettingKey_GENERAL.String(): + generalSetting := &storepb.WorkspaceGeneralSetting{} + if err := protojsonUnmarshaler.Unmarshal([]byte(workspaceSettingRaw.Value), generalSetting); err != nil { + return nil, err + } + workspaceSetting.Value = &storepb.WorkspaceSetting_GeneralSetting{GeneralSetting: generalSetting} + case storepb.WorkspaceSettingKey_STORAGE.String(): + storageSetting := &storepb.WorkspaceStorageSetting{} + if err := protojsonUnmarshaler.Unmarshal([]byte(workspaceSettingRaw.Value), storageSetting); err != nil { + return nil, err + } + workspaceSetting.Value = &storepb.WorkspaceSetting_StorageSetting{StorageSetting: storageSetting} + case storepb.WorkspaceSettingKey_MEMO_RELATED.String(): + memoRelatedSetting := &storepb.WorkspaceMemoRelatedSetting{} + if err := protojsonUnmarshaler.Unmarshal([]byte(workspaceSettingRaw.Value), memoRelatedSetting); err != nil { + return nil, err + } + workspaceSetting.Value = &storepb.WorkspaceSetting_MemoRelatedSetting{MemoRelatedSetting: memoRelatedSetting} + default: + // Skip unsupported workspace setting key. + return nil, nil + } + return workspaceSetting, nil +} diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..0e8b17d --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,7 @@ +node_modules +.pnpm-store +.DS_Store +dist +dist-ssr +*.local +src/types/proto/store diff --git a/web/.prettierrc.js b/web/.prettierrc.js new file mode 100644 index 0000000..e9531d6 --- /dev/null +++ b/web/.prettierrc.js @@ -0,0 +1,8 @@ +module.exports = { + printWidth: 140, + useTabs: false, + semi: true, + singleQuote: false, + plugins: [require.resolve("@trivago/prettier-plugin-sort-imports")], + importOrder: ["", "", "^@/((?!css).+)", "^[./]", "^(.+).css"], +}; diff --git a/web/README.md b/web/README.md new file mode 100644 index 0000000..bb1b934 --- /dev/null +++ b/web/README.md @@ -0,0 +1 @@ +# The frontend of Memos diff --git a/web/components.json b/web/components.json new file mode 100644 index 0000000..2082f48 --- /dev/null +++ b/web/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/index.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs new file mode 100644 index 0000000..d6af954 --- /dev/null +++ b/web/eslint.config.mjs @@ -0,0 +1,34 @@ +import eslint from "@eslint/js"; +import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended"; +import tseslint from "typescript-eslint"; + +export default [ + ...tseslint.config(eslint.configs.recommended, tseslint.configs.recommended), + eslintPluginPrettierRecommended, + { + ignores: ["**/dist/**", "**/node_modules/**", "**/proto/**"], + }, + { + rules: { + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": "error", + "@typescript-eslint/no-explicit-any": ["off"], + "react/react-in-jsx-scope": "off", + "react/jsx-no-target-blank": "off", + "no-restricted-syntax": [ + "error", + { + selector: + "VariableDeclarator[init.callee.name='useTranslation'] > ObjectPattern > Property[key.name='t']:not([parent.declarations.0.init.callee.object.name='i18n'])", + message: "Destructuring 't' from useTranslation is not allowed. Please use the 'useTranslate' hook from '@/utils/i18n'.", + }, + ], + }, + }, + { + files: ["src/utils/i18n.ts"], + rules: { + "no-restricted-syntax": "off", + }, + }, +]; diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..867364e --- /dev/null +++ b/web/index.html @@ -0,0 +1,19 @@ + + + + + + + + + + + + Memos + + +
+ + + + diff --git a/web/package-lock.json b/web/package-lock.json new file mode 100644 index 0000000..0c689fe --- /dev/null +++ b/web/package-lock.json @@ -0,0 +1,9947 @@ +{ + "name": "memos", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "memos", + "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@github/relative-time-element": "^4.4.8", + "@matejmazur/react-katex": "^3.1.3", + "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-radio-group": "^1.3.7", + "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.5", + "@radix-ui/react-tooltip": "^1.2.7", + "@tailwindcss/vite": "^4.1.11", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "copy-to-clipboard": "^3.3.3", + "dayjs": "^1.11.13", + "fuse.js": "^7.1.0", + "highlight.js": "^11.11.1", + "i18next": "^25.3.0", + "katex": "^0.16.22", + "leaflet": "^1.9.4", + "lodash-es": "^4.17.21", + "lucide-react": "^0.486.0", + "mermaid": "^11.8.0", + "mobx": "^6.13.7", + "mobx-react-lite": "^4.1.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-force-graph-2d": "^1.28.0", + "react-hot-toast": "^2.5.2", + "react-i18next": "^15.5.3", + "react-leaflet": "^4.2.1", + "react-router-dom": "^7.6.3", + "react-simple-pull-to-refresh": "^1.3.3", + "react-use": "^17.6.0", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^4.1.11", + "textarea-caret": "^3.1.0", + "uuid": "^11.1.0" + }, + "devDependencies": { + "@bufbuild/protobuf": "^2.6.0", + "@eslint/js": "^9.30.1", + "@trivago/prettier-plugin-sort-imports": "^5.2.2", + "@types/d3": "^7.4.3", + "@types/katex": "^0.16.7", + "@types/leaflet": "^1.9.19", + "@types/lodash-es": "^4.17.12", + "@types/node": "^24.0.10", + "@types/qs": "^6.14.0", + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@types/textarea-caret": "^3.0.4", + "@types/uuid": "^10.0.0", + "@vitejs/plugin-react": "^4.6.0", + "code-inspector-plugin": "^0.18.3", + "eslint": "^9.30.1", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-prettier": "^5.5.1", + "eslint-plugin-react": "^7.37.5", + "long": "^5.3.2", + "nice-grpc-web": "^3.3.7", + "prettier": "^3.6.2", + "terser": "^5.43.1", + "tw-animate-css": "^1.3.4", + "typescript": "^5.8.3", + "typescript-eslint": "^8.35.1", + "vite": "^7.0.1" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@antfu/utils": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-8.1.1.tgz", + "integrity": "sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/generator": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.0.tgz", + "integrity": "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz", + "integrity": "sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==", + "license": "MIT" + }, + "node_modules/@bufbuild/protobuf": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.6.0.tgz", + "integrity": "sha512-6cuonJVNOIL7lTj5zgo/Rc2bKAo4/GvN+rKCrUj7GdEHRzCk8zKOfFwUsL9nAVk5rSIsRmlgcpLzTRysopEeeg==", + "dev": true, + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz", + "integrity": "sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "11.0.3", + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/gast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz", + "integrity": "sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz", + "integrity": "sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz", + "integrity": "sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz", + "integrity": "sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==", + "license": "Apache-2.0" + }, + "node_modules/@dnd-kit/accessibility": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz", + "integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/core": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz", + "integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==", + "license": "MIT", + "dependencies": { + "@dnd-kit/accessibility": "^3.1.1", + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/sortable": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz", + "integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==", + "license": "MIT", + "dependencies": { + "@dnd-kit/utilities": "^3.2.2", + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@dnd-kit/core": "^6.3.0", + "react": ">=16.8.0" + } + }, + "node_modules/@dnd-kit/utilities": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz", + "integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", + "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz", + "integrity": "sha512-ShbM/3XxwuxjFiuVBHA+d3j5dyac0aEVVq1oluIDf71hUw0aRF59dV/efUsIwFnR6m8JNM2FjZOzmaZ8yG61kw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.6.tgz", + "integrity": "sha512-S8ToEOVfg++AU/bHwdksHNnyLyVM+eMVAOf6yRKFitnwnbwwPNqKr3srzFRe7nzV69RQKb5DgchIX5pt3L53xg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.6.tgz", + "integrity": "sha512-hd5zdUarsK6strW+3Wxi5qWws+rJhCCbMiC9QZyzoxfk5uHRIE8T287giQxzVpEvCwuJ9Qjg6bEjcRJcgfLqoA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.6.tgz", + "integrity": "sha512-0Z7KpHSr3VBIO9A/1wcT3NTy7EB4oNC4upJ5ye3R7taCc2GUdeynSLArnon5G8scPwaU866d3H4BCrE5xLW25A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.6.tgz", + "integrity": "sha512-FFCssz3XBavjxcFxKsGy2DYK5VSvJqa6y5HXljKzhRZ87LvEi13brPrf/wdyl/BbpbMKJNOr1Sd0jtW4Ge1pAA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.6.tgz", + "integrity": "sha512-GfXs5kry/TkGM2vKqK2oyiLFygJRqKVhawu3+DOCk7OxLy/6jYkWXhlHwOoTb0WqGnWGAS7sooxbZowy+pK9Yg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.6.tgz", + "integrity": "sha512-aoLF2c3OvDn2XDTRvn8hN6DRzVVpDlj2B/F66clWd/FHLiHaG3aVZjxQX2DYphA5y/evbdGvC6Us13tvyt4pWg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.6.tgz", + "integrity": "sha512-2SkqTjTSo2dYi/jzFbU9Plt1vk0+nNg8YC8rOXXea+iA3hfNJWebKYPs3xnOUf9+ZWhKAaxnQNUf2X9LOpeiMQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.6.tgz", + "integrity": "sha512-SZHQlzvqv4Du5PrKE2faN0qlbsaW/3QQfUUc6yO2EjFcA83xnwm91UbEEVx4ApZ9Z5oG8Bxz4qPE+HFwtVcfyw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.6.tgz", + "integrity": "sha512-b967hU0gqKd9Drsh/UuAm21Khpoh6mPBSgz8mKRq4P5mVK8bpA+hQzmm/ZwGVULSNBzKdZPQBRT3+WuVavcWsQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.6.tgz", + "integrity": "sha512-aHWdQ2AAltRkLPOsKdi3xv0mZ8fUGPdlKEjIEhxCPm5yKEThcUjHpWB1idN74lfXGnZ5SULQSgtr5Qos5B0bPw==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.6.tgz", + "integrity": "sha512-VgKCsHdXRSQ7E1+QXGdRPlQ/e08bN6WMQb27/TMfV+vPjjTImuT9PmLXupRlC90S1JeNNW5lzkAEO/McKeJ2yg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.6.tgz", + "integrity": "sha512-WViNlpivRKT9/py3kCmkHnn44GkGXVdXfdc4drNmRl15zVQ2+D2uFwdlGh6IuK5AAnGTo2qPB1Djppj+t78rzw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.6.tgz", + "integrity": "sha512-wyYKZ9NTdmAMb5730I38lBqVu6cKl4ZfYXIs31Baf8aoOtB4xSGi3THmDYt4BTFHk7/EcVixkOV2uZfwU3Q2Jw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.6.tgz", + "integrity": "sha512-KZh7bAGGcrinEj4qzilJ4hqTY3Dg2U82c8bv+e1xqNqZCrCyc+TL9AUEn5WGKDzm3CfC5RODE/qc96OcbIe33w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.6.tgz", + "integrity": "sha512-9N1LsTwAuE9oj6lHMyyAM+ucxGiVnEqUdp4v7IaMmrwb06ZTEVCIs3oPPplVsnjPfyjmxwHxHMF8b6vzUVAUGw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.6.tgz", + "integrity": "sha512-A6bJB41b4lKFWRKNrWoP2LHsjVzNiaurf7wyj/XtFNTsnPuxwEBWHLty+ZE0dWBKuSK1fvKgrKaNjBS7qbFKig==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.6.tgz", + "integrity": "sha512-IjA+DcwoVpjEvyxZddDqBY+uJ2Snc6duLpjmkXm/v4xuS3H+3FkLZlDm9ZsAbF9rsfP3zeA0/ArNDORZgrxR/Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.6.tgz", + "integrity": "sha512-dUXuZr5WenIDlMHdMkvDc1FAu4xdWixTCRgP7RQLBOkkGgwuuzaGSYcOpW4jFxzpzL1ejb8yF620UxAqnBrR9g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.6.tgz", + "integrity": "sha512-l8ZCvXP0tbTJ3iaqdNf3pjaOSd5ex/e6/omLIQCVBLmHTlfXW3zAxQ4fnDmPLOB1x9xrcSi/xtCWFwCZRIaEwg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.6.tgz", + "integrity": "sha512-hKrmDa0aOFOr71KQ/19JC7az1P0GWtCN1t2ahYAf4O007DHZt/dW8ym5+CUdJhQ/qkZmI1HAF8KkJbEFtCL7gw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.6.tgz", + "integrity": "sha512-+SqBcAWoB1fYKmpWoQP4pGtx+pUUC//RNYhFdbcSA16617cchuryuhOCRpPsjCblKukAckWsV+aQ3UKT/RMPcA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.6.tgz", + "integrity": "sha512-dyCGxv1/Br7MiSC42qinGL8KkG4kX0pEsdb0+TKhmJZgCUDBGmyo1/ArCjNGiOLiIAgdbWgmWgib4HoCi5t7kA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.6.tgz", + "integrity": "sha512-42QOgcZeZOvXfsCBJF5Afw73t4veOId//XD3i+/9gSkhSV6Gk3VPlWncctI+JcOyERv85FUo7RxuxGy+z8A43Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.6.tgz", + "integrity": "sha512-4AWhgXmDuYN7rJI6ORB+uU9DHLq/erBbuMoAuB4VWJTu5KtCgcKYPynF0YI1VkBNuEfjNlLrFr9KZPJzrtLkrQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.6.tgz", + "integrity": "sha512-NgJPHHbEpLQgDH2MjQu90pzW/5vvXIZ7KOnPyNBm92A6WgZ/7b6fJyUBjoumLqeOQQGqY2QjQxRo97ah4Sj0cA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.31.0.tgz", + "integrity": "sha512-LOm5OVt7D4qiKCqoiPbA7LWmI+tbw1VbTUowBcUMgQSuM6poJufkFkYDcQpo5KfgD39TnNySV26QjOh7VFpSyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.3.tgz", + "integrity": "sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz", + "integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz", + "integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.2", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.4.tgz", + "integrity": "sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.7.2" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@github/relative-time-element": { + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.4.8.tgz", + "integrity": "sha512-FSLYm6F3TSQnqHE1EMQUVVgi2XjbCvsESwwXfugHFpBnhyF1uhJOtu0Psp/BB/qqazfdkk7f5fVcu7WuXl3t8Q==", + "license": "MIT" + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-2.3.0.tgz", + "integrity": "sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.0.0", + "@antfu/utils": "^8.1.0", + "@iconify/types": "^2.0.0", + "debug": "^4.4.0", + "globals": "^15.14.0", + "kolorist": "^1.8.0", + "local-pkg": "^1.0.0", + "mlly": "^1.7.4" + } + }, + "node_modules/@iconify/utils/node_modules/globals": { + "version": "15.15.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz", + "integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.10", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.10.tgz", + "integrity": "sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@matejmazur/react-katex": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@matejmazur/react-katex/-/react-katex-3.1.3.tgz", + "integrity": "sha512-rBp7mJ9An7ktNoU653BWOYdO4FoR4YNwofHZi+vaytX/nWbIlmHVIF+X8VFOn6c3WYmrLT5FFBjKqCZ1sjR5uQ==", + "license": "MIT", + "engines": { + "node": ">=12", + "yarn": ">=1.1" + }, + "peerDependencies": { + "katex": ">=0.9", + "react": ">=16" + } + }, + "node_modules/@mermaid-js/parser": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.1.tgz", + "integrity": "sha512-lCQNpV8R4lgsGcjX5667UiuDLk2micCtjtxR1YKbBXvN5w2v+FeLYoHrTSSrjwXdMcDYvE4ZBPvKT31dfeSmmA==", + "license": "MIT", + "dependencies": { + "langium": "3.3.1" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.7.tgz", + "integrity": "sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@radix-ui/number": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz", + "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==", + "license": "MIT" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.2.tgz", + "integrity": "sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==", + "license": "MIT" + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz", + "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.3.2.tgz", + "integrity": "sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz", + "integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", + "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz", + "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.14.tgz", + "integrity": "sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz", + "integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.10.tgz", + "integrity": "sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-escape-keydown": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.15.tgz", + "integrity": "sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-menu": "2.1.15", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.2.tgz", + "integrity": "sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.7.tgz", + "integrity": "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz", + "integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.1.7.tgz", + "integrity": "sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.15", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.15.tgz", + "integrity": "sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.14.tgz", + "integrity": "sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.7.tgz", + "integrity": "sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-rect": "1.1.1", + "@radix-ui/react-use-size": "1.1.1", + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz", + "integrity": "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.4.tgz", + "integrity": "sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz", + "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.7.tgz", + "integrity": "sha512-9w5XhD0KPOrm92OTTE0SysH3sYzHsSTHNvZgUBo/VZ80VdYyB5RneDbc0dKpURS24IxkoFRu/hI0i4XyfFwY6g==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-roving-focus": "1.1.10", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz", + "integrity": "sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.5.tgz", + "integrity": "sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==", + "license": "MIT", + "dependencies": { + "@radix-ui/number": "1.1.1", + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-collection": "1.1.7", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-direction": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-focus-guards": "1.1.2", + "@radix-ui/react-focus-scope": "1.1.7", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-callback-ref": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-layout-effect": "1.1.1", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-visually-hidden": "1.2.3", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz", + "integrity": "sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", + "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.5.tgz", + "integrity": "sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-use-previous": "1.1.1", + "@radix-ui/react-use-size": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.2.7.tgz", + "integrity": "sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.2", + "@radix-ui/react-context": "1.1.2", + "@radix-ui/react-dismissable-layer": "1.1.10", + "@radix-ui/react-id": "1.1.1", + "@radix-ui/react-popper": "1.2.7", + "@radix-ui/react-portal": "1.1.9", + "@radix-ui/react-presence": "1.1.4", + "@radix-ui/react-primitive": "2.1.3", + "@radix-ui/react-slot": "1.2.3", + "@radix-ui/react-use-controllable-state": "1.2.2", + "@radix-ui/react-visually-hidden": "1.2.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz", + "integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz", + "integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-effect-event": "0.0.2", + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-effect-event": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz", + "integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz", + "integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz", + "integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz", + "integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==", + "license": "MIT", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz", + "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==", + "license": "MIT", + "dependencies": { + "@radix-ui/rect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz", + "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz", + "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-primitive": "2.1.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz", + "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", + "license": "MIT" + }, + "node_modules/@react-leaflet/core": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@react-leaflet/core/-/core-2.1.0.tgz", + "integrity": "sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==", + "license": "Hippocratic-2.1", + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.19", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz", + "integrity": "sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.44.2.tgz", + "integrity": "sha512-g0dF8P1e2QYPOj1gu7s/3LVP6kze9A7m6x0BZ9iTdXK8N5c2V7cpBKHV3/9A4Zd8xxavdhK0t4PnqjkqVmUc9Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.44.2.tgz", + "integrity": "sha512-Yt5MKrOosSbSaAK5Y4J+vSiID57sOvpBNBR6K7xAaQvk3MkcNVV0f9fE20T+41WYN8hDn6SGFlFrKudtx4EoxA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.44.2.tgz", + "integrity": "sha512-EsnFot9ZieM35YNA26nhbLTJBHD0jTwWpPwmRVDzjylQT6gkar+zenfb8mHxWpRrbn+WytRRjE0WKsfaxBkVUA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.44.2.tgz", + "integrity": "sha512-dv/t1t1RkCvJdWWxQ2lWOO+b7cMsVw5YFaS04oHpZRWehI1h0fV1gF4wgGCTyQHHjJDfbNpwOi6PXEafRBBezw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.44.2.tgz", + "integrity": "sha512-W4tt4BLorKND4qeHElxDoim0+BsprFTwb+vriVQnFFtT/P6v/xO5I99xvYnVzKWrK6j7Hb0yp3x7V5LUbaeOMg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.44.2.tgz", + "integrity": "sha512-tdT1PHopokkuBVyHjvYehnIe20fxibxFCEhQP/96MDSOcyjM/shlTkZZLOufV3qO6/FQOSiJTBebhVc12JyPTA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.44.2.tgz", + "integrity": "sha512-+xmiDGGaSfIIOXMzkhJ++Oa0Gwvl9oXUeIiwarsdRXSe27HUIvjbSIpPxvnNsRebsNdUo7uAiQVgBD1hVriwSQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.44.2.tgz", + "integrity": "sha512-bDHvhzOfORk3wt8yxIra8N4k/N0MnKInCW5OGZaeDYa/hMrdPaJzo7CSkjKZqX4JFUWjUGm88lI6QJLCM7lDrA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.44.2.tgz", + "integrity": "sha512-NMsDEsDiYghTbeZWEGnNi4F0hSbGnsuOG+VnNvxkKg0IGDvFh7UVpM/14mnMwxRxUf9AdAVJgHPvKXf6FpMB7A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.44.2.tgz", + "integrity": "sha512-lb5bxXnxXglVq+7imxykIp5xMq+idehfl+wOgiiix0191av84OqbjUED+PRC5OA8eFJYj5xAGcpAZ0pF2MnW+A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.44.2.tgz", + "integrity": "sha512-Yl5Rdpf9pIc4GW1PmkUGHdMtbx0fBLE1//SxDmuf3X0dUC57+zMepow2LK0V21661cjXdTn8hO2tXDdAWAqE5g==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.44.2.tgz", + "integrity": "sha512-03vUDH+w55s680YYryyr78jsO1RWU9ocRMaeV2vMniJJW/6HhoTBwyyiiTPVHNWLnhsnwcQ0oH3S9JSBEKuyqw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.44.2.tgz", + "integrity": "sha512-iYtAqBg5eEMG4dEfVlkqo05xMOk6y/JXIToRca2bAWuqjrJYJlx/I7+Z+4hSrsWU8GdJDFPL4ktV3dy4yBSrzg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.44.2.tgz", + "integrity": "sha512-e6vEbgaaqz2yEHqtkPXa28fFuBGmUJ0N2dOJK8YUfijejInt9gfCSA7YDdJ4nYlv67JfP3+PSWFX4IVw/xRIPg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.44.2.tgz", + "integrity": "sha512-evFOtkmVdY3udE+0QKrV5wBx7bKI0iHz5yEVx5WqDJkxp9YQefy4Mpx3RajIVcM6o7jxTvVd/qpC1IXUhGc1Mw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.44.2.tgz", + "integrity": "sha512-/bXb0bEsWMyEkIsUL2Yt5nFB5naLAwyOWMEviQfQY1x3l5WsLKgvZf66TM7UTfED6erckUVUJQ/jJ1FSpm3pRQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.44.2.tgz", + "integrity": "sha512-3D3OB1vSSBXmkGEZR27uiMRNiwN08/RVAcBKwhUYPaiZ8bcvdeEwWPvbnXvvXHY+A/7xluzcN+kaiOFNiOZwWg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.44.2.tgz", + "integrity": "sha512-VfU0fsMK+rwdK8mwODqYeM2hDrF2WiHaSmCBrS7gColkQft95/8tphyzv2EupVxn3iE0FI78wzffoULH1G+dkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.44.2.tgz", + "integrity": "sha512-+qMUrkbUurpE6DVRjiJCNGZBGo9xM4Y0FXU5cjgudWqIBWbcLkjE3XprJUsOFgC6xjBClwVa9k6O3A7K3vxb5Q==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.2.tgz", + "integrity": "sha512-3+QZROYfJ25PDcxFF66UEk8jGWigHJeecZILvkPkyQN7oc5BvFo4YEXFkOs154j3FTMp9mn9Ky8RCOwastduEA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tailwindcss/node": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.11.tgz", + "integrity": "sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==", + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "enhanced-resolve": "^5.18.1", + "jiti": "^2.4.2", + "lightningcss": "1.30.1", + "magic-string": "^0.30.17", + "source-map-js": "^1.2.1", + "tailwindcss": "4.1.11" + } + }, + "node_modules/@tailwindcss/oxide": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.11.tgz", + "integrity": "sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.4", + "tar": "^7.4.3" + }, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-arm64": "4.1.11", + "@tailwindcss/oxide-darwin-x64": "4.1.11", + "@tailwindcss/oxide-freebsd-x64": "4.1.11", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.11", + "@tailwindcss/oxide-linux-arm64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-arm64-musl": "4.1.11", + "@tailwindcss/oxide-linux-x64-gnu": "4.1.11", + "@tailwindcss/oxide-linux-x64-musl": "4.1.11", + "@tailwindcss/oxide-wasm32-wasi": "4.1.11", + "@tailwindcss/oxide-win32-arm64-msvc": "4.1.11", + "@tailwindcss/oxide-win32-x64-msvc": "4.1.11" + } + }, + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.11.tgz", + "integrity": "sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.11.tgz", + "integrity": "sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.11.tgz", + "integrity": "sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.11.tgz", + "integrity": "sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.11.tgz", + "integrity": "sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.11.tgz", + "integrity": "sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.11.tgz", + "integrity": "sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.11.tgz", + "integrity": "sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.11.tgz", + "integrity": "sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.11.tgz", + "integrity": "sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@emnapi/wasi-threads": "^1.0.2", + "@napi-rs/wasm-runtime": "^0.2.11", + "@tybys/wasm-util": "^0.9.0", + "tslib": "^2.8.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.11.tgz", + "integrity": "sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.11.tgz", + "integrity": "sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tailwindcss/vite": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.11.tgz", + "integrity": "sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==", + "license": "MIT", + "dependencies": { + "@tailwindcss/node": "4.1.11", + "@tailwindcss/oxide": "4.1.11", + "tailwindcss": "4.1.11" + }, + "peerDependencies": { + "vite": "^5.2.0 || ^6 || ^7" + } + }, + "node_modules/@trivago/prettier-plugin-sort-imports": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-5.2.2.tgz", + "integrity": "sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/generator": "^7.26.5", + "@babel/parser": "^7.26.7", + "@babel/traverse": "^7.26.7", + "@babel/types": "^7.26.7", + "javascript-natural-sort": "^0.7.1", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">18.12" + }, + "peerDependencies": { + "@vue/compiler-sfc": "3.x", + "prettier": "2.x - 3.x", + "prettier-plugin-svelte": "3.x", + "svelte": "4.x || 5.x" + }, + "peerDependenciesMeta": { + "@vue/compiler-sfc": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + }, + "svelte": { + "optional": true + } + } + }, + "node_modules/@tweenjs/tween.js": { + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-25.0.0.tgz", + "integrity": "sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==", + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.6.tgz", + "integrity": "sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, + "node_modules/@types/js-cookie": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-2.2.7.tgz", + "integrity": "sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==", + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/katex": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz", + "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/leaflet": { + "version": "1.9.20", + "resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.20.tgz", + "integrity": "sha512-rooalPMlk61LCaLOvBF2VIf9M47HgMQqi5xQ9QRi7c8PkdIe0WrIi5IxXUXQjAdL0c+vcQ01mYWbthzmp9GHWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/node": { + "version": "24.0.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.13.tgz", + "integrity": "sha512-Qm9OYVOFHFYg3wJoTSrz80hoec5Lia/dPp84do3X7dZvLikQvM1YpmvTBEdIr/e+U8HTkFjLHLnl78K/qjf+jQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.8.0" + } + }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.23", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", + "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "devOptional": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, + "node_modules/@types/textarea-caret": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/textarea-caret/-/textarea-caret-3.0.4.tgz", + "integrity": "sha512-epJGYB37/sNrTDbhfyRjHkXsQSAcO6zby0JBDS0QMt6HQ1f1W2E4YpSc7TQkNmWaWmYXv92zOIfN5PHA8CmThg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, + "node_modules/@types/uuid": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", + "integrity": "sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.36.0.tgz", + "integrity": "sha512-lZNihHUVB6ZZiPBNgOQGSxUASI7UJWhT8nHyUGCnaQ28XFCw98IfrMCG3rUl1uwUWoAvodJQby2KTs79UTcrAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.36.0", + "@typescript-eslint/type-utils": "8.36.0", + "@typescript-eslint/utils": "8.36.0", + "@typescript-eslint/visitor-keys": "8.36.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.36.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.36.0.tgz", + "integrity": "sha512-FuYgkHwZLuPbZjQHzJXrtXreJdFMKl16BFYyRrLxDhWr6Qr7Kbcu2s1Yhu8tsiMXw1S0W1pjfFfYEt+R604s+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.36.0", + "@typescript-eslint/types": "8.36.0", + "@typescript-eslint/typescript-estree": "8.36.0", + "@typescript-eslint/visitor-keys": "8.36.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.36.0.tgz", + "integrity": "sha512-JAhQFIABkWccQYeLMrHadu/fhpzmSQ1F1KXkpzqiVxA/iYI6UnRt2trqXHt1sYEcw1mxLnB9rKMsOxXPxowN/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.36.0", + "@typescript-eslint/types": "^8.36.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.36.0.tgz", + "integrity": "sha512-wCnapIKnDkN62fYtTGv2+RY8FlnBYA3tNm0fm91kc2BjPhV2vIjwwozJ7LToaLAyb1ca8BxrS7vT+Pvvf7RvqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.36.0", + "@typescript-eslint/visitor-keys": "8.36.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.36.0.tgz", + "integrity": "sha512-Nhh3TIEgN18mNbdXpd5Q8mSCBnrZQeY9V7Ca3dqYvNDStNIGRmJA6dmrIPMJ0kow3C7gcQbpsG2rPzy1Ks/AnA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.36.0.tgz", + "integrity": "sha512-5aaGYG8cVDd6cxfk/ynpYzxBRZJk7w/ymto6uiyUFtdCozQIsQWh7M28/6r57Fwkbweng8qAzoMCPwSJfWlmsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "8.36.0", + "@typescript-eslint/utils": "8.36.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.36.0.tgz", + "integrity": "sha512-xGms6l5cTJKQPZOKM75Dl9yBfNdGeLRsIyufewnxT4vZTrjC0ImQT4fj8QmtJK84F58uSh5HVBSANwcfiXxABQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.36.0.tgz", + "integrity": "sha512-JaS8bDVrfVJX4av0jLpe4ye0BpAaUW7+tnS4Y4ETa3q7NoZgzYbN9zDQTJ8kPb5fQ4n0hliAt9tA4Pfs2zA2Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.36.0", + "@typescript-eslint/tsconfig-utils": "8.36.0", + "@typescript-eslint/types": "8.36.0", + "@typescript-eslint/visitor-keys": "8.36.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.36.0.tgz", + "integrity": "sha512-VOqmHu42aEMT+P2qYjylw6zP/3E/HvptRwdn/PZxyV27KhZg2IOszXod4NcXisWzPAGSS4trE/g4moNj6XmH2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.36.0", + "@typescript-eslint/types": "8.36.0", + "@typescript-eslint/typescript-estree": "8.36.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.36.0.tgz", + "integrity": "sha512-vZrhV2lRPWDuGoxcmrzRZyxAggPL+qp3WzUrlZD+slFueDiYHxeBa34dUXPuC0RmGKzl4lS5kFJYvKCq9cnNDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.36.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.6.0.tgz", + "integrity": "sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.19", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.17.tgz", + "integrity": "sha512-Xe+AittLbAyV0pabcN7cP7/BenRBNcteM4aSDCtRvGw0d9OL+HG1u/XHLY/kt1q4fyMeZYXyIYrsHuPSiDPosA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.5", + "@vue/shared": "3.5.17", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.17.tgz", + "integrity": "sha512-+2UgfLKoaNLhgfhV5Ihnk6wB4ljyW1/7wUIog2puUqajiC29Lp5R/IKDdkebh9jTbTogTbsgB+OY9cEWzG95JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.17", + "@vue/shared": "3.5.17" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.17", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.17.tgz", + "integrity": "sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@xobotyi/scrollbar-width": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz", + "integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==", + "license": "MIT" + }, + "node_modules/abort-controller-x": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/abort-controller-x/-/abort-controller-x-0.4.3.tgz", + "integrity": "sha512-VtUwTNU8fpMwvWGn4xE93ywbogTYsuT+AUxAXOeelbXuQVIwNmC5YLeho9sH4vZ4ITW8414TTAOG1nW6uIVHCA==", + "dev": true, + "license": "MIT" + }, + "node_modules/accessor-fn": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/accessor-fn/-/accessor-fn-1.5.3.tgz", + "integrity": "sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/aria-hidden": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.6.tgz", + "integrity": "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/bezier-js": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/bezier-js/-/bezier-js-6.1.4.tgz", + "integrity": "sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==", + "license": "MIT", + "funding": { + "type": "individual", + "url": "https://github.com/Pomax/bezierjs/blob/master/FUNDING.md" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001727", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", + "integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/canvas-color-tracker": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/canvas-color-tracker/-/canvas-color-tracker-1.3.2.tgz", + "integrity": "sha512-ryQkDX26yJ3CXzb3hxUVNlg1NKE4REc5crLBq661Nxzr8TNd236SaEf2ffYLXyI5tSABSeguHLqcVq4vf9L3Zg==", + "license": "MIT", + "dependencies": { + "tinycolor2": "^1.6.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chevrotain": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz", + "integrity": "sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "11.0.3", + "@chevrotain/gast": "11.0.3", + "@chevrotain/regexp-to-ast": "11.0.3", + "@chevrotain/types": "11.0.3", + "@chevrotain/utils": "11.0.3", + "lodash-es": "4.17.21" + } + }, + "node_modules/chevrotain-allstar": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz", + "integrity": "sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==", + "license": "MIT", + "dependencies": { + "lodash-es": "^4.17.21" + }, + "peerDependencies": { + "chevrotain": "^11.0.0" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cmdk": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz", + "integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "^1.1.1", + "@radix-ui/react-dialog": "^1.1.6", + "@radix-ui/react-id": "^1.1.0", + "@radix-ui/react-primitive": "^2.0.2" + }, + "peerDependencies": { + "react": "^18 || ^19 || ^19.0.0-rc", + "react-dom": "^18 || ^19 || ^19.0.0-rc" + } + }, + "node_modules/code-inspector-core": { + "version": "0.18.3", + "resolved": "https://registry.npmjs.org/code-inspector-core/-/code-inspector-core-0.18.3.tgz", + "integrity": "sha512-60pT2cPoguMTUYdN1MMpjoPUnuF0ud/u7M2y+Vqit/bniLEit9dySEWAVxLU/Ukc5ILrDeLKEttc6fCMl9RUrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "^3.2.47", + "chalk": "^4.1.1", + "dotenv": "^16.1.4", + "launch-ide": "1.0.1", + "portfinder": "^1.0.28" + } + }, + "node_modules/code-inspector-plugin": { + "version": "0.18.3", + "resolved": "https://registry.npmjs.org/code-inspector-plugin/-/code-inspector-plugin-0.18.3.tgz", + "integrity": "sha512-d9oJXZUsnvfTaQDwFmDNA2F+AR/TXIxWg1rr8KGcEskltR2prbZsfuu1z70EAn4khpx0smfi/PvIIwNJQ7FAMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "4.1.1", + "code-inspector-core": "0.18.3", + "dotenv": "^16.3.1", + "esbuild-code-inspector-plugin": "0.18.3", + "vite-code-inspector-plugin": "0.18.3", + "webpack-code-inspector-plugin": "0.18.3" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", + "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/copy-to-clipboard": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", + "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", + "license": "MIT", + "dependencies": { + "toggle-selection": "^1.0.6" + } + }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-in-js-utils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-in-js-utils/-/css-in-js-utils-3.1.0.tgz", + "integrity": "sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==", + "license": "MIT", + "dependencies": { + "hyphenate-style-name": "^1.0.3" + } + }, + "node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/css-tree/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/cytoscape": { + "version": "3.32.1", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.32.1.tgz", + "integrity": "sha512-dbeqFTLYEwlFg7UGtcZhCCG/2WayX72zK3Sq323CEX29CY81tYfVhw1MIdduCtpstB0cTOhJswWlM/OEB3Xp+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-binarytree": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d3-binarytree/-/d3-binarytree-1.0.2.tgz", + "integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==", + "license": "MIT" + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force-3d": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/d3-force-3d/-/d3-force-3d-3.0.6.tgz", + "integrity": "sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==", + "license": "MIT", + "dependencies": { + "d3-binarytree": "1", + "d3-dispatch": "1 - 3", + "d3-octree": "1", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-octree": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/d3-octree/-/d3-octree-1.1.0.tgz", + "integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==", + "license": "MIT" + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dagre-d3-es": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.11.tgz", + "integrity": "sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", + "license": "MIT" + }, + "node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dompurify": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz", + "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.182", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.182.tgz", + "integrity": "sha512-Lv65Btwv9W4J9pyODI6EWpdnhfvrve/us5h1WspW8B2Fb0366REPtY3hX7ounk1CkV/TBjWCEvCBBbYbmV0qCA==", + "dev": true, + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.2.tgz", + "integrity": "sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/es-abstract": { + "version": "1.24.0", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", + "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", + "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.0.3", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.6", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.4", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/esbuild": { + "version": "0.25.6", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.6.tgz", + "integrity": "sha512-GVuzuUwtdsghE3ocJ9Bs8PNoF13HNQ5TXbEi2AhvVb8xU1Iwt9Fos9FEamfoee+u/TOsn7GUWc04lz46n2bbTg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.6", + "@esbuild/android-arm": "0.25.6", + "@esbuild/android-arm64": "0.25.6", + "@esbuild/android-x64": "0.25.6", + "@esbuild/darwin-arm64": "0.25.6", + "@esbuild/darwin-x64": "0.25.6", + "@esbuild/freebsd-arm64": "0.25.6", + "@esbuild/freebsd-x64": "0.25.6", + "@esbuild/linux-arm": "0.25.6", + "@esbuild/linux-arm64": "0.25.6", + "@esbuild/linux-ia32": "0.25.6", + "@esbuild/linux-loong64": "0.25.6", + "@esbuild/linux-mips64el": "0.25.6", + "@esbuild/linux-ppc64": "0.25.6", + "@esbuild/linux-riscv64": "0.25.6", + "@esbuild/linux-s390x": "0.25.6", + "@esbuild/linux-x64": "0.25.6", + "@esbuild/netbsd-arm64": "0.25.6", + "@esbuild/netbsd-x64": "0.25.6", + "@esbuild/openbsd-arm64": "0.25.6", + "@esbuild/openbsd-x64": "0.25.6", + "@esbuild/openharmony-arm64": "0.25.6", + "@esbuild/sunos-x64": "0.25.6", + "@esbuild/win32-arm64": "0.25.6", + "@esbuild/win32-ia32": "0.25.6", + "@esbuild/win32-x64": "0.25.6" + } + }, + "node_modules/esbuild-code-inspector-plugin": { + "version": "0.18.3", + "resolved": "https://registry.npmjs.org/esbuild-code-inspector-plugin/-/esbuild-code-inspector-plugin-0.18.3.tgz", + "integrity": "sha512-FaPt5eFMtW1oXMWqAcqfAJByNagP1V/R9dwDDLQO29JmryMF35+frskTqy+G53whmTaVi19+TCrFqhNbMZH5ZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "code-inspector-core": "0.18.3" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.31.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.31.0.tgz", + "integrity": "sha512-QldCVh/ztyKJJZLr4jXNUByx3gR+TDYZCRXEktiZoUR3PGy4qCmSbkxcIle8GEwGpb5JBZazlaJ/CxLidXdEbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.31.0", + "@eslint/plugin-kit": "^0.3.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.5", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz", + "integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.1.tgz", + "integrity": "sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.5", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", + "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/exsolve": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-shallow-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fast-shallow-equal/-/fast-shallow-equal-1.0.0.tgz", + "integrity": "sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==" + }, + "node_modules/fastest-stable-stringify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fastest-stable-stringify/-/fastest-stable-stringify-2.0.2.tgz", + "integrity": "sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==", + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/float-tooltip": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/float-tooltip/-/float-tooltip-1.7.5.tgz", + "integrity": "sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg==", + "license": "MIT", + "dependencies": { + "d3-selection": "2 - 3", + "kapsule": "^1.16", + "preact": "10" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/force-graph": { + "version": "1.50.1", + "resolved": "https://registry.npmjs.org/force-graph/-/force-graph-1.50.1.tgz", + "integrity": "sha512-CtldBdsUHLmlnerVYe09V9Bxi5iz8GZce1WdBSkwGAFgNFTYn6cW90NQ1lOh/UVm0NhktMRHKugXrS9Sl8Bl3A==", + "license": "MIT", + "dependencies": { + "@tweenjs/tween.js": "18 - 25", + "accessor-fn": "1", + "bezier-js": "3 - 6", + "canvas-color-tracker": "^1.3", + "d3-array": "1 - 3", + "d3-drag": "2 - 3", + "d3-force-3d": "2 - 3", + "d3-scale": "1 - 4", + "d3-scale-chromatic": "1 - 3", + "d3-selection": "2 - 3", + "d3-zoom": "2 - 3", + "float-tooltip": "^1.7", + "index-array-by": "1", + "kapsule": "^1.16", + "lodash-es": "4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fuse.js": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.1.0.tgz", + "integrity": "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/goober": { + "version": "2.1.16", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.16.tgz", + "integrity": "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==", + "license": "MIT", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, + "node_modules/html-parse-stringify": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz", + "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==", + "license": "MIT", + "dependencies": { + "void-elements": "3.1.0" + } + }, + "node_modules/hyphenate-style-name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", + "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==", + "license": "BSD-3-Clause" + }, + "node_modules/i18next": { + "version": "25.3.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-25.3.2.tgz", + "integrity": "sha512-JSnbZDxRVbphc5jiptxr3o2zocy5dEqpVm9qCGdJwRNO+9saUJS0/u4LnM/13C23fUEWxAylPqKU/NpMV/IjqA==", + "funding": [ + { + "type": "individual", + "url": "https://locize.com" + }, + { + "type": "individual", + "url": "https://locize.com/i18next.html" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + } + ], + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6" + }, + "peerDependencies": { + "typescript": "^5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/index-array-by": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/index-array-by/-/index-array-by-1.4.2.tgz", + "integrity": "sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/inline-style-prefixer": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/inline-style-prefixer/-/inline-style-prefixer-7.0.1.tgz", + "integrity": "sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==", + "license": "MIT", + "dependencies": { + "css-in-js-utils": "^3.1.0" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.2.tgz", + "integrity": "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/javascript-natural-sort": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz", + "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jerrypick": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/jerrypick/-/jerrypick-1.1.2.tgz", + "integrity": "sha512-YKnxXEekXKzhpf7CLYA0A+oDP8V0OhICNCr5lv96FvSsDEmrb0GKM776JgQvHTMjr7DTTPEVv/1Ciaw0uEWzBA==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/jiti": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/js-base64": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", + "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/js-cookie": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-2.2.1.tgz", + "integrity": "sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/kapsule": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/kapsule/-/kapsule-1.16.3.tgz", + "integrity": "sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==", + "license": "MIT", + "dependencies": { + "lodash-es": "4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/katex": { + "version": "0.16.22", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz", + "integrity": "sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "license": "MIT" + }, + "node_modules/langium": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz", + "integrity": "sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==", + "license": "MIT", + "dependencies": { + "chevrotain": "~11.0.3", + "chevrotain-allstar": "~0.3.0", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.11", + "vscode-uri": "~3.0.8" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/launch-ide": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/launch-ide/-/launch-ide-1.0.1.tgz", + "integrity": "sha512-U7qBxSNk774PxWq4XbmRe0ThiIstPoa4sMH/OGSYxrFVvg8x3biXcF1fsH6wasDpEmEXMdINUrQhBdwsSgKyMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "dotenv": "^16.1.4" + } + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, + "node_modules/leaflet": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/leaflet/-/leaflet-1.9.4.tgz", + "integrity": "sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==", + "license": "BSD-2-Clause" + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", + "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.30.1", + "lightningcss-darwin-x64": "1.30.1", + "lightningcss-freebsd-x64": "1.30.1", + "lightningcss-linux-arm-gnueabihf": "1.30.1", + "lightningcss-linux-arm64-gnu": "1.30.1", + "lightningcss-linux-arm64-musl": "1.30.1", + "lightningcss-linux-x64-gnu": "1.30.1", + "lightningcss-linux-x64-musl": "1.30.1", + "lightningcss-win32-arm64-msvc": "1.30.1", + "lightningcss-win32-x64-msvc": "1.30.1" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", + "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", + "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", + "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", + "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", + "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", + "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", + "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", + "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", + "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.30.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", + "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/local-pkg": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.1.tgz", + "integrity": "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==", + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.0.1", + "quansync": "^0.2.8" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.486.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.486.0.tgz", + "integrity": "sha512-xWop/wMsC1ikiEVLZrxXjPKw4vU/eAip33G2mZHgbWnr4Nr5Rt4Vx4s/q1D3B/rQVbxjOuqASkEZcUxDEKzecw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "license": "CC0-1.0" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/mermaid": { + "version": "11.8.1", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.8.1.tgz", + "integrity": "sha512-VSXJLqP1Sqw5sGr273mhvpPRhXwE6NlmMSqBZQw+yZJoAJkOIPPn/uT3teeCBx60Fkt5zEI3FrH2eVT0jXRDzw==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.0.4", + "@iconify/utils": "^2.1.33", + "@mermaid-js/parser": "^0.6.1", + "@types/d3": "^7.4.3", + "cytoscape": "^3.29.3", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.11", + "dayjs": "^1.11.13", + "dompurify": "^3.2.5", + "katex": "^0.16.9", + "khroma": "^2.1.0", + "lodash-es": "^4.17.21", + "marked": "^15.0.7", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0" + } + }, + "node_modules/mermaid/node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mlly": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.4.tgz", + "integrity": "sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==", + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "pathe": "^2.0.1", + "pkg-types": "^1.3.0", + "ufo": "^1.5.4" + } + }, + "node_modules/mlly/node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, + "node_modules/mlly/node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/mobx": { + "version": "6.13.7", + "resolved": "https://registry.npmjs.org/mobx/-/mobx-6.13.7.tgz", + "integrity": "sha512-aChaVU/DO5aRPmk1GX8L+whocagUUpBQqoPtJk+cm7UOXUk87J4PeWCh6nNmTTIfEhiR9DI/+FnA8dln/hTK7g==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mobx" + } + }, + "node_modules/mobx-react-lite": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mobx-react-lite/-/mobx-react-lite-4.1.0.tgz", + "integrity": "sha512-QEP10dpHHBeQNv1pks3WnHRCem2Zp636lq54M2nKO2Sarr13pL4u6diQXf65yzXUn0mkk18SyIDCm9UOJYTi1w==", + "license": "MIT", + "dependencies": { + "use-sync-external-store": "^1.4.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mobx" + }, + "peerDependencies": { + "mobx": "^6.9.0", + "react": "^16.8.0 || ^17 || ^18 || ^19" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nano-css": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/nano-css/-/nano-css-5.6.2.tgz", + "integrity": "sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw==", + "license": "Unlicense", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "css-tree": "^1.1.2", + "csstype": "^3.1.2", + "fastest-stable-stringify": "^2.0.2", + "inline-style-prefixer": "^7.0.1", + "rtl-css-js": "^1.16.1", + "stacktrace-js": "^2.0.2", + "stylis": "^4.3.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/nano-css/node_modules/stylis": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz", + "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nice-grpc-common": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/nice-grpc-common/-/nice-grpc-common-2.0.2.tgz", + "integrity": "sha512-7RNWbls5kAL1QVUOXvBsv1uO0wPQK3lHv+cY1gwkTzirnG1Nop4cBJZubpgziNbaVc/bl9QJcyvsf/NQxa3rjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ts-error": "^1.0.6" + } + }, + "node_modules/nice-grpc-web": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nice-grpc-web/-/nice-grpc-web-3.3.7.tgz", + "integrity": "sha512-9RyOGxmPm5rCLizn2uNFSSnlBPU/2n1i8inaNSAT52QAcOITKvCTFmOUnU+1CAJrKlxiGV64KFfcxJG77JDV5w==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller-x": "^0.4.0", + "isomorphic-ws": "^5.0.0", + "js-base64": "^3.7.2", + "nice-grpc-common": "^2.0.2" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-manager-detector": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.3.0.tgz", + "integrity": "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==", + "license": "MIT" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.2.0.tgz", + "integrity": "sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, + "node_modules/portfinder": { + "version": "1.0.37", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.37.tgz", + "integrity": "sha512-yuGIEjDAYnnOex9ddMnKZEMFE0CcGo6zbfzDklkmT1m5z734ss6JMzN9rNB3+RR7iS+F10D4/BVIaXOyh8PQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^3.2.6", + "debug": "^4.3.6" + }, + "engines": { + "node": ">= 10.12" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/preact": { + "version": "10.26.9", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.26.9.tgz", + "integrity": "sha512-SSjF9vcnF27mJK1XyFMNJzFd5u3pQiATFqoaDy03XuN00u4ziveVVEGt5RKJrDR8MHE/wJo9Nnad56RLzS2RMA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quansync": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-0.2.10.tgz", + "integrity": "sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-force-graph-2d": { + "version": "1.28.0", + "resolved": "https://registry.npmjs.org/react-force-graph-2d/-/react-force-graph-2d-1.28.0.tgz", + "integrity": "sha512-NYA8GLxJnoZyLWjob8xea38B1cZqSGdcA8lDpvTc1hcJrpzFyBEHkeJ4xtFoJp66tsM4PAlj5af4HWnU0OQ3Sg==", + "license": "MIT", + "dependencies": { + "force-graph": "^1.50", + "prop-types": "15", + "react-kapsule": "^2.5" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-hot-toast": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.5.2.tgz", + "integrity": "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw==", + "license": "MIT", + "dependencies": { + "csstype": "^3.1.3", + "goober": "^2.1.16" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/react-i18next": { + "version": "15.6.0", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.6.0.tgz", + "integrity": "sha512-W135dB0rDfiFmbMipC17nOhGdttO5mzH8BivY+2ybsQBbXvxWIwl3cmeH3T9d+YPBSJu/ouyJKFJTtkK7rJofw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.27.6", + "html-parse-stringify": "^3.0.1" + }, + "peerDependencies": { + "i18next": ">= 23.2.3", + "react": ">= 16.8.0", + "typescript": "^5" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-kapsule": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/react-kapsule/-/react-kapsule-2.5.7.tgz", + "integrity": "sha512-kifAF4ZPD77qZKc4CKLmozq6GY1sBzPEJTIJb0wWFK6HsePJatK3jXplZn2eeAt3x67CDozgi7/rO8fNQ/AL7A==", + "license": "MIT", + "dependencies": { + "jerrypick": "^1.1.1" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "react": ">=16.13.1" + } + }, + "node_modules/react-leaflet": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/react-leaflet/-/react-leaflet-4.2.1.tgz", + "integrity": "sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==", + "license": "Hippocratic-2.1", + "dependencies": { + "@react-leaflet/core": "^2.1.0" + }, + "peerDependencies": { + "leaflet": "^1.9.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-remove-scroll": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.7.1.tgz", + "integrity": "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==", + "license": "MIT", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "license": "MIT", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-router": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.6.3.tgz", + "integrity": "sha512-zf45LZp5skDC6I3jDLXQUu0u26jtuP4lEGbc7BbdyxenBN1vJSTA18czM2D+h5qyMBuMrD+9uB+mU37HIoKGRA==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.6.3.tgz", + "integrity": "sha512-DiWJm9qdUAmiJrVWaeJdu4TKu13+iB/8IEi0EW/XgaHCjW/vWGrwzup0GVvaMteuZjKnh5bEvJP/K0MDnzawHw==", + "license": "MIT", + "dependencies": { + "react-router": "7.6.3" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-simple-pull-to-refresh": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/react-simple-pull-to-refresh/-/react-simple-pull-to-refresh-1.3.3.tgz", + "integrity": "sha512-6qXsa5RtNVmKJhLWvDLIX8UK51HFtCEGjdqQGf+M1Qjrcc4qH4fki97sgVpGEFBRwbY7DiVDA5N5p97kF16DTw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.10.2 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.10.2 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "license": "MIT", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-universal-interface": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/react-universal-interface/-/react-universal-interface-0.6.2.tgz", + "integrity": "sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==", + "peerDependencies": { + "react": "*", + "tslib": "*" + } + }, + "node_modules/react-use": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/react-use/-/react-use-17.6.0.tgz", + "integrity": "sha512-OmedEScUMKFfzn1Ir8dBxiLLSOzhKe/dPZwVxcujweSj45aNM7BEGPb9BEVIgVEqEXx6f3/TsXzwIktNgUR02g==", + "license": "Unlicense", + "dependencies": { + "@types/js-cookie": "^2.2.6", + "@xobotyi/scrollbar-width": "^1.9.5", + "copy-to-clipboard": "^3.3.1", + "fast-deep-equal": "^3.1.3", + "fast-shallow-equal": "^1.0.0", + "js-cookie": "^2.2.1", + "nano-css": "^5.6.2", + "react-universal-interface": "^0.6.2", + "resize-observer-polyfill": "^1.5.1", + "screenfull": "^5.1.0", + "set-harmonic-interval": "^1.0.1", + "throttle-debounce": "^3.0.1", + "ts-easing": "^0.2.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, + "node_modules/rollup": { + "version": "4.44.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.44.2.tgz", + "integrity": "sha512-PVoapzTwSEcelaWGth3uR66u7ZRo6qhPHc0f2uRO9fX6XDVNrIiGYS0Pj9+R8yIIYSD/mCx2b16Ws9itljKSPg==", + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.44.2", + "@rollup/rollup-android-arm64": "4.44.2", + "@rollup/rollup-darwin-arm64": "4.44.2", + "@rollup/rollup-darwin-x64": "4.44.2", + "@rollup/rollup-freebsd-arm64": "4.44.2", + "@rollup/rollup-freebsd-x64": "4.44.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.44.2", + "@rollup/rollup-linux-arm-musleabihf": "4.44.2", + "@rollup/rollup-linux-arm64-gnu": "4.44.2", + "@rollup/rollup-linux-arm64-musl": "4.44.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.44.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.44.2", + "@rollup/rollup-linux-riscv64-gnu": "4.44.2", + "@rollup/rollup-linux-riscv64-musl": "4.44.2", + "@rollup/rollup-linux-s390x-gnu": "4.44.2", + "@rollup/rollup-linux-x64-gnu": "4.44.2", + "@rollup/rollup-linux-x64-musl": "4.44.2", + "@rollup/rollup-win32-arm64-msvc": "4.44.2", + "@rollup/rollup-win32-ia32-msvc": "4.44.2", + "@rollup/rollup-win32-x64-msvc": "4.44.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "node_modules/rtl-css-js": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz", + "integrity": "sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/screenfull": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/screenfull/-/screenfull-5.2.0.tgz", + "integrity": "sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", + "license": "MIT" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-harmonic-interval": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/set-harmonic-interval/-/set-harmonic-interval-1.0.1.tgz", + "integrity": "sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==", + "license": "Unlicense", + "engines": { + "node": ">=6.9" + } + }, + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "devOptional": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stack-generator": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/stack-generator/-/stack-generator-2.0.10.tgz", + "integrity": "sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==", + "license": "MIT", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==", + "license": "MIT" + }, + "node_modules/stacktrace-gps": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/stacktrace-gps/-/stacktrace-gps-3.1.2.tgz", + "integrity": "sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==", + "license": "MIT", + "dependencies": { + "source-map": "0.5.6", + "stackframe": "^1.3.4" + } + }, + "node_modules/stacktrace-gps/node_modules/source-map": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", + "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stacktrace-js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stacktrace-js/-/stacktrace-js-2.0.2.tgz", + "integrity": "sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==", + "license": "MIT", + "dependencies": { + "error-stack-parser": "^2.0.6", + "stack-generator": "^2.0.5", + "stacktrace-gps": "^3.0.4" + } + }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" + } + }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/synckit": { + "version": "0.11.8", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.8.tgz", + "integrity": "sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.4" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/tailwind-merge": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz", + "integrity": "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, + "node_modules/tailwindcss": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.11.tgz", + "integrity": "sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==", + "license": "MIT" + }, + "node_modules/tapable": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", + "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/terser": { + "version": "5.43.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", + "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", + "devOptional": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.14.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/textarea-caret": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/textarea-caret/-/textarea-caret-3.1.0.tgz", + "integrity": "sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q==", + "license": "MIT" + }, + "node_modules/throttle-debounce": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-3.0.1.tgz", + "integrity": "sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.14", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", + "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toggle-selection": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", + "license": "MIT" + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, + "node_modules/ts-easing": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/ts-easing/-/ts-easing-0.2.0.tgz", + "integrity": "sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==", + "license": "Unlicense" + }, + "node_modules/ts-error": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/ts-error/-/ts-error-1.0.6.tgz", + "integrity": "sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/tw-animate-css": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.3.5.tgz", + "integrity": "sha512-t3u+0YNoloIhj1mMXs779P6MO9q3p3mvGn4k1n3nJPqJw/glZcuijG2qTSN4z4mgNRfW5ZC3aXJFLwDtiipZXA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Wombosvideo" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typescript": { + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.36.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.36.0.tgz", + "integrity": "sha512-fTCqxthY+h9QbEgSIBfL9iV6CvKDFuoxg6bHPNpJ9HIUzS+jy2lCEyCmGyZRWEBSaykqcDPf1SJ+BfCI8DRopA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.36.0", + "@typescript-eslint/parser": "8.36.0", + "@typescript-eslint/utils": "8.36.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.9.0" + } + }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "license": "MIT" + }, + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undici-types": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", + "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "license": "MIT", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sync-external-store": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", + "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/vite": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.0.4.tgz", + "integrity": "sha512-SkaSguuS7nnmV7mfJ8l81JGBFV7Gvzp8IzgE8A8t23+AxuNX61Q5H1Tpz5efduSN7NHC8nQXD3sKQKZAu5mNEA==", + "license": "MIT", + "dependencies": { + "esbuild": "^0.25.0", + "fdir": "^6.4.6", + "picomatch": "^4.0.2", + "postcss": "^8.5.6", + "rollup": "^4.40.0", + "tinyglobby": "^0.2.14" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-code-inspector-plugin": { + "version": "0.18.3", + "resolved": "https://registry.npmjs.org/vite-code-inspector-plugin/-/vite-code-inspector-plugin-0.18.3.tgz", + "integrity": "sha512-178H73vbDUHE+JpvfAfioUHlUr7qXCYIEa2YNXtzenFQGOjtae59P1jjcxGfa6pPHEnOoaitb13K+0qxwhi/WA==", + "dev": true, + "license": "MIT", + "dependencies": { + "code-inspector-core": "0.18.3" + } + }, + "node_modules/vite/node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "license": "MIT" + }, + "node_modules/webpack-code-inspector-plugin": { + "version": "0.18.3", + "resolved": "https://registry.npmjs.org/webpack-code-inspector-plugin/-/webpack-code-inspector-plugin-0.18.3.tgz", + "integrity": "sha512-3782rsJhBnRiw0IpR6EqnyGDQoiSq0CcGeLJ52rZXlszYCe8igXtcujq7OhI0byaivWQ1LW7sXKyMEoVpBhq0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "code-inspector-core": "0.18.3" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..ef56adc --- /dev/null +++ b/web/package.json @@ -0,0 +1,93 @@ +{ + "name": "memos", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "release": "vite build --mode release --outDir=../server/router/frontend/dist --emptyOutDir", + "lint": "tsc --noEmit --skipLibCheck && eslint --ext .js,.ts,.tsx, src" + }, + "dependencies": { + "@dnd-kit/core": "^6.3.1", + "@dnd-kit/sortable": "^10.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.1", + "@github/relative-time-element": "^4.4.8", + "@matejmazur/react-katex": "^3.1.3", + "@radix-ui/react-checkbox": "^1.3.2", + "@radix-ui/react-dialog": "^1.1.14", + "@radix-ui/react-dropdown-menu": "^2.1.15", + "@radix-ui/react-label": "^2.1.7", + "@radix-ui/react-popover": "^1.1.14", + "@radix-ui/react-radio-group": "^1.3.7", + "@radix-ui/react-select": "^2.2.5", + "@radix-ui/react-separator": "^1.1.7", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.5", + "@radix-ui/react-tooltip": "^1.2.7", + "@tailwindcss/vite": "^4.1.11", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "copy-to-clipboard": "^3.3.3", + "dayjs": "^1.11.13", + "fuse.js": "^7.1.0", + "highlight.js": "^11.11.1", + "i18next": "^25.3.0", + "katex": "^0.16.22", + "leaflet": "^1.9.4", + "lodash-es": "^4.17.21", + "lucide-react": "^0.486.0", + "mermaid": "^11.8.0", + "mobx": "^6.13.7", + "mobx-react-lite": "^4.1.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-force-graph-2d": "^1.28.0", + "react-hot-toast": "^2.5.2", + "react-i18next": "^15.5.3", + "react-leaflet": "^4.2.1", + "react-router-dom": "^7.6.3", + "react-simple-pull-to-refresh": "^1.3.3", + "react-use": "^17.6.0", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^4.1.11", + "textarea-caret": "^3.1.0", + "uuid": "^11.1.0" + }, + "devDependencies": { + "@bufbuild/protobuf": "^2.6.0", + "@eslint/js": "^9.30.1", + "@trivago/prettier-plugin-sort-imports": "^5.2.2", + "@types/d3": "^7.4.3", + "@types/katex": "^0.16.7", + "@types/leaflet": "^1.9.19", + "@types/lodash-es": "^4.17.12", + "@types/node": "^24.0.10", + "@types/qs": "^6.14.0", + "@types/react": "^18.3.23", + "@types/react-dom": "^18.3.7", + "@types/textarea-caret": "^3.0.4", + "@types/uuid": "^10.0.0", + "@vitejs/plugin-react": "^4.6.0", + "code-inspector-plugin": "^0.18.3", + "eslint": "^9.30.1", + "eslint-config-prettier": "^10.1.5", + "eslint-plugin-prettier": "^5.5.1", + "eslint-plugin-react": "^7.37.5", + "long": "^5.3.2", + "nice-grpc-web": "^3.3.7", + "prettier": "^3.6.2", + "terser": "^5.43.1", + "tw-animate-css": "^1.3.4", + "typescript": "^5.8.3", + "typescript-eslint": "^8.35.1", + "vite": "^7.0.1" + }, + "pnpm": { + "onlyBuiltDependencies": [ + "esbuild" + ] + } +} \ No newline at end of file diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml new file mode 100644 index 0000000..cf9f0f2 --- /dev/null +++ b/web/pnpm-lock.yaml @@ -0,0 +1,7056 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@dnd-kit/core': + specifier: ^6.3.1 + version: 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/sortable': + specifier: ^10.0.0 + version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@dnd-kit/utilities': + specifier: ^3.2.2 + version: 3.2.2(react@18.3.1) + '@emotion/react': + specifier: ^11.14.0 + version: 11.14.0(@types/react@18.3.23)(react@18.3.1) + '@emotion/styled': + specifier: ^11.14.1 + version: 11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1) + '@github/relative-time-element': + specifier: ^4.4.8 + version: 4.4.8 + '@matejmazur/react-katex': + specifier: ^3.1.3 + version: 3.1.3(katex@0.16.22)(react@18.3.1) + '@radix-ui/react-checkbox': + specifier: ^1.3.2 + version: 1.3.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dialog': + specifier: ^1.1.14 + version: 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.15 + version: 2.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-label': + specifier: ^2.1.7 + version: 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-popover': + specifier: ^1.1.14 + version: 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-radio-group': + specifier: ^1.3.7 + version: 1.3.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-select': + specifier: ^2.2.5 + version: 2.2.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-separator': + specifier: ^1.1.7 + version: 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': + specifier: ^1.2.3 + version: 1.2.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-switch': + specifier: ^1.2.5 + version: 1.2.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tooltip': + specifier: ^1.2.7 + version: 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@tailwindcss/vite': + specifier: ^4.1.11 + version: 4.1.11(vite@7.0.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)) + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + cmdk: + specifier: ^1.1.1 + version: 1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + copy-to-clipboard: + specifier: ^3.3.3 + version: 3.3.3 + dayjs: + specifier: ^1.11.13 + version: 1.11.13 + fuse.js: + specifier: ^7.1.0 + version: 7.1.0 + highlight.js: + specifier: ^11.11.1 + version: 11.11.1 + i18next: + specifier: ^25.3.0 + version: 25.3.0(typescript@5.8.3) + katex: + specifier: ^0.16.22 + version: 0.16.22 + leaflet: + specifier: ^1.9.4 + version: 1.9.4 + lodash-es: + specifier: ^4.17.21 + version: 4.17.21 + lucide-react: + specifier: ^0.486.0 + version: 0.486.0(react@18.3.1) + mermaid: + specifier: ^11.8.0 + version: 11.8.0 + mobx: + specifier: ^6.13.7 + version: 6.13.7 + mobx-react-lite: + specifier: ^4.1.0 + version: 4.1.0(mobx@6.13.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + react-force-graph-2d: + specifier: ^1.28.0 + version: 1.28.0(react@18.3.1) + react-hot-toast: + specifier: ^2.5.2 + version: 2.5.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-i18next: + specifier: ^15.5.3 + version: 15.5.3(i18next@25.3.0(typescript@5.8.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3) + react-leaflet: + specifier: ^4.2.1 + version: 4.2.1(leaflet@1.9.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-router-dom: + specifier: ^7.6.3 + version: 7.6.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-simple-pull-to-refresh: + specifier: ^1.3.3 + version: 1.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-use: + specifier: ^17.6.0 + version: 17.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tailwind-merge: + specifier: ^3.3.1 + version: 3.3.1 + tailwindcss: + specifier: ^4.1.11 + version: 4.1.11 + textarea-caret: + specifier: ^3.1.0 + version: 3.1.0 + uuid: + specifier: ^11.1.0 + version: 11.1.0 + devDependencies: + '@bufbuild/protobuf': + specifier: ^2.6.0 + version: 2.6.0 + '@eslint/js': + specifier: ^9.30.1 + version: 9.30.1 + '@trivago/prettier-plugin-sort-imports': + specifier: ^5.2.2 + version: 5.2.2(prettier@3.6.2) + '@types/d3': + specifier: ^7.4.3 + version: 7.4.3 + '@types/katex': + specifier: ^0.16.7 + version: 0.16.7 + '@types/leaflet': + specifier: ^1.9.19 + version: 1.9.19 + '@types/lodash-es': + specifier: ^4.17.12 + version: 4.17.12 + '@types/node': + specifier: ^24.0.10 + version: 24.0.10 + '@types/qs': + specifier: ^6.14.0 + version: 6.14.0 + '@types/react': + specifier: ^18.3.23 + version: 18.3.23 + '@types/react-dom': + specifier: ^18.3.7 + version: 18.3.7(@types/react@18.3.23) + '@types/textarea-caret': + specifier: ^3.0.4 + version: 3.0.4 + '@types/uuid': + specifier: ^10.0.0 + version: 10.0.0 + '@vitejs/plugin-react': + specifier: ^4.6.0 + version: 4.6.0(vite@7.0.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1)) + code-inspector-plugin: + specifier: ^0.18.3 + version: 0.18.3 + eslint: + specifier: ^9.30.1 + version: 9.30.1(jiti@2.4.2) + eslint-config-prettier: + specifier: ^10.1.5 + version: 10.1.5(eslint@9.30.1(jiti@2.4.2)) + eslint-plugin-prettier: + specifier: ^5.5.1 + version: 5.5.1(eslint-config-prettier@10.1.5(eslint@9.30.1(jiti@2.4.2)))(eslint@9.30.1(jiti@2.4.2))(prettier@3.6.2) + eslint-plugin-react: + specifier: ^7.37.5 + version: 7.37.5(eslint@9.30.1(jiti@2.4.2)) + long: + specifier: ^5.3.2 + version: 5.3.2 + nice-grpc-web: + specifier: ^3.3.7 + version: 3.3.7(ws@8.18.3) + prettier: + specifier: ^3.6.2 + version: 3.6.2 + terser: + specifier: ^5.43.1 + version: 5.43.1 + tw-animate-css: + specifier: ^1.3.4 + version: 1.3.4 + typescript: + specifier: ^5.8.3 + version: 5.8.3 + typescript-eslint: + specifier: ^8.35.1 + version: 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + vite: + specifier: ^7.0.1 + version: 7.0.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1) + +packages: + + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@antfu/install-pkg@1.1.0': + resolution: {integrity: sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==} + + '@antfu/utils@8.1.1': + resolution: {integrity: sha512-Mex9nXf9vR6AhcXmMrlz/HVgYYZpVGJ6YlPgwl7UnaFpnshXs6EK/oa5Gpf3CzENMjkvEx2tQtntGnb7UtSTOQ==} + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.0': + resolution: {integrity: sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.0': + resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.0': + resolution: {integrity: sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.27.3': + resolution: {integrity: sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.27.6': + resolution: {integrity: sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.0': + resolution: {integrity: sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.27.1': + resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.27.1': + resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.27.6': + resolution: {integrity: sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.0': + resolution: {integrity: sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.0': + resolution: {integrity: sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg==} + engines: {node: '>=6.9.0'} + + '@braintree/sanitize-url@7.1.1': + resolution: {integrity: sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==} + + '@bufbuild/protobuf@2.6.0': + resolution: {integrity: sha512-6cuonJVNOIL7lTj5zgo/Rc2bKAo4/GvN+rKCrUj7GdEHRzCk8zKOfFwUsL9nAVk5rSIsRmlgcpLzTRysopEeeg==} + + '@chevrotain/cst-dts-gen@11.0.3': + resolution: {integrity: sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==} + + '@chevrotain/gast@11.0.3': + resolution: {integrity: sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==} + + '@chevrotain/regexp-to-ast@11.0.3': + resolution: {integrity: sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==} + + '@chevrotain/types@11.0.3': + resolution: {integrity: sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==} + + '@chevrotain/utils@11.0.3': + resolution: {integrity: sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==} + + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/sortable@10.0.0': + resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + + '@emotion/babel-plugin@11.13.5': + resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} + + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + + '@emotion/is-prop-valid@1.3.1': + resolution: {integrity: sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@emotion/react@11.14.0': + resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} + + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + + '@emotion/styled@11.14.1': + resolution: {integrity: sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==} + peerDependencies: + '@emotion/react': ^11.0.0-rc.0 + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0': + resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} + peerDependencies: + react: '>=16.8.0' + + '@emotion/utils@1.4.2': + resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} + + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + + '@esbuild/aix-ppc64@0.25.5': + resolution: {integrity: sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.5': + resolution: {integrity: sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.5': + resolution: {integrity: sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.5': + resolution: {integrity: sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.5': + resolution: {integrity: sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.5': + resolution: {integrity: sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.5': + resolution: {integrity: sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.5': + resolution: {integrity: sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.5': + resolution: {integrity: sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.5': + resolution: {integrity: sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.5': + resolution: {integrity: sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.5': + resolution: {integrity: sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.5': + resolution: {integrity: sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.5': + resolution: {integrity: sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.5': + resolution: {integrity: sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.5': + resolution: {integrity: sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.5': + resolution: {integrity: sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.5': + resolution: {integrity: sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.5': + resolution: {integrity: sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.5': + resolution: {integrity: sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.5': + resolution: {integrity: sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.25.5': + resolution: {integrity: sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.5': + resolution: {integrity: sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.5': + resolution: {integrity: sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.5': + resolution: {integrity: sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.7.0': + resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.1': + resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.0': + resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.3.0': + resolution: {integrity: sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.14.0': + resolution: {integrity: sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.15.1': + resolution: {integrity: sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.1': + resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.30.1': + resolution: {integrity: sha512-zXhuECFlyep42KZUhWjfvsmXGX39W8K8LFb8AWXM9gSV9dQB+MrJGLKvW6Zw0Ggnbpw0VHTtrhFXYe3Gym18jg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.6': + resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.3.3': + resolution: {integrity: sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@floating-ui/core@1.7.2': + resolution: {integrity: sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==} + + '@floating-ui/dom@1.7.2': + resolution: {integrity: sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==} + + '@floating-ui/react-dom@2.1.4': + resolution: {integrity: sha512-JbbpPhp38UmXDDAu60RJmbeme37Jbgsm7NrHGgzYYFKmblzRUh6Pa641dII6LsjwF4XlScDrde2UAzDo/b9KPw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@github/relative-time-element@4.4.8': + resolution: {integrity: sha512-FSLYm6F3TSQnqHE1EMQUVVgi2XjbCvsESwwXfugHFpBnhyF1uhJOtu0Psp/BB/qqazfdkk7f5fVcu7WuXl3t8Q==} + + '@humanfs/core@0.19.1': + resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.6': + resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.3.1': + resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==} + engines: {node: '>=18.18'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@iconify/types@2.0.0': + resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} + + '@iconify/utils@2.3.0': + resolution: {integrity: sha512-GmQ78prtwYW6EtzXRU1rY+KwOKfz32PD7iJh6Iyqw68GiKuoZ2A6pRtzWONz5VQJbp50mEjXh/7NkumtrAgRKA==} + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@jridgewell/gen-mapping@0.3.12': + resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/source-map@0.3.10': + resolution: {integrity: sha512-0pPkgz9dY+bijgistcTTJ5mR+ocqRXLuhXHYdzoMmmoJ2C9S46RCm2GMUbatPEUK9Yjy26IrAy8D/M00lLkv+Q==} + + '@jridgewell/sourcemap-codec@1.5.4': + resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==} + + '@jridgewell/trace-mapping@0.3.29': + resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + + '@matejmazur/react-katex@3.1.3': + resolution: {integrity: sha512-rBp7mJ9An7ktNoU653BWOYdO4FoR4YNwofHZi+vaytX/nWbIlmHVIF+X8VFOn6c3WYmrLT5FFBjKqCZ1sjR5uQ==} + engines: {node: '>=12', yarn: '>=1.1'} + peerDependencies: + katex: '>=0.9' + react: '>=16' + + '@mermaid-js/parser@0.6.0': + resolution: {integrity: sha512-7DNESgpyZ5WG1SIkrYafVBhWmImtmQuoxOO1lawI3gQYWxBX3v1FW3IyuuRfKJAO06XrZR71W0Kif5VEGGd4VA==} + + '@nodelib/fs.scandir@2.1.5': + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + + '@nodelib/fs.stat@2.0.5': + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + + '@nodelib/fs.walk@1.2.8': + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + + '@pkgr/core@0.2.7': + resolution: {integrity: sha512-YLT9Zo3oNPJoBjBc4q8G2mjU4tqIbf5CEOORbUUr48dCD9q3umJ3IPlVqOqDakPfd2HuwccBaqlGhN4Gmr5OWg==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} + + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.2': + resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-checkbox@1.3.2': + resolution: {integrity: sha512-yd+dI56KZqawxKZrJ31eENUwqc1QSqg4OZ15rybGjF2ZNwMO+wCyHzAVLRp9qoYJf7kYy0YpZ2b0JCzJ42HZpA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.14': + resolution: {integrity: sha512-+CpweKjqpzTmwRwcYECQcNYbI8V9VSQt0SNFKeEBLgfucbsLssU6Ppq7wUdNXEGb573bMjFhVjKVll8rmV6zMw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.10': + resolution: {integrity: sha512-IM1zzRV4W3HtVgftdQiiOmA0AdJlCtMLe00FXaHwgt3rAnNsIyDqshvkIW3hj/iu5hu8ERP7KIYki6NkqDxAwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.15': + resolution: {integrity: sha512-mIBnOjgwo9AH3FyKaSWoSu/dYj6VdhJ7frEPiGTeXCdUFHjl9h3mFh2wwhEtINOmYXWhdpf1rY2minFsmaNgVQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.2': + resolution: {integrity: sha512-fyjAACV62oPV925xFCrH8DR5xWhg9KYtJT4s3u54jxp+L/hbpTY2kIeEFFbFe+a/HCE94zGQMZLIpVTPVZDhaA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.7': + resolution: {integrity: sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menu@2.1.15': + resolution: {integrity: sha512-tVlmA3Vb9n8SZSd+YSbuFR66l87Wiy4du+YE+0hzKQEANA+7cWKH1WgqcEX4pXqxUFQKrWQGHdvEfw00TjFiew==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.14': + resolution: {integrity: sha512-ODz16+1iIbGUfFEfKx2HTPKizg2MN39uIOV8MXeHnmdd3i/N9Wt7vU46wbHsqA0xoaQyXVcs0KIlBdOA2Y95bw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.7': + resolution: {integrity: sha512-IUFAccz1JyKcf/RjB552PlWwxjeCJB8/4KxT7EhBHOJM+mN7LdW+B3kacJXILm32xawcMMjb2i0cIZpo+f9kiQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.4': + resolution: {integrity: sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-radio-group@1.3.7': + resolution: {integrity: sha512-9w5XhD0KPOrm92OTTE0SysH3sYzHsSTHNvZgUBo/VZ80VdYyB5RneDbc0dKpURS24IxkoFRu/hI0i4XyfFwY6g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.10': + resolution: {integrity: sha512-dT9aOXUen9JSsxnMPv/0VqySQf5eDQ6LCk5Sw28kamz8wSOW2bJdlX2Bg5VUIIcV+6XlHpWTIuTPCf/UNIyq8Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.2.5': + resolution: {integrity: sha512-HnMTdXEVuuyzx63ME0ut4+sEMYW6oouHWNGUZc7ddvUWIcfCva/AMoqEW/3wnEllriMWBa0RHspCYnfCWJQYmA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.7': + resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.2.5': + resolution: {integrity: sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.2.7': + resolution: {integrity: sha512-Ap+fNYwKTYJ9pzqW+Xe2HtMRbQ/EeWkj2qykZ6SuEV4iS/o1bZI5ssJbk4D2r8XuDuOBVz/tIx2JObtuqU+5Zw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + + '@react-leaflet/core@2.1.0': + resolution: {integrity: sha512-Qk7Pfu8BSarKGqILj4x7bCSZ1pjuAPZ+qmRwH5S7mDS91VSbVVsJSrW4qA+GPrro8t69gFYVMWb1Zc4yFmPiVg==} + peerDependencies: + leaflet: ^1.9.0 + react: ^18.0.0 + react-dom: ^18.0.0 + + '@rolldown/pluginutils@1.0.0-beta.19': + resolution: {integrity: sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA==} + + '@rollup/rollup-android-arm-eabi@4.44.1': + resolution: {integrity: sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.44.1': + resolution: {integrity: sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.44.1': + resolution: {integrity: sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.44.1': + resolution: {integrity: sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.44.1': + resolution: {integrity: sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.44.1': + resolution: {integrity: sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.44.1': + resolution: {integrity: sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.44.1': + resolution: {integrity: sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.44.1': + resolution: {integrity: sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.44.1': + resolution: {integrity: sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.44.1': + resolution: {integrity: sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.44.1': + resolution: {integrity: sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.44.1': + resolution: {integrity: sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.44.1': + resolution: {integrity: sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.44.1': + resolution: {integrity: sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.44.1': + resolution: {integrity: sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.44.1': + resolution: {integrity: sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.44.1': + resolution: {integrity: sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.44.1': + resolution: {integrity: sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.44.1': + resolution: {integrity: sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug==} + cpu: [x64] + os: [win32] + + '@tailwindcss/node@4.1.11': + resolution: {integrity: sha512-yzhzuGRmv5QyU9qLNg4GTlYI6STedBWRE7NjxP45CsFYYq9taI0zJXZBMqIC/c8fViNLhmrbpSFS57EoxUmD6Q==} + + '@tailwindcss/oxide-android-arm64@4.1.11': + resolution: {integrity: sha512-3IfFuATVRUMZZprEIx9OGDjG3Ou3jG4xQzNTvjDoKmU9JdmoCohQJ83MYd0GPnQIu89YoJqvMM0G3uqLRFtetg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.11': + resolution: {integrity: sha512-ESgStEOEsyg8J5YcMb1xl8WFOXfeBmrhAwGsFxxB2CxY9evy63+AtpbDLAyRkJnxLy2WsD1qF13E97uQyP1lfQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.11': + resolution: {integrity: sha512-EgnK8kRchgmgzG6jE10UQNaH9Mwi2n+yw1jWmof9Vyg2lpKNX2ioe7CJdf9M5f8V9uaQxInenZkOxnTVL3fhAw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.11': + resolution: {integrity: sha512-xdqKtbpHs7pQhIKmqVpxStnY1skuNh4CtbcyOHeX1YBE0hArj2romsFGb6yUmzkq/6M24nkxDqU8GYrKrz+UcA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11': + resolution: {integrity: sha512-ryHQK2eyDYYMwB5wZL46uoxz2zzDZsFBwfjssgB7pzytAeCCa6glsiJGjhTEddq/4OsIjsLNMAiMlHNYnkEEeg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.11': + resolution: {integrity: sha512-mYwqheq4BXF83j/w75ewkPJmPZIqqP1nhoghS9D57CLjsh3Nfq0m4ftTotRYtGnZd3eCztgbSPJ9QhfC91gDZQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.11': + resolution: {integrity: sha512-m/NVRFNGlEHJrNVk3O6I9ggVuNjXHIPoD6bqay/pubtYC9QIdAMpS+cswZQPBLvVvEF6GtSNONbDkZrjWZXYNQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.11': + resolution: {integrity: sha512-YW6sblI7xukSD2TdbbaeQVDysIm/UPJtObHJHKxDEcW2exAtY47j52f8jZXkqE1krdnkhCMGqP3dbniu1Te2Fg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.11': + resolution: {integrity: sha512-e3C/RRhGunWYNC3aSF7exsQkdXzQ/M+aYuZHKnw4U7KQwTJotnWsGOIVih0s2qQzmEzOFIJ3+xt7iq67K/p56Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.11': + resolution: {integrity: sha512-Xo1+/GU0JEN/C/dvcammKHzeM6NqKovG+6921MR6oadee5XPBaKOumrJCXvopJ/Qb5TH7LX/UAywbqrP4lax0g==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.11': + resolution: {integrity: sha512-UgKYx5PwEKrac3GPNPf6HVMNhUIGuUh4wlDFR2jYYdkX6pL/rn73zTq/4pzUm8fOjAn5L8zDeHp9iXmUGOXZ+w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.11': + resolution: {integrity: sha512-YfHoggn1j0LK7wR82TOucWc5LDCguHnoS879idHekmmiR7g9HUtMw9MI0NHatS28u/Xlkfi9w5RJWgz2Dl+5Qg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.11': + resolution: {integrity: sha512-Q69XzrtAhuyfHo+5/HMgr1lAiPP/G40OMFAnws7xcFEYqcypZmdW8eGXaOUIeOl1dzPJBPENXgbjsOyhg2nkrg==} + engines: {node: '>= 10'} + + '@tailwindcss/vite@4.1.11': + resolution: {integrity: sha512-RHYhrR3hku0MJFRV+fN2gNbDNEh3dwKvY8XJvTxCSXeMOsCRSr+uKvDWQcbizrHgjML6ZmTE5OwMrl5wKcujCw==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + + '@trivago/prettier-plugin-sort-imports@5.2.2': + resolution: {integrity: sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA==} + engines: {node: '>18.12'} + peerDependencies: + '@vue/compiler-sfc': 3.x + prettier: 2.x - 3.x + prettier-plugin-svelte: 3.x + svelte: 4.x || 5.x + peerDependenciesMeta: + '@vue/compiler-sfc': + optional: true + prettier-plugin-svelte: + optional: true + svelte: + optional: true + + '@tweenjs/tween.js@25.0.0': + resolution: {integrity: sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==} + + '@types/babel__core@7.20.5': + resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} + + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} + + '@types/babel__template@7.4.4': + resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + + '@types/babel__traverse@7.20.7': + resolution: {integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==} + + '@types/d3-array@3.2.1': + resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==} + + '@types/d3-axis@3.0.6': + resolution: {integrity: sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==} + + '@types/d3-brush@3.0.6': + resolution: {integrity: sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==} + + '@types/d3-chord@3.0.6': + resolution: {integrity: sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-contour@3.0.6': + resolution: {integrity: sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==} + + '@types/d3-delaunay@6.0.4': + resolution: {integrity: sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==} + + '@types/d3-dispatch@3.0.6': + resolution: {integrity: sha512-4fvZhzMeeuBJYZXRXrRIQnvUYfyXwYmLsdiN7XXmVNQKKw1cM8a5WdID0g1hVFZDqT9ZqZEY5pD44p24VS7iZQ==} + + '@types/d3-drag@3.0.7': + resolution: {integrity: sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==} + + '@types/d3-dsv@3.0.7': + resolution: {integrity: sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-fetch@3.0.7': + resolution: {integrity: sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==} + + '@types/d3-force@3.0.10': + resolution: {integrity: sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==} + + '@types/d3-format@3.0.4': + resolution: {integrity: sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==} + + '@types/d3-geo@3.1.0': + resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} + + '@types/d3-hierarchy@3.1.7': + resolution: {integrity: sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-polygon@3.0.2': + resolution: {integrity: sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==} + + '@types/d3-quadtree@3.0.6': + resolution: {integrity: sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==} + + '@types/d3-random@3.0.3': + resolution: {integrity: sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==} + + '@types/d3-scale-chromatic@3.1.0': + resolution: {integrity: sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-selection@3.0.11': + resolution: {integrity: sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==} + + '@types/d3-shape@3.1.7': + resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==} + + '@types/d3-time-format@4.0.3': + resolution: {integrity: sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/d3-transition@3.0.9': + resolution: {integrity: sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==} + + '@types/d3-zoom@3.0.8': + resolution: {integrity: sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==} + + '@types/d3@7.4.3': + resolution: {integrity: sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + + '@types/js-cookie@2.2.7': + resolution: {integrity: sha512-aLkWa0C0vO5b4Sr798E26QgOkss68Un0bLjs7u9qxzPT5CG+8DuNTffWES58YzJs3hrVAOs1wonycqEBqNJubA==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/katex@0.16.7': + resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} + + '@types/leaflet@1.9.19': + resolution: {integrity: sha512-pB+n2daHcZPF2FDaWa+6B0a0mSDf4dPU35y5iTXsx7x/PzzshiX5atYiS1jlBn43X7XvM8AP+AB26lnSk0J4GA==} + + '@types/lodash-es@4.17.12': + resolution: {integrity: sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==} + + '@types/lodash@4.17.20': + resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} + + '@types/node@24.0.10': + resolution: {integrity: sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==} + + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.23': + resolution: {integrity: sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==} + + '@types/textarea-caret@3.0.4': + resolution: {integrity: sha512-epJGYB37/sNrTDbhfyRjHkXsQSAcO6zby0JBDS0QMt6HQ1f1W2E4YpSc7TQkNmWaWmYXv92zOIfN5PHA8CmThg==} + + '@types/trusted-types@2.0.7': + resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + + '@typescript-eslint/eslint-plugin@8.35.1': + resolution: {integrity: sha512-9XNTlo7P7RJxbVeICaIIIEipqxLKguyh+3UbXuT2XQuFp6d8VOeDEGuz5IiX0dgZo8CiI6aOFLg4e8cF71SFVg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.35.1 + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/parser@8.35.1': + resolution: {integrity: sha512-3MyiDfrfLeK06bi/g9DqJxP5pV74LNv4rFTyvGDmT3x2p1yp1lOd+qYZfiRPIOf/oON+WRZR5wxxuF85qOar+w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/project-service@8.35.1': + resolution: {integrity: sha512-VYxn/5LOpVxADAuP3NrnxxHYfzVtQzLKeldIhDhzC8UHaiQvYlXvKuVho1qLduFbJjjy5U5bkGwa3rUGUb1Q6Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/scope-manager@8.35.1': + resolution: {integrity: sha512-s/Bpd4i7ht2934nG+UoSPlYXd08KYz3bmjLEb7Ye1UVob0d1ENiT3lY8bsCmik4RqfSbPw9xJJHbugpPpP5JUg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.35.1': + resolution: {integrity: sha512-K5/U9VmT9dTHoNowWZpz+/TObS3xqC5h0xAIjXPw+MNcKV9qg6eSatEnmeAwkjHijhACH0/N7bkhKvbt1+DXWQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/type-utils@8.35.1': + resolution: {integrity: sha512-HOrUBlfVRz5W2LIKpXzZoy6VTZzMu2n8q9C2V/cFngIC5U1nStJgv0tMV4sZPzdf4wQm9/ToWUFPMN9Vq9VJQQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/types@8.35.1': + resolution: {integrity: sha512-q/O04vVnKHfrrhNAscndAn1tuQhIkwqnaW+eu5waD5IPts2eX1dgJxgqcPx5BX109/qAz7IG6VrEPTOYKCNfRQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.35.1': + resolution: {integrity: sha512-Vvpuvj4tBxIka7cPs6Y1uvM7gJgdF5Uu9F+mBJBPY4MhvjrjWGK4H0lVgLJd/8PWZ23FTqsaJaLEkBCFUk8Y9g==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/utils@8.35.1': + resolution: {integrity: sha512-lhnwatFmOFcazAsUm3ZnZFpXSxiwoa1Lj50HphnDe1Et01NF4+hrdXONSUHIcbVu2eFb1bAf+5yjXkGVkXBKAQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + '@typescript-eslint/visitor-keys@8.35.1': + resolution: {integrity: sha512-VRwixir4zBWCSTP/ljEo091lbpypz57PoeAQ9imjG+vbeof9LplljsL1mos4ccG6H9IjfrVGM359RozUnuFhpw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@vitejs/plugin-react@4.6.0': + resolution: {integrity: sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0 + + '@vue/compiler-core@3.5.17': + resolution: {integrity: sha512-Xe+AittLbAyV0pabcN7cP7/BenRBNcteM4aSDCtRvGw0d9OL+HG1u/XHLY/kt1q4fyMeZYXyIYrsHuPSiDPosA==} + + '@vue/compiler-dom@3.5.17': + resolution: {integrity: sha512-+2UgfLKoaNLhgfhV5Ihnk6wB4ljyW1/7wUIog2puUqajiC29Lp5R/IKDdkebh9jTbTogTbsgB+OY9cEWzG95JQ==} + + '@vue/shared@3.5.17': + resolution: {integrity: sha512-CabR+UN630VnsJO/jHWYBC1YVXyMq94KKp6iF5MQgZJs5I8cmjw6oVMO1oDbtBkENSHSSn/UadWlW/OAgdmKrg==} + + '@xobotyi/scrollbar-width@1.9.5': + resolution: {integrity: sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==} + + abort-controller-x@0.4.3: + resolution: {integrity: sha512-VtUwTNU8fpMwvWGn4xE93ywbogTYsuT+AUxAXOeelbXuQVIwNmC5YLeho9sH4vZ4ITW8414TTAOG1nW6uIVHCA==} + + accessor-fn@1.5.3: + resolution: {integrity: sha512-rkAofCwe/FvYFUlMB0v0gWmhqtfAtV1IUkdPbfhTUyYniu5LrC0A0UJkTH0Jv3S8SvwkmfuAlY+mQIJATdocMA==} + engines: {node: '>=12'} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + array-buffer-byte-length@1.0.2: + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} + + array-includes@3.1.9: + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} + + array.prototype.findlast@1.2.5: + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} + + array.prototype.flat@1.3.3: + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} + + array.prototype.flatmap@1.3.3: + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} + + array.prototype.tosorted@1.1.4: + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} + + arraybuffer.prototype.slice@1.0.4: + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} + + async-function@1.0.0: + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + + babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + bezier-js@6.1.4: + resolution: {integrity: sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==} + + brace-expansion@1.1.12: + resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + browserslist@4.25.1: + resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bind@1.0.8: + resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001726: + resolution: {integrity: sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw==} + + canvas-color-tracker@1.3.2: + resolution: {integrity: sha512-ryQkDX26yJ3CXzb3hxUVNlg1NKE4REc5crLBq661Nxzr8TNd236SaEf2ffYLXyI5tSABSeguHLqcVq4vf9L3Zg==} + engines: {node: '>=12'} + + chalk@4.1.1: + resolution: {integrity: sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==} + engines: {node: '>=10'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chevrotain-allstar@0.3.1: + resolution: {integrity: sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==} + peerDependencies: + chevrotain: ^11.0.0 + + chevrotain@11.0.3: + resolution: {integrity: sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + cmdk@1.1.1: + resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + + code-inspector-core@0.18.3: + resolution: {integrity: sha512-60pT2cPoguMTUYdN1MMpjoPUnuF0ud/u7M2y+Vqit/bniLEit9dySEWAVxLU/Ukc5ILrDeLKEttc6fCMl9RUrA==} + + code-inspector-plugin@0.18.3: + resolution: {integrity: sha512-d9oJXZUsnvfTaQDwFmDNA2F+AR/TXIxWg1rr8KGcEskltR2prbZsfuu1z70EAn4khpx0smfi/PvIIwNJQ7FAMw==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@8.3.0: + resolution: {integrity: sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==} + engines: {node: '>= 12'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@1.0.2: + resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} + engines: {node: '>=18'} + + copy-to-clipboard@3.3.3: + resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} + + cose-base@1.0.3: + resolution: {integrity: sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==} + + cose-base@2.2.0: + resolution: {integrity: sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==} + + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-in-js-utils@3.1.0: + resolution: {integrity: sha512-fJAcud6B3rRu+KHYk+Bwf+WFL2MDCJJ1XG9x137tJQ0xYxor7XziQtuGFbWNdqrvF4Tk26O3H73nfVqXt/fW1A==} + + css-tree@1.1.3: + resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} + engines: {node: '>=8.0.0'} + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + cytoscape-cose-bilkent@4.1.0: + resolution: {integrity: sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape-fcose@2.2.0: + resolution: {integrity: sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==} + peerDependencies: + cytoscape: ^3.2.0 + + cytoscape@3.32.0: + resolution: {integrity: sha512-5JHBC9n75kz5851jeklCPmZWcg3hUe6sjqJvyk3+hVqFaKcHwHgxsjeN1yLmggoUc6STbtm9/NQyabQehfjvWQ==} + engines: {node: '>=0.10'} + + d3-array@2.12.1: + resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-binarytree@1.0.2: + resolution: {integrity: sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force-3d@3.0.6: + resolution: {integrity: sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.0: + resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-octree@1.1.0: + resolution: {integrity: sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==} + + d3-path@1.0.9: + resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-sankey@0.12.3: + resolution: {integrity: sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@1.3.7: + resolution: {integrity: sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + dagre-d3-es@7.0.11: + resolution: {integrity: sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw==} + + data-view-buffer@1.0.2: + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} + + data-view-byte-length@1.0.2: + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} + + data-view-byte-offset@1.0.1: + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} + + dayjs@1.11.13: + resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} + + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + delaunator@5.0.1: + resolution: {integrity: sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==} + + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + doctrine@2.1.0: + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} + + dompurify@3.2.6: + resolution: {integrity: sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.179: + resolution: {integrity: sha512-UWKi/EbBopgfFsc5k61wFpV7WrnnSlSzW/e2XcBmS6qKYTivZlLtoll5/rdqRTxGglGHkmkW0j0pFNJG10EUIQ==} + + enhanced-resolve@5.18.2: + resolution: {integrity: sha512-6Jw4sE1maoRJo3q8MsSIn2onJFbLTOjY9hlx4DZXmOKvLRd1Ok2kXmAGXaafL2+ijsJZ1ClYbl/pmqr9+k4iUQ==} + engines: {node: '>=10.13.0'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + error-ex@1.3.2: + resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + + error-stack-parser@2.1.4: + resolution: {integrity: sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==} + + es-abstract@1.24.0: + resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} + engines: {node: '>= 0.4'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-iterator-helpers@1.2.1: + resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-shim-unscopables@1.1.0: + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} + + es-to-primitive@1.3.0: + resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==} + engines: {node: '>= 0.4'} + + esbuild-code-inspector-plugin@0.18.3: + resolution: {integrity: sha512-FaPt5eFMtW1oXMWqAcqfAJByNagP1V/R9dwDDLQO29JmryMF35+frskTqy+G53whmTaVi19+TCrFqhNbMZH5ZQ==} + + esbuild@0.25.5: + resolution: {integrity: sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-config-prettier@10.1.5: + resolution: {integrity: sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + + eslint-plugin-prettier@5.5.1: + resolution: {integrity: sha512-dobTkHT6XaEVOo8IO90Q4DOSxnm3Y151QxPJlM/vKC0bVy+d6cVWQZLlFiuZPP0wS6vZwSKeJgKkcS+KfMBlRw==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + '@types/eslint': '>=8.0.0' + eslint: '>=8.0.0' + eslint-config-prettier: '>= 7.0.0 <10.0.0 || >=10.1.0' + prettier: '>=3.0.0' + peerDependenciesMeta: + '@types/eslint': + optional: true + eslint-config-prettier: + optional: true + + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} + peerDependencies: + eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint@9.30.1: + resolution: {integrity: sha512-zmxXPNMOXmwm9E0yQLi5uqXHs7uq2UIiqEKo3Gq+3fwo1XrJ+hijAZImyF7hclW3E6oHz43Yk3RP8at6OTKflQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esquery@1.6.0: + resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + exsolve@1.0.7: + resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-diff@1.3.0: + resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} + + fast-glob@3.3.3: + resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} + engines: {node: '>=8.6.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-shallow-equal@1.0.0: + resolution: {integrity: sha512-HPtaa38cPgWvaCFmRNhlc6NG7pv6NUHqjPgVAkWGoB9mQMwYB27/K0CvOM5Czy+qpT3e8XJ6Q4aPAnzpNpzNaw==} + + fastest-stable-stringify@2.0.2: + resolution: {integrity: sha512-bijHueCGd0LqqNK9b5oCMHc0MluJAx0cwqASgbWMvkO01lCYgIhacVRLcaDz3QnyYIRNJRDwMb41VuT6pHJ91Q==} + + fastq@1.19.1: + resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} + + fdir@6.4.6: + resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.3.3: + resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + + float-tooltip@1.7.5: + resolution: {integrity: sha512-/kXzuDnnBqyyWyhDMH7+PfP8J/oXiAavGzcRxASOMRHFuReDtofizLLJsf7nnDLAfEaMW4pVWaXrAjtnglpEkg==} + engines: {node: '>=12'} + + for-each@0.3.5: + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} + + force-graph@1.50.0: + resolution: {integrity: sha512-IMUUv9DL6PQGLP8DdeGQZ96Fht8gXmuNM7e63CIinSxXk8bbAukR0Apj+vXVKcxa2S+DZTyrfHRk9T7FEvILTw==} + engines: {node: '>=12'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + function.prototype.name@1.1.8: + resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==} + engines: {node: '>= 0.4'} + + functions-have-names@1.2.3: + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} + + fuse.js@7.1.0: + resolution: {integrity: sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ==} + engines: {node: '>=10'} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-symbol-description@1.1.0: + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + globals@15.15.0: + resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} + engines: {node: '>=18'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + goober@2.1.16: + resolution: {integrity: sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g==} + peerDependencies: + csstype: ^3.0.10 + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphemer@1.4.0: + resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + + hachure-fill@0.5.2: + resolution: {integrity: sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==} + + has-bigints@1.1.0: + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-proto@1.2.0: + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + highlight.js@11.11.1: + resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} + engines: {node: '>=12.0.0'} + + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + + html-parse-stringify@3.0.1: + resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + + hyphenate-style-name@1.1.0: + resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==} + + i18next@25.3.0: + resolution: {integrity: sha512-ZSQIiNGfqSG6yoLHaCvrkPp16UejHI8PCDxFYaNG/1qxtmqNmqEg4JlWKlxkrUmrin2sEjsy+Mjy1TRozBhOgw==} + peerDependencies: + typescript: ^5 + peerDependenciesMeta: + typescript: + optional: true + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + index-array-by@1.4.2: + resolution: {integrity: sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==} + engines: {node: '>=12'} + + inline-style-prefixer@7.0.1: + resolution: {integrity: sha512-lhYo5qNTQp3EvSSp3sRvXMbVQTLrvGV6DycRMJ5dm2BLMiJ30wpXKdDdgX+GmJZ5uQMucwRKHamXSst3Sj/Giw==} + + internal-slot@1.1.0: + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} + + internmap@1.0.1: + resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + is-array-buffer@3.0.5: + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-async-function@2.1.1: + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} + + is-bigint@1.1.0: + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} + + is-boolean-object@1.2.2: + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} + + is-callable@1.2.7: + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-data-view@1.0.2: + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} + + is-date-object@1.1.0: + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-finalizationregistry@1.1.1: + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} + + is-generator-function@1.1.0: + resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==} + engines: {node: '>= 0.4'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-map@2.0.3: + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} + + is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + + is-number-object@1.1.1: + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + is-regex@1.2.1: + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} + + is-set@2.0.3: + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} + + is-shared-array-buffer@1.0.4: + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} + + is-string@1.1.1: + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} + + is-symbol@1.1.1: + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} + + is-typed-array@1.1.15: + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} + + is-weakmap@2.0.2: + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} + + is-weakref@1.1.1: + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} + + is-weakset@2.0.4: + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} + + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} + peerDependencies: + ws: '*' + + iterator.prototype@1.1.5: + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} + + javascript-natural-sort@0.7.1: + resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==} + + jerrypick@1.1.2: + resolution: {integrity: sha512-YKnxXEekXKzhpf7CLYA0A+oDP8V0OhICNCr5lv96FvSsDEmrb0GKM776JgQvHTMjr7DTTPEVv/1Ciaw0uEWzBA==} + engines: {node: '>=12'} + + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} + hasBin: true + + js-base64@3.7.7: + resolution: {integrity: sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==} + + js-cookie@2.2.1: + resolution: {integrity: sha512-HvdH2LzI/EAZcUwA8+0nKNtWHqS+ZmijLA30RwZA0bo7ToCckjK5MkGhjED9KoRcXO6BaGI3I9UIzSA1FKFPOQ==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsx-ast-utils@3.3.5: + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} + + kapsule@1.16.3: + resolution: {integrity: sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==} + engines: {node: '>=12'} + + katex@0.16.22: + resolution: {integrity: sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + khroma@2.1.0: + resolution: {integrity: sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + langium@3.3.1: + resolution: {integrity: sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==} + engines: {node: '>=16.0.0'} + + launch-ide@1.0.1: + resolution: {integrity: sha512-U7qBxSNk774PxWq4XbmRe0ThiIstPoa4sMH/OGSYxrFVvg8x3biXcF1fsH6wasDpEmEXMdINUrQhBdwsSgKyMg==} + + layout-base@1.0.2: + resolution: {integrity: sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==} + + layout-base@2.0.1: + resolution: {integrity: sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==} + + leaflet@1.9.4: + resolution: {integrity: sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-darwin-arm64@1.30.1: + resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.1: + resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.1: + resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.1: + resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.1: + resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.1: + resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.1: + resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.1: + resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.1: + resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.1: + resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.1: + resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + engines: {node: '>= 12.0.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + local-pkg@1.1.1: + resolution: {integrity: sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==} + engines: {node: '>=14'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash-es@4.17.21: + resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + lodash@4.17.21: + resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@0.486.0: + resolution: {integrity: sha512-xWop/wMsC1ikiEVLZrxXjPKw4vU/eAip33G2mZHgbWnr4Nr5Rt4Vx4s/q1D3B/rQVbxjOuqASkEZcUxDEKzecw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + marked@15.0.12: + resolution: {integrity: sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==} + engines: {node: '>= 18'} + hasBin: true + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdn-data@2.0.14: + resolution: {integrity: sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==} + + merge2@1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + + mermaid@11.8.0: + resolution: {integrity: sha512-uAZUwnBiqREZcUrFw3G5iQ5Pj3hTYUP95EZc3ec/nGBzHddJZydzYGE09tGZDBS1VoSoDn0symZ85FmypSTo5g==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + minimatch@3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + + minimatch@9.0.5: + resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.0.2: + resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} + engines: {node: '>= 18'} + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + + mlly@1.7.4: + resolution: {integrity: sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw==} + + mobx-react-lite@4.1.0: + resolution: {integrity: sha512-QEP10dpHHBeQNv1pks3WnHRCem2Zp636lq54M2nKO2Sarr13pL4u6diQXf65yzXUn0mkk18SyIDCm9UOJYTi1w==} + peerDependencies: + mobx: ^6.9.0 + react: ^16.8.0 || ^17 || ^18 || ^19 + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + + mobx@6.13.7: + resolution: {integrity: sha512-aChaVU/DO5aRPmk1GX8L+whocagUUpBQqoPtJk+cm7UOXUk87J4PeWCh6nNmTTIfEhiR9DI/+FnA8dln/hTK7g==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nano-css@5.6.2: + resolution: {integrity: sha512-+6bHaC8dSDGALM1HJjOHVXpuastdu2xFoZlC77Jh4cg+33Zcgm+Gxd+1xsnpZK14eyHObSp82+ll5y3SX75liw==} + peerDependencies: + react: '*' + react-dom: '*' + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + nice-grpc-common@2.0.2: + resolution: {integrity: sha512-7RNWbls5kAL1QVUOXvBsv1uO0wPQK3lHv+cY1gwkTzirnG1Nop4cBJZubpgziNbaVc/bl9QJcyvsf/NQxa3rjQ==} + + nice-grpc-web@3.3.7: + resolution: {integrity: sha512-9RyOGxmPm5rCLizn2uNFSSnlBPU/2n1i8inaNSAT52QAcOITKvCTFmOUnU+1CAJrKlxiGV64KFfcxJG77JDV5w==} + + node-releases@2.0.19: + resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + object.assign@4.1.7: + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} + + object.entries@1.1.9: + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} + + object.fromentries@2.0.8: + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} + + object.values@1.2.1: + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + own-keys@1.0.1: + resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==} + engines: {node: '>= 0.4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + package-manager-detector@1.3.0: + resolution: {integrity: sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ==} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + path-data-parser@0.1.0: + resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.2.0: + resolution: {integrity: sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ==} + + points-on-curve@0.2.0: + resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} + + points-on-path@0.2.1: + resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + + portfinder@1.0.37: + resolution: {integrity: sha512-yuGIEjDAYnnOex9ddMnKZEMFE0CcGo6zbfzDklkmT1m5z734ss6JMzN9rNB3+RR7iS+F10D4/BVIaXOyh8PQKw==} + engines: {node: '>= 10.12'} + + possible-typed-array-names@1.1.0: + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + preact@10.26.9: + resolution: {integrity: sha512-SSjF9vcnF27mJK1XyFMNJzFd5u3pQiATFqoaDy03XuN00u4ziveVVEGt5RKJrDR8MHE/wJo9Nnad56RLzS2RMA==} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier-linter-helpers@1.0.0: + resolution: {integrity: sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==} + engines: {node: '>=6.0.0'} + + prettier@3.6.2: + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} + hasBin: true + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + quansync@0.2.10: + resolution: {integrity: sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A==} + + queue-microtask@1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-force-graph-2d@1.28.0: + resolution: {integrity: sha512-NYA8GLxJnoZyLWjob8xea38B1cZqSGdcA8lDpvTc1hcJrpzFyBEHkeJ4xtFoJp66tsM4PAlj5af4HWnU0OQ3Sg==} + engines: {node: '>=12'} + peerDependencies: + react: '*' + + react-hot-toast@2.5.2: + resolution: {integrity: sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw==} + engines: {node: '>=10'} + peerDependencies: + react: '>=16' + react-dom: '>=16' + + react-i18next@15.5.3: + resolution: {integrity: sha512-ypYmOKOnjqPEJZO4m1BI0kS8kWqkBNsKYyhVUfij0gvjy9xJNoG/VcGkxq5dRlVwzmrmY1BQMAmpbbUBLwC4Kw==} + peerDependencies: + i18next: '>= 23.2.3' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + typescript: ^5 + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + typescript: + optional: true + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-kapsule@2.5.7: + resolution: {integrity: sha512-kifAF4ZPD77qZKc4CKLmozq6GY1sBzPEJTIJb0wWFK6HsePJatK3jXplZn2eeAt3x67CDozgi7/rO8fNQ/AL7A==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.13.1' + + react-leaflet@4.2.1: + resolution: {integrity: sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==} + peerDependencies: + leaflet: ^1.9.0 + react: ^18.0.0 + react-dom: ^18.0.0 + + react-refresh@0.17.0: + resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} + engines: {node: '>=0.10.0'} + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.1: + resolution: {integrity: sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-router-dom@7.6.3: + resolution: {integrity: sha512-DiWJm9qdUAmiJrVWaeJdu4TKu13+iB/8IEi0EW/XgaHCjW/vWGrwzup0GVvaMteuZjKnh5bEvJP/K0MDnzawHw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.6.3: + resolution: {integrity: sha512-zf45LZp5skDC6I3jDLXQUu0u26jtuP4lEGbc7BbdyxenBN1vJSTA18czM2D+h5qyMBuMrD+9uB+mU37HIoKGRA==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react-simple-pull-to-refresh@1.3.3: + resolution: {integrity: sha512-6qXsa5RtNVmKJhLWvDLIX8UK51HFtCEGjdqQGf+M1Qjrcc4qH4fki97sgVpGEFBRwbY7DiVDA5N5p97kF16DTw==} + peerDependencies: + react: ^16.10.2 || ^17.0.0 || ^18.0.0 + react-dom: ^16.10.2 || ^17.0.0 || ^18.0.0 + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-universal-interface@0.6.2: + resolution: {integrity: sha512-dg8yXdcQmvgR13RIlZbTRQOoUrDciFVoSBZILwjE2LFISxZZ8loVJKAkuzswl5js8BHda79bIb2b84ehU8IjXw==} + peerDependencies: + react: '*' + tslib: '*' + + react-use@17.6.0: + resolution: {integrity: sha512-OmedEScUMKFfzn1Ir8dBxiLLSOzhKe/dPZwVxcujweSj45aNM7BEGPb9BEVIgVEqEXx6f3/TsXzwIktNgUR02g==} + peerDependencies: + react: '*' + react-dom: '*' + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + reflect.getprototypeof@1.0.10: + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} + + regexp.prototype.flags@1.5.4: + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} + + resize-observer-polyfill@1.5.1: + resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve@1.22.10: + resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} + engines: {node: '>= 0.4'} + hasBin: true + + resolve@2.0.0-next.5: + resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} + hasBin: true + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + robust-predicates@3.0.2: + resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==} + + rollup@4.44.1: + resolution: {integrity: sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + roughjs@4.6.6: + resolution: {integrity: sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==} + + rtl-css-js@1.16.1: + resolution: {integrity: sha512-lRQgou1mu19e+Ya0LsTvKrVJ5TYUbqCVPAiImX3UfLTenarvPUl1QFdvu5Z3PYmHT9RCcwIfbjRQBntExyj3Zg==} + + run-parallel@1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + safe-array-concat@1.1.3: + resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} + engines: {node: '>=0.4'} + + safe-push-apply@1.0.0: + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} + + safe-regex-test@1.1.0: + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + screenfull@5.2.0: + resolution: {integrity: sha512-9BakfsO2aUQN2K9Fdbj87RJIEZ82Q9IGim7FqM5OsebfoFC6ZHXgDq/KvniuLTPdeM8wY2o6Dj3WQ7KeQCj3cA==} + engines: {node: '>=0.10.0'} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@2.7.1: + resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==} + + set-function-length@1.2.2: + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} + + set-function-name@2.0.2: + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} + + set-harmonic-interval@1.0.1: + resolution: {integrity: sha512-AhICkFV84tBP1aWqPwLZqFvAwqEoVA9kxNMniGEUvzOlm4vLmOFLiTT3UZ6bziJTy4bOVpzWGTfSCbmaayGx8g==} + engines: {node: '>=6.9'} + + set-proto@1.0.0: + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.5.6: + resolution: {integrity: sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==} + engines: {node: '>=0.10.0'} + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + stack-generator@2.0.10: + resolution: {integrity: sha512-mwnua/hkqM6pF4k8SnmZ2zfETsRUpWXREfA/goT8SLCV4iOFa4bzOX2nDipWAZFPTjLvQB82f5yaodMVhK0yJQ==} + + stackframe@1.3.4: + resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} + + stacktrace-gps@3.1.2: + resolution: {integrity: sha512-GcUgbO4Jsqqg6RxfyTHFiPxdPqF+3LFmQhm7MgCuYQOYuWyqxo5pwRPz5d/u6/WYJdEnWfK4r+jGbyD8TSggXQ==} + + stacktrace-js@2.0.2: + resolution: {integrity: sha512-Je5vBeY4S1r/RnLydLl0TBTi3F2qdfWmYsGvtfZgEI+SCprPppaIhQf5nGcal4gI4cGpCV/duLcAzT1np6sQqg==} + + stop-iteration-iterator@1.1.0: + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} + + string.prototype.matchall@4.0.12: + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} + + string.prototype.repeat@1.0.0: + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} + + string.prototype.trim@1.2.10: + resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==} + engines: {node: '>= 0.4'} + + string.prototype.trimend@1.0.9: + resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==} + engines: {node: '>= 0.4'} + + string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + + stylis@4.3.6: + resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + synckit@0.11.8: + resolution: {integrity: sha512-+XZ+r1XGIJGeQk3VvXhT6xx/VpbHsRzsTkGgF6E5RX9TTXD0118l87puaEBZ566FhqblC6U0d4XnubznJDm30A==} + engines: {node: ^14.18.0 || >=16.0.0} + + tailwind-merge@3.3.1: + resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} + + tailwindcss@4.1.11: + resolution: {integrity: sha512-2E9TBm6MDD/xKYe+dvJZAmg3yxIEDNRc0jwlNyDg/4Fil2QcSLjFKGVff0lAf1jjeaArlG/M75Ey/EYr/OJtBA==} + + tapable@2.2.2: + resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==} + engines: {node: '>=6'} + + tar@7.4.3: + resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} + engines: {node: '>=18'} + + terser@5.43.1: + resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==} + engines: {node: '>=10'} + hasBin: true + + textarea-caret@3.1.0: + resolution: {integrity: sha512-cXAvzO9pP5CGa6NKx0WYHl+8CHKZs8byMkt3PCJBCmq2a34YA9pO1NrQET5pzeqnBjBdToF5No4rrmkDUgQC2Q==} + + throttle-debounce@3.0.1: + resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==} + engines: {node: '>=10'} + + tinycolor2@1.6.0: + resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + + tinyexec@1.0.1: + resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} + + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + engines: {node: '>=12.0.0'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toggle-selection@1.0.6: + resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} + + ts-api-utils@2.1.0: + resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + ts-dedent@2.2.0: + resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==} + engines: {node: '>=6.10'} + + ts-easing@0.2.0: + resolution: {integrity: sha512-Z86EW+fFFh/IFB1fqQ3/+7Zpf9t2ebOAxNI/V6Wo7r5gqiqtxmgTlQ1qbqQcjLKYeSHPTsEmvlJUDg/EuL0uHQ==} + + ts-error@1.0.6: + resolution: {integrity: sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tw-animate-css@1.3.4: + resolution: {integrity: sha512-dd1Ht6/YQHcNbq0znIT6dG8uhO7Ce+VIIhZUhjsryXsMPJQz3bZg7Q2eNzLwipb25bRZslGb2myio5mScd1TFg==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + + typed-array-byte-length@1.0.3: + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} + + typed-array-byte-offset@1.0.4: + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} + + typed-array-length@1.0.7: + resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==} + engines: {node: '>= 0.4'} + + typescript-eslint@8.35.1: + resolution: {integrity: sha512-xslJjFzhOmHYQzSB/QTeASAHbjmxOGEP6Coh93TXmUBFQoJ1VU35UHIDmG06Jd6taf3wqqC1ntBnCMeymy5Ovw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <5.9.0' + + typescript@5.8.3: + resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==} + engines: {node: '>=14.17'} + hasBin: true + + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + + unbox-primitive@1.1.0: + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} + + undici-types@7.8.0: + resolution: {integrity: sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==} + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.5.0: + resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + vite-code-inspector-plugin@0.18.3: + resolution: {integrity: sha512-178H73vbDUHE+JpvfAfioUHlUr7qXCYIEa2YNXtzenFQGOjtae59P1jjcxGfa6pPHEnOoaitb13K+0qxwhi/WA==} + + vite@7.0.1: + resolution: {integrity: sha512-BiKOQoW5HGR30E6JDeNsati6HnSPMVEKbkIWbCiol+xKeu3g5owrjy7kbk/QEMuzCV87dSUTvycYKmlcfGKq3Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + void-elements@3.1.0: + resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} + engines: {node: '>=0.10.0'} + + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-textdocument@1.0.12: + resolution: {integrity: sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + vscode-languageserver@9.0.1: + resolution: {integrity: sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==} + hasBin: true + + vscode-uri@3.0.8: + resolution: {integrity: sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==} + + webpack-code-inspector-plugin@0.18.3: + resolution: {integrity: sha512-3782rsJhBnRiw0IpR6EqnyGDQoiSq0CcGeLJ52rZXlszYCe8igXtcujq7OhI0byaivWQ1LW7sXKyMEoVpBhq0w==} + + which-boxed-primitive@1.1.1: + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} + + which-builtin-type@1.2.1: + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} + + which-collection@1.0.2: + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} + + which-typed-array@1.1.19: + resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==} + engines: {node: '>= 0.4'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yaml@1.10.2: + resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} + engines: {node: '>= 6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + + '@antfu/install-pkg@1.1.0': + dependencies: + package-manager-detector: 1.3.0 + tinyexec: 1.0.1 + + '@antfu/utils@8.1.1': {} + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.0': {} + + '@babel/core@7.28.0': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.0 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.27.3(@babel/core@7.28.0) + '@babel/helpers': 7.27.6 + '@babel/parser': 7.28.0 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.0 + convert-source-map: 2.0.0 + debug: 4.4.1 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.0': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.0 + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.25.1 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.27.3(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.27.6': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.0 + + '@babel/parser@7.28.0': + dependencies: + '@babel/types': 7.28.0 + + '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/runtime@7.27.6': {} + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.0 + '@babel/types': 7.28.0 + + '@babel/traverse@7.28.0': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.0 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.0 + '@babel/template': 7.27.2 + '@babel/types': 7.28.0 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@braintree/sanitize-url@7.1.1': {} + + '@bufbuild/protobuf@2.6.0': {} + + '@chevrotain/cst-dts-gen@11.0.3': + dependencies: + '@chevrotain/gast': 11.0.3 + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + + '@chevrotain/gast@11.0.3': + dependencies: + '@chevrotain/types': 11.0.3 + lodash-es: 4.17.21 + + '@chevrotain/regexp-to-ast@11.0.3': {} + + '@chevrotain/types@11.0.3': {} + + '@chevrotain/utils@11.0.3': {} + + '@dnd-kit/accessibility@3.1.1(react@18.3.1)': + dependencies: + react: 18.3.1 + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.8.1 + + '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@18.3.1)': + dependencies: + react: 18.3.1 + tslib: 2.8.1 + + '@emotion/babel-plugin@11.13.5': + dependencies: + '@babel/helper-module-imports': 7.27.1 + '@babel/runtime': 7.27.6 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/serialize': 1.3.3 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + transitivePeerDependencies: + - supports-color + + '@emotion/cache@11.14.0': + dependencies: + '@emotion/memoize': 0.9.0 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + stylis: 4.2.0 + + '@emotion/hash@0.9.2': {} + + '@emotion/is-prop-valid@1.3.1': + dependencies: + '@emotion/memoize': 0.9.0 + + '@emotion/memoize@0.9.0': {} + + '@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.27.6 + '@emotion/babel-plugin': 11.13.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@18.3.1) + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + hoist-non-react-statics: 3.3.2 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + transitivePeerDependencies: + - supports-color + + '@emotion/serialize@1.3.3': + dependencies: + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 + '@emotion/utils': 1.4.2 + csstype: 3.1.3 + + '@emotion/sheet@1.4.0': {} + + '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@18.3.23)(react@18.3.1))(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@babel/runtime': 7.27.6 + '@emotion/babel-plugin': 11.13.5 + '@emotion/is-prop-valid': 1.3.1 + '@emotion/react': 11.14.0(@types/react@18.3.23)(react@18.3.1) + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@18.3.1) + '@emotion/utils': 1.4.2 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + transitivePeerDependencies: + - supports-color + + '@emotion/unitless@0.10.0': {} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@18.3.1)': + dependencies: + react: 18.3.1 + + '@emotion/utils@1.4.2': {} + + '@emotion/weak-memoize@0.4.0': {} + + '@esbuild/aix-ppc64@0.25.5': + optional: true + + '@esbuild/android-arm64@0.25.5': + optional: true + + '@esbuild/android-arm@0.25.5': + optional: true + + '@esbuild/android-x64@0.25.5': + optional: true + + '@esbuild/darwin-arm64@0.25.5': + optional: true + + '@esbuild/darwin-x64@0.25.5': + optional: true + + '@esbuild/freebsd-arm64@0.25.5': + optional: true + + '@esbuild/freebsd-x64@0.25.5': + optional: true + + '@esbuild/linux-arm64@0.25.5': + optional: true + + '@esbuild/linux-arm@0.25.5': + optional: true + + '@esbuild/linux-ia32@0.25.5': + optional: true + + '@esbuild/linux-loong64@0.25.5': + optional: true + + '@esbuild/linux-mips64el@0.25.5': + optional: true + + '@esbuild/linux-ppc64@0.25.5': + optional: true + + '@esbuild/linux-riscv64@0.25.5': + optional: true + + '@esbuild/linux-s390x@0.25.5': + optional: true + + '@esbuild/linux-x64@0.25.5': + optional: true + + '@esbuild/netbsd-arm64@0.25.5': + optional: true + + '@esbuild/netbsd-x64@0.25.5': + optional: true + + '@esbuild/openbsd-arm64@0.25.5': + optional: true + + '@esbuild/openbsd-x64@0.25.5': + optional: true + + '@esbuild/sunos-x64@0.25.5': + optional: true + + '@esbuild/win32-arm64@0.25.5': + optional: true + + '@esbuild/win32-ia32@0.25.5': + optional: true + + '@esbuild/win32-x64@0.25.5': + optional: true + + '@eslint-community/eslint-utils@4.7.0(eslint@9.30.1(jiti@2.4.2))': + dependencies: + eslint: 9.30.1(jiti@2.4.2) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.1': {} + + '@eslint/config-array@0.21.0': + dependencies: + '@eslint/object-schema': 2.1.6 + debug: 4.4.1 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.3.0': {} + + '@eslint/core@0.14.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/core@0.15.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.1': + dependencies: + ajv: 6.12.6 + debug: 4.4.1 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.30.1': {} + + '@eslint/object-schema@2.1.6': {} + + '@eslint/plugin-kit@0.3.3': + dependencies: + '@eslint/core': 0.15.1 + levn: 0.4.1 + + '@floating-ui/core@1.7.2': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.2': + dependencies: + '@floating-ui/core': 1.7.2 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/react-dom@2.1.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/dom': 1.7.2 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@floating-ui/utils@0.2.10': {} + + '@github/relative-time-element@4.4.8': {} + + '@humanfs/core@0.19.1': {} + + '@humanfs/node@0.16.6': + dependencies: + '@humanfs/core': 0.19.1 + '@humanwhocodes/retry': 0.3.1 + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.3.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@iconify/types@2.0.0': {} + + '@iconify/utils@2.3.0': + dependencies: + '@antfu/install-pkg': 1.1.0 + '@antfu/utils': 8.1.1 + '@iconify/types': 2.0.0 + debug: 4.4.1 + globals: 15.15.0 + kolorist: 1.8.0 + local-pkg: 1.1.1 + mlly: 1.7.4 + transitivePeerDependencies: + - supports-color + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + + '@jridgewell/gen-mapping@0.3.12': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + '@jridgewell/trace-mapping': 0.3.29 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/source-map@0.3.10': + dependencies: + '@jridgewell/gen-mapping': 0.3.12 + '@jridgewell/trace-mapping': 0.3.29 + + '@jridgewell/sourcemap-codec@1.5.4': {} + + '@jridgewell/trace-mapping@0.3.29': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.4 + + '@matejmazur/react-katex@3.1.3(katex@0.16.22)(react@18.3.1)': + dependencies: + katex: 0.16.22 + react: 18.3.1 + + '@mermaid-js/parser@0.6.0': + dependencies: + langium: 3.3.1 + + '@nodelib/fs.scandir@2.1.5': + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + + '@nodelib/fs.stat@2.0.5': {} + + '@nodelib/fs.walk@1.2.8': + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.19.1 + + '@pkgr/core@0.2.7': {} + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.2': {} + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-checkbox@1.3.2(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.23)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-context@1.1.2(@types/react@18.3.23)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-dialog@1.1.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.23)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-direction@1.1.1(@types/react@18.3.23)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-dismissable-layer@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-dropdown-menu@2.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-menu': 2.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-focus-guards@1.1.2(@types/react@18.3.23)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-id@1.1.1(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-label@2.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-menu@2.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.23)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-popover@1.1.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.23)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-popper@1.2.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/rect': 1.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-presence@1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-radio-group@1.3.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-roving-focus@1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-select@2.2.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.23)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-separator@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-slot@1.2.3(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-switch@1.2.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-tooltip@1.2.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.10(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-popper': 1.2.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.23)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.23)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-use-previous@1.1.1(@types/react@18.3.23)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-use-rect@1.1.1(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-use-size@1.1.1(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/rect@1.1.1': {} + + '@react-leaflet/core@2.1.0(leaflet@1.9.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + leaflet: 1.9.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@rolldown/pluginutils@1.0.0-beta.19': {} + + '@rollup/rollup-android-arm-eabi@4.44.1': + optional: true + + '@rollup/rollup-android-arm64@4.44.1': + optional: true + + '@rollup/rollup-darwin-arm64@4.44.1': + optional: true + + '@rollup/rollup-darwin-x64@4.44.1': + optional: true + + '@rollup/rollup-freebsd-arm64@4.44.1': + optional: true + + '@rollup/rollup-freebsd-x64@4.44.1': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.44.1': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.44.1': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.44.1': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.44.1': + optional: true + + '@rollup/rollup-linux-loongarch64-gnu@4.44.1': + optional: true + + '@rollup/rollup-linux-powerpc64le-gnu@4.44.1': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.44.1': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.44.1': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.44.1': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.44.1': + optional: true + + '@rollup/rollup-linux-x64-musl@4.44.1': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.44.1': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.44.1': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.44.1': + optional: true + + '@tailwindcss/node@4.1.11': + dependencies: + '@ampproject/remapping': 2.3.0 + enhanced-resolve: 5.18.2 + jiti: 2.4.2 + lightningcss: 1.30.1 + magic-string: 0.30.17 + source-map-js: 1.2.1 + tailwindcss: 4.1.11 + + '@tailwindcss/oxide-android-arm64@4.1.11': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.11': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.11': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.11': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.11': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.11': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.11': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.11': + optional: true + + '@tailwindcss/oxide@4.1.11': + dependencies: + detect-libc: 2.0.4 + tar: 7.4.3 + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.11 + '@tailwindcss/oxide-darwin-arm64': 4.1.11 + '@tailwindcss/oxide-darwin-x64': 4.1.11 + '@tailwindcss/oxide-freebsd-x64': 4.1.11 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.11 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.11 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.11 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.11 + '@tailwindcss/oxide-linux-x64-musl': 4.1.11 + '@tailwindcss/oxide-wasm32-wasi': 4.1.11 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.11 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.11 + + '@tailwindcss/vite@4.1.11(vite@7.0.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1))': + dependencies: + '@tailwindcss/node': 4.1.11 + '@tailwindcss/oxide': 4.1.11 + tailwindcss: 4.1.11 + vite: 7.0.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1) + + '@trivago/prettier-plugin-sort-imports@5.2.2(prettier@3.6.2)': + dependencies: + '@babel/generator': 7.28.0 + '@babel/parser': 7.28.0 + '@babel/traverse': 7.28.0 + '@babel/types': 7.28.0 + javascript-natural-sort: 0.7.1 + lodash: 4.17.21 + prettier: 3.6.2 + transitivePeerDependencies: + - supports-color + + '@tweenjs/tween.js@25.0.0': {} + + '@types/babel__core@7.20.5': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.0 + '@types/babel__generator': 7.27.0 + '@types/babel__template': 7.4.4 + '@types/babel__traverse': 7.20.7 + + '@types/babel__generator@7.27.0': + dependencies: + '@babel/types': 7.28.0 + + '@types/babel__template@7.4.4': + dependencies: + '@babel/parser': 7.28.0 + '@babel/types': 7.28.0 + + '@types/babel__traverse@7.20.7': + dependencies: + '@babel/types': 7.28.0 + + '@types/d3-array@3.2.1': {} + + '@types/d3-axis@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-brush@3.0.6': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-chord@3.0.6': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-contour@3.0.6': + dependencies: + '@types/d3-array': 3.2.1 + '@types/geojson': 7946.0.16 + + '@types/d3-delaunay@6.0.4': {} + + '@types/d3-dispatch@3.0.6': {} + + '@types/d3-drag@3.0.7': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-dsv@3.0.7': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-fetch@3.0.7': + dependencies: + '@types/d3-dsv': 3.0.7 + + '@types/d3-force@3.0.10': {} + + '@types/d3-format@3.0.4': {} + + '@types/d3-geo@3.1.0': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/d3-hierarchy@3.1.7': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-polygon@3.0.2': {} + + '@types/d3-quadtree@3.0.6': {} + + '@types/d3-random@3.0.3': {} + + '@types/d3-scale-chromatic@3.1.0': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-selection@3.0.11': {} + + '@types/d3-shape@3.1.7': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time-format@4.0.3': {} + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/d3-transition@3.0.9': + dependencies: + '@types/d3-selection': 3.0.11 + + '@types/d3-zoom@3.0.8': + dependencies: + '@types/d3-interpolate': 3.0.4 + '@types/d3-selection': 3.0.11 + + '@types/d3@7.4.3': + dependencies: + '@types/d3-array': 3.2.1 + '@types/d3-axis': 3.0.6 + '@types/d3-brush': 3.0.6 + '@types/d3-chord': 3.0.6 + '@types/d3-color': 3.1.3 + '@types/d3-contour': 3.0.6 + '@types/d3-delaunay': 6.0.4 + '@types/d3-dispatch': 3.0.6 + '@types/d3-drag': 3.0.7 + '@types/d3-dsv': 3.0.7 + '@types/d3-ease': 3.0.2 + '@types/d3-fetch': 3.0.7 + '@types/d3-force': 3.0.10 + '@types/d3-format': 3.0.4 + '@types/d3-geo': 3.1.0 + '@types/d3-hierarchy': 3.1.7 + '@types/d3-interpolate': 3.0.4 + '@types/d3-path': 3.1.1 + '@types/d3-polygon': 3.0.2 + '@types/d3-quadtree': 3.0.6 + '@types/d3-random': 3.0.3 + '@types/d3-scale': 4.0.9 + '@types/d3-scale-chromatic': 3.1.0 + '@types/d3-selection': 3.0.11 + '@types/d3-shape': 3.1.7 + '@types/d3-time': 3.0.4 + '@types/d3-time-format': 4.0.3 + '@types/d3-timer': 3.0.2 + '@types/d3-transition': 3.0.9 + '@types/d3-zoom': 3.0.8 + + '@types/estree@1.0.8': {} + + '@types/geojson@7946.0.16': {} + + '@types/js-cookie@2.2.7': {} + + '@types/json-schema@7.0.15': {} + + '@types/katex@0.16.7': {} + + '@types/leaflet@1.9.19': + dependencies: + '@types/geojson': 7946.0.16 + + '@types/lodash-es@4.17.12': + dependencies: + '@types/lodash': 4.17.20 + + '@types/lodash@4.17.20': {} + + '@types/node@24.0.10': + dependencies: + undici-types: 7.8.0 + + '@types/parse-json@4.0.2': {} + + '@types/prop-types@15.7.15': {} + + '@types/qs@6.14.0': {} + + '@types/react-dom@18.3.7(@types/react@18.3.23)': + dependencies: + '@types/react': 18.3.23 + + '@types/react@18.3.23': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.1.3 + + '@types/textarea-caret@3.0.4': {} + + '@types/trusted-types@2.0.7': + optional: true + + '@types/uuid@10.0.0': {} + + '@typescript-eslint/eslint-plugin@8.35.1(@typescript-eslint/parser@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@eslint-community/regexpp': 4.12.1 + '@typescript-eslint/parser': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/scope-manager': 8.35.1 + '@typescript-eslint/type-utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.35.1 + eslint: 9.30.1(jiti@2.4.2) + graphemer: 1.4.0 + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.35.1 + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) + '@typescript-eslint/visitor-keys': 8.35.1 + debug: 4.4.1 + eslint: 9.30.1(jiti@2.4.2) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.35.1(typescript@5.8.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) + '@typescript-eslint/types': 8.35.1 + debug: 4.4.1 + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.35.1': + dependencies: + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/visitor-keys': 8.35.1 + + '@typescript-eslint/tsconfig-utils@8.35.1(typescript@5.8.3)': + dependencies: + typescript: 5.8.3 + + '@typescript-eslint/type-utils@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) + '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + debug: 4.4.1 + eslint: 9.30.1(jiti@2.4.2) + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.35.1': {} + + '@typescript-eslint/typescript-estree@8.35.1(typescript@5.8.3)': + dependencies: + '@typescript-eslint/project-service': 8.35.1(typescript@5.8.3) + '@typescript-eslint/tsconfig-utils': 8.35.1(typescript@5.8.3) + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/visitor-keys': 8.35.1 + debug: 4.4.1 + fast-glob: 3.3.3 + is-glob: 4.0.3 + minimatch: 9.0.5 + semver: 7.7.2 + ts-api-utils: 2.1.0(typescript@5.8.3) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3)': + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@2.4.2)) + '@typescript-eslint/scope-manager': 8.35.1 + '@typescript-eslint/types': 8.35.1 + '@typescript-eslint/typescript-estree': 8.35.1(typescript@5.8.3) + eslint: 9.30.1(jiti@2.4.2) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.35.1': + dependencies: + '@typescript-eslint/types': 8.35.1 + eslint-visitor-keys: 4.2.1 + + '@vitejs/plugin-react@4.6.0(vite@7.0.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1))': + dependencies: + '@babel/core': 7.28.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.0) + '@rolldown/pluginutils': 1.0.0-beta.19 + '@types/babel__core': 7.20.5 + react-refresh: 0.17.0 + vite: 7.0.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1) + transitivePeerDependencies: + - supports-color + + '@vue/compiler-core@3.5.17': + dependencies: + '@babel/parser': 7.28.0 + '@vue/shared': 3.5.17 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.17': + dependencies: + '@vue/compiler-core': 3.5.17 + '@vue/shared': 3.5.17 + + '@vue/shared@3.5.17': {} + + '@xobotyi/scrollbar-width@1.9.5': {} + + abort-controller-x@0.4.3: {} + + accessor-fn@1.5.3: {} + + acorn-jsx@5.3.2(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + + acorn@8.15.0: {} + + ajv@6.12.6: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + argparse@2.0.1: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + array-buffer-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + is-array-buffer: 3.0.5 + + array-includes@3.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + is-string: 1.1.1 + math-intrinsics: 1.1.0 + + array.prototype.findlast@1.2.5: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-shim-unscopables: 1.1.0 + + array.prototype.flat@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 + + array.prototype.flatmap@1.3.3: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-shim-unscopables: 1.1.0 + + array.prototype.tosorted@1.1.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-shim-unscopables: 1.1.0 + + arraybuffer.prototype.slice@1.0.4: + dependencies: + array-buffer-byte-length: 1.0.2 + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + is-array-buffer: 3.0.5 + + async-function@1.0.0: {} + + async@3.2.6: {} + + available-typed-arrays@1.0.7: + dependencies: + possible-typed-array-names: 1.1.0 + + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.27.6 + cosmiconfig: 7.1.0 + resolve: 1.22.10 + + balanced-match@1.0.2: {} + + bezier-js@6.1.4: {} + + brace-expansion@1.1.12: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + browserslist@4.25.1: + dependencies: + caniuse-lite: 1.0.30001726 + electron-to-chromium: 1.5.179 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.25.1) + + buffer-from@1.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bind@1.0.8: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + get-intrinsic: 1.3.0 + set-function-length: 1.2.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001726: {} + + canvas-color-tracker@1.3.2: + dependencies: + tinycolor2: 1.6.0 + + chalk@4.1.1: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chevrotain-allstar@0.3.1(chevrotain@11.0.3): + dependencies: + chevrotain: 11.0.3 + lodash-es: 4.17.21 + + chevrotain@11.0.3: + dependencies: + '@chevrotain/cst-dts-gen': 11.0.3 + '@chevrotain/gast': 11.0.3 + '@chevrotain/regexp-to-ast': 11.0.3 + '@chevrotain/types': 11.0.3 + '@chevrotain/utils': 11.0.3 + lodash-es: 4.17.21 + + chownr@3.0.0: {} + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + clsx@2.1.1: {} + + cmdk@1.1.1(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-dialog': 1.1.14(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + code-inspector-core@0.18.3: + dependencies: + '@vue/compiler-dom': 3.5.17 + chalk: 4.1.1 + dotenv: 16.6.1 + launch-ide: 1.0.1 + portfinder: 1.0.37 + transitivePeerDependencies: + - supports-color + + code-inspector-plugin@0.18.3: + dependencies: + chalk: 4.1.1 + code-inspector-core: 0.18.3 + dotenv: 16.6.1 + esbuild-code-inspector-plugin: 0.18.3 + vite-code-inspector-plugin: 0.18.3 + webpack-code-inspector-plugin: 0.18.3 + transitivePeerDependencies: + - supports-color + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + commander@2.20.3: {} + + commander@7.2.0: {} + + commander@8.3.0: {} + + concat-map@0.0.1: {} + + confbox@0.1.8: {} + + confbox@0.2.2: {} + + convert-source-map@1.9.0: {} + + convert-source-map@2.0.0: {} + + cookie@1.0.2: {} + + copy-to-clipboard@3.3.3: + dependencies: + toggle-selection: 1.0.6 + + cose-base@1.0.3: + dependencies: + layout-base: 1.0.2 + + cose-base@2.2.0: + dependencies: + layout-base: 2.0.1 + + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.1 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.2 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-in-js-utils@3.1.0: + dependencies: + hyphenate-style-name: 1.1.0 + + css-tree@1.1.3: + dependencies: + mdn-data: 2.0.14 + source-map: 0.6.1 + + csstype@3.1.3: {} + + cytoscape-cose-bilkent@4.1.0(cytoscape@3.32.0): + dependencies: + cose-base: 1.0.3 + cytoscape: 3.32.0 + + cytoscape-fcose@2.2.0(cytoscape@3.32.0): + dependencies: + cose-base: 2.2.0 + cytoscape: 3.32.0 + + cytoscape@3.32.0: {} + + d3-array@2.12.1: + dependencies: + internmap: 1.0.1 + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-binarytree@1.0.2: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.0.1 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force-3d@3.0.6: + dependencies: + d3-binarytree: 1.0.2 + d3-dispatch: 3.0.1 + d3-octree: 1.1.0 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.0: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-octree@1.1.0: {} + + d3-path@1.0.9: {} + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-sankey@0.12.3: + dependencies: + d3-array: 2.12.1 + d3-shape: 1.3.7 + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.0 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@1.3.7: + dependencies: + d3-path: 1.0.9 + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.0 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + dagre-d3-es@7.0.11: + dependencies: + d3: 7.9.0 + lodash-es: 4.17.21 + + data-view-buffer@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-length@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + data-view-byte-offset@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-data-view: 1.0.2 + + dayjs@1.11.13: {} + + debug@4.4.1: + dependencies: + ms: 2.1.3 + + deep-is@0.1.4: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + delaunator@5.0.1: + dependencies: + robust-predicates: 3.0.2 + + detect-libc@2.0.4: {} + + detect-node-es@1.1.0: {} + + doctrine@2.1.0: + dependencies: + esutils: 2.0.3 + + dompurify@3.2.6: + optionalDependencies: + '@types/trusted-types': 2.0.7 + + dotenv@16.6.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.179: {} + + enhanced-resolve@5.18.2: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.2 + + entities@4.5.0: {} + + error-ex@1.3.2: + dependencies: + is-arrayish: 0.2.1 + + error-stack-parser@2.1.4: + dependencies: + stackframe: 1.3.4 + + es-abstract@1.24.0: + dependencies: + array-buffer-byte-length: 1.0.2 + arraybuffer.prototype.slice: 1.0.4 + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + data-view-buffer: 1.0.2 + data-view-byte-length: 1.0.2 + data-view-byte-offset: 1.0.1 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + es-set-tostringtag: 2.1.0 + es-to-primitive: 1.3.0 + function.prototype.name: 1.1.8 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + get-symbol-description: 1.1.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + internal-slot: 1.1.0 + is-array-buffer: 3.0.5 + is-callable: 1.2.7 + is-data-view: 1.0.2 + is-negative-zero: 2.0.3 + is-regex: 1.2.1 + is-set: 2.0.3 + is-shared-array-buffer: 1.0.4 + is-string: 1.1.1 + is-typed-array: 1.1.15 + is-weakref: 1.1.1 + math-intrinsics: 1.1.0 + object-inspect: 1.13.4 + object-keys: 1.1.1 + object.assign: 4.1.7 + own-keys: 1.0.1 + regexp.prototype.flags: 1.5.4 + safe-array-concat: 1.1.3 + safe-push-apply: 1.0.0 + safe-regex-test: 1.1.0 + set-proto: 1.0.0 + stop-iteration-iterator: 1.1.0 + string.prototype.trim: 1.2.10 + string.prototype.trimend: 1.0.9 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.3 + typed-array-byte-length: 1.0.3 + typed-array-byte-offset: 1.0.4 + typed-array-length: 1.0.7 + unbox-primitive: 1.1.0 + which-typed-array: 1.1.19 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-iterator-helpers@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-set-tostringtag: 2.1.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + globalthis: 1.0.4 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + has-proto: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + iterator.prototype: 1.1.5 + safe-array-concat: 1.1.3 + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es-shim-unscopables@1.1.0: + dependencies: + hasown: 2.0.2 + + es-to-primitive@1.3.0: + dependencies: + is-callable: 1.2.7 + is-date-object: 1.1.0 + is-symbol: 1.1.1 + + esbuild-code-inspector-plugin@0.18.3: + dependencies: + code-inspector-core: 0.18.3 + transitivePeerDependencies: + - supports-color + + esbuild@0.25.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.5 + '@esbuild/android-arm': 0.25.5 + '@esbuild/android-arm64': 0.25.5 + '@esbuild/android-x64': 0.25.5 + '@esbuild/darwin-arm64': 0.25.5 + '@esbuild/darwin-x64': 0.25.5 + '@esbuild/freebsd-arm64': 0.25.5 + '@esbuild/freebsd-x64': 0.25.5 + '@esbuild/linux-arm': 0.25.5 + '@esbuild/linux-arm64': 0.25.5 + '@esbuild/linux-ia32': 0.25.5 + '@esbuild/linux-loong64': 0.25.5 + '@esbuild/linux-mips64el': 0.25.5 + '@esbuild/linux-ppc64': 0.25.5 + '@esbuild/linux-riscv64': 0.25.5 + '@esbuild/linux-s390x': 0.25.5 + '@esbuild/linux-x64': 0.25.5 + '@esbuild/netbsd-arm64': 0.25.5 + '@esbuild/netbsd-x64': 0.25.5 + '@esbuild/openbsd-arm64': 0.25.5 + '@esbuild/openbsd-x64': 0.25.5 + '@esbuild/sunos-x64': 0.25.5 + '@esbuild/win32-arm64': 0.25.5 + '@esbuild/win32-ia32': 0.25.5 + '@esbuild/win32-x64': 0.25.5 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-config-prettier@10.1.5(eslint@9.30.1(jiti@2.4.2)): + dependencies: + eslint: 9.30.1(jiti@2.4.2) + + eslint-plugin-prettier@5.5.1(eslint-config-prettier@10.1.5(eslint@9.30.1(jiti@2.4.2)))(eslint@9.30.1(jiti@2.4.2))(prettier@3.6.2): + dependencies: + eslint: 9.30.1(jiti@2.4.2) + prettier: 3.6.2 + prettier-linter-helpers: 1.0.0 + synckit: 0.11.8 + optionalDependencies: + eslint-config-prettier: 10.1.5(eslint@9.30.1(jiti@2.4.2)) + + eslint-plugin-react@7.37.5(eslint@9.30.1(jiti@2.4.2)): + dependencies: + array-includes: 3.1.9 + array.prototype.findlast: 1.2.5 + array.prototype.flatmap: 1.3.3 + array.prototype.tosorted: 1.1.4 + doctrine: 2.1.0 + es-iterator-helpers: 1.2.1 + eslint: 9.30.1(jiti@2.4.2) + estraverse: 5.3.0 + hasown: 2.0.2 + jsx-ast-utils: 3.3.5 + minimatch: 3.1.2 + object.entries: 1.1.9 + object.fromentries: 2.0.8 + object.values: 1.2.1 + prop-types: 15.8.1 + resolve: 2.0.0-next.5 + semver: 6.3.1 + string.prototype.matchall: 4.0.12 + string.prototype.repeat: 1.0.0 + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint@9.30.1(jiti@2.4.2): + dependencies: + '@eslint-community/eslint-utils': 4.7.0(eslint@9.30.1(jiti@2.4.2)) + '@eslint-community/regexpp': 4.12.1 + '@eslint/config-array': 0.21.0 + '@eslint/config-helpers': 0.3.0 + '@eslint/core': 0.14.0 + '@eslint/eslintrc': 3.3.1 + '@eslint/js': 9.30.1 + '@eslint/plugin-kit': 0.3.3 + '@humanfs/node': 0.16.6 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + '@types/json-schema': 7.0.15 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.1 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.6.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.4.2 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.15.0 + acorn-jsx: 5.3.2(acorn@8.15.0) + eslint-visitor-keys: 4.2.1 + + esquery@1.6.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@2.0.2: {} + + esutils@2.0.3: {} + + exsolve@1.0.7: {} + + fast-deep-equal@3.1.3: {} + + fast-diff@1.3.0: {} + + fast-glob@3.3.3: + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.8 + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-shallow-equal@1.0.0: {} + + fastest-stable-stringify@2.0.2: {} + + fastq@1.19.1: + dependencies: + reusify: 1.1.0 + + fdir@6.4.6(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-root@1.1.0: {} + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.3.3 + keyv: 4.5.4 + + flatted@3.3.3: {} + + float-tooltip@1.7.5: + dependencies: + d3-selection: 3.0.0 + kapsule: 1.16.3 + preact: 10.26.9 + + for-each@0.3.5: + dependencies: + is-callable: 1.2.7 + + force-graph@1.50.0: + dependencies: + '@tweenjs/tween.js': 25.0.0 + accessor-fn: 1.5.3 + bezier-js: 6.1.4 + canvas-color-tracker: 1.3.2 + d3-array: 3.2.4 + d3-drag: 3.0.0 + d3-force-3d: 3.0.6 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-zoom: 3.0.0 + float-tooltip: 1.7.5 + index-array-by: 1.4.2 + kapsule: 1.16.3 + lodash-es: 4.17.21 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + function.prototype.name@1.1.8: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + functions-have-names: 1.2.3 + hasown: 2.0.2 + is-callable: 1.2.7 + + functions-have-names@1.2.3: {} + + fuse.js@7.1.0: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-nonce@1.0.1: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-symbol-description@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@14.0.0: {} + + globals@15.15.0: {} + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + + goober@2.1.16(csstype@3.1.3): + dependencies: + csstype: 3.1.3 + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphemer@1.4.0: {} + + hachure-fill@0.5.2: {} + + has-bigints@1.1.0: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + + has-proto@1.2.0: + dependencies: + dunder-proto: 1.0.1 + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + highlight.js@11.11.1: {} + + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + + html-parse-stringify@3.0.1: + dependencies: + void-elements: 3.1.0 + + hyphenate-style-name@1.1.0: {} + + i18next@25.3.0(typescript@5.8.3): + dependencies: + '@babel/runtime': 7.27.6 + optionalDependencies: + typescript: 5.8.3 + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + index-array-by@1.4.2: {} + + inline-style-prefixer@7.0.1: + dependencies: + css-in-js-utils: 3.1.0 + + internal-slot@1.1.0: + dependencies: + es-errors: 1.3.0 + hasown: 2.0.2 + side-channel: 1.1.0 + + internmap@1.0.1: {} + + internmap@2.0.3: {} + + is-array-buffer@3.0.5: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + is-arrayish@0.2.1: {} + + is-async-function@2.1.1: + dependencies: + async-function: 1.0.0 + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-bigint@1.1.0: + dependencies: + has-bigints: 1.1.0 + + is-boolean-object@1.2.2: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-callable@1.2.7: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.2 + + is-data-view@1.0.2: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + is-typed-array: 1.1.15 + + is-date-object@1.1.0: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-extglob@2.1.1: {} + + is-finalizationregistry@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-generator-function@1.1.0: + dependencies: + call-bound: 1.0.4 + get-proto: 1.0.1 + has-tostringtag: 1.0.2 + safe-regex-test: 1.1.0 + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-map@2.0.3: {} + + is-negative-zero@2.0.3: {} + + is-number-object@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-number@7.0.0: {} + + is-regex@1.2.1: + dependencies: + call-bound: 1.0.4 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + is-set@2.0.3: {} + + is-shared-array-buffer@1.0.4: + dependencies: + call-bound: 1.0.4 + + is-string@1.1.1: + dependencies: + call-bound: 1.0.4 + has-tostringtag: 1.0.2 + + is-symbol@1.1.1: + dependencies: + call-bound: 1.0.4 + has-symbols: 1.1.0 + safe-regex-test: 1.1.0 + + is-typed-array@1.1.15: + dependencies: + which-typed-array: 1.1.19 + + is-weakmap@2.0.2: {} + + is-weakref@1.1.1: + dependencies: + call-bound: 1.0.4 + + is-weakset@2.0.4: + dependencies: + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + + isarray@2.0.5: {} + + isexe@2.0.0: {} + + isomorphic-ws@5.0.0(ws@8.18.3): + dependencies: + ws: 8.18.3 + + iterator.prototype@1.1.5: + dependencies: + define-data-property: 1.1.4 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + has-symbols: 1.1.0 + set-function-name: 2.0.2 + + javascript-natural-sort@0.7.1: {} + + jerrypick@1.1.2: {} + + jiti@2.4.2: {} + + js-base64@3.7.7: {} + + js-cookie@2.2.1: {} + + js-tokens@4.0.0: {} + + js-yaml@4.1.0: + dependencies: + argparse: 2.0.1 + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + jsx-ast-utils@3.3.5: + dependencies: + array-includes: 3.1.9 + array.prototype.flat: 1.3.3 + object.assign: 4.1.7 + object.values: 1.2.1 + + kapsule@1.16.3: + dependencies: + lodash-es: 4.17.21 + + katex@0.16.22: + dependencies: + commander: 8.3.0 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + khroma@2.1.0: {} + + kolorist@1.8.0: {} + + langium@3.3.1: + dependencies: + chevrotain: 11.0.3 + chevrotain-allstar: 0.3.1(chevrotain@11.0.3) + vscode-languageserver: 9.0.1 + vscode-languageserver-textdocument: 1.0.12 + vscode-uri: 3.0.8 + + launch-ide@1.0.1: + dependencies: + chalk: 4.1.1 + dotenv: 16.6.1 + + layout-base@1.0.2: {} + + layout-base@2.0.1: {} + + leaflet@1.9.4: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-darwin-arm64@1.30.1: + optional: true + + lightningcss-darwin-x64@1.30.1: + optional: true + + lightningcss-freebsd-x64@1.30.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.1: + optional: true + + lightningcss-linux-arm64-gnu@1.30.1: + optional: true + + lightningcss-linux-arm64-musl@1.30.1: + optional: true + + lightningcss-linux-x64-gnu@1.30.1: + optional: true + + lightningcss-linux-x64-musl@1.30.1: + optional: true + + lightningcss-win32-arm64-msvc@1.30.1: + optional: true + + lightningcss-win32-x64-msvc@1.30.1: + optional: true + + lightningcss@1.30.1: + dependencies: + detect-libc: 2.0.4 + optionalDependencies: + lightningcss-darwin-arm64: 1.30.1 + lightningcss-darwin-x64: 1.30.1 + lightningcss-freebsd-x64: 1.30.1 + lightningcss-linux-arm-gnueabihf: 1.30.1 + lightningcss-linux-arm64-gnu: 1.30.1 + lightningcss-linux-arm64-musl: 1.30.1 + lightningcss-linux-x64-gnu: 1.30.1 + lightningcss-linux-x64-musl: 1.30.1 + lightningcss-win32-arm64-msvc: 1.30.1 + lightningcss-win32-x64-msvc: 1.30.1 + + lines-and-columns@1.2.4: {} + + local-pkg@1.1.1: + dependencies: + mlly: 1.7.4 + pkg-types: 2.2.0 + quansync: 0.2.10 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash-es@4.17.21: {} + + lodash.merge@4.6.2: {} + + lodash@4.17.21: {} + + long@5.3.2: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@0.486.0(react@18.3.1): + dependencies: + react: 18.3.1 + + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + + marked@15.0.12: {} + + math-intrinsics@1.1.0: {} + + mdn-data@2.0.14: {} + + merge2@1.4.1: {} + + mermaid@11.8.0: + dependencies: + '@braintree/sanitize-url': 7.1.1 + '@iconify/utils': 2.3.0 + '@mermaid-js/parser': 0.6.0 + '@types/d3': 7.4.3 + cytoscape: 3.32.0 + cytoscape-cose-bilkent: 4.1.0(cytoscape@3.32.0) + cytoscape-fcose: 2.2.0(cytoscape@3.32.0) + d3: 7.9.0 + d3-sankey: 0.12.3 + dagre-d3-es: 7.0.11 + dayjs: 1.11.13 + dompurify: 3.2.6 + katex: 0.16.22 + khroma: 2.1.0 + lodash-es: 4.17.21 + marked: 15.0.12 + roughjs: 4.6.6 + stylis: 4.3.6 + ts-dedent: 2.2.0 + uuid: 11.1.0 + transitivePeerDependencies: + - supports-color + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.1 + + minimatch@3.1.2: + dependencies: + brace-expansion: 1.1.12 + + minimatch@9.0.5: + dependencies: + brace-expansion: 2.0.2 + + minipass@7.1.2: {} + + minizlib@3.0.2: + dependencies: + minipass: 7.1.2 + + mkdirp@3.0.1: {} + + mlly@1.7.4: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + + mobx-react-lite@4.1.0(mobx@6.13.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + mobx: 6.13.7 + react: 18.3.1 + use-sync-external-store: 1.5.0(react@18.3.1) + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + + mobx@6.13.7: {} + + ms@2.1.3: {} + + nano-css@5.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@jridgewell/sourcemap-codec': 1.5.4 + css-tree: 1.1.3 + csstype: 3.1.3 + fastest-stable-stringify: 2.0.2 + inline-style-prefixer: 7.0.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + rtl-css-js: 1.16.1 + stacktrace-js: 2.0.2 + stylis: 4.3.6 + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + nice-grpc-common@2.0.2: + dependencies: + ts-error: 1.0.6 + + nice-grpc-web@3.3.7(ws@8.18.3): + dependencies: + abort-controller-x: 0.4.3 + isomorphic-ws: 5.0.0(ws@8.18.3) + js-base64: 3.7.7 + nice-grpc-common: 2.0.2 + transitivePeerDependencies: + - ws + + node-releases@2.0.19: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + object-keys@1.1.1: {} + + object.assign@4.1.7: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + has-symbols: 1.1.0 + object-keys: 1.1.1 + + object.entries@1.1.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + object.fromentries@2.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + + object.values@1.2.1: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + own-keys@1.0.1: + dependencies: + get-intrinsic: 1.3.0 + object-keys: 1.1.1 + safe-push-apply: 1.0.0 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + package-manager-detector@1.3.0: {} + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.27.1 + error-ex: 1.3.2 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + path-data-parser@0.1.0: {} + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-type@4.0.0: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@2.3.1: {} + + picomatch@4.0.2: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.7.4 + pathe: 2.0.3 + + pkg-types@2.2.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.7 + pathe: 2.0.3 + + points-on-curve@0.2.0: {} + + points-on-path@0.2.1: + dependencies: + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + + portfinder@1.0.37: + dependencies: + async: 3.2.6 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + + possible-typed-array-names@1.1.0: {} + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + preact@10.26.9: {} + + prelude-ls@1.2.1: {} + + prettier-linter-helpers@1.0.0: + dependencies: + fast-diff: 1.3.0 + + prettier@3.6.2: {} + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + punycode@2.3.1: {} + + quansync@0.2.10: {} + + queue-microtask@1.2.3: {} + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-force-graph-2d@1.28.0(react@18.3.1): + dependencies: + force-graph: 1.50.0 + prop-types: 15.8.1 + react: 18.3.1 + react-kapsule: 2.5.7(react@18.3.1) + + react-hot-toast@2.5.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + csstype: 3.1.3 + goober: 2.1.16(csstype@3.1.3) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-i18next@15.5.3(i18next@25.3.0(typescript@5.8.3))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.8.3): + dependencies: + '@babel/runtime': 7.27.6 + html-parse-stringify: 3.0.1 + i18next: 25.3.0(typescript@5.8.3) + react: 18.3.1 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + typescript: 5.8.3 + + react-is@16.13.1: {} + + react-kapsule@2.5.7(react@18.3.1): + dependencies: + jerrypick: 1.1.2 + react: 18.3.1 + + react-leaflet@4.2.1(leaflet@1.9.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@react-leaflet/core': 2.1.0(leaflet@1.9.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + leaflet: 1.9.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-refresh@0.17.0: {} + + react-remove-scroll-bar@2.3.8(@types/react@18.3.23)(react@18.3.1): + dependencies: + react: 18.3.1 + react-style-singleton: 2.2.3(@types/react@18.3.23)(react@18.3.1) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.23 + + react-remove-scroll@2.7.1(@types/react@18.3.23)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.8(@types/react@18.3.23)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.3.23)(react@18.3.1) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@18.3.23)(react@18.3.1) + use-sidecar: 1.1.3(@types/react@18.3.23)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + + react-router-dom@7.6.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router: 7.6.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + + react-router@7.6.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + cookie: 1.0.2 + react: 18.3.1 + set-cookie-parser: 2.7.1 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + + react-simple-pull-to-refresh@1.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-style-singleton@2.2.3(@types/react@18.3.23)(react@18.3.1): + dependencies: + get-nonce: 1.0.1 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.23 + + react-universal-interface@0.6.2(react@18.3.1)(tslib@2.8.1): + dependencies: + react: 18.3.1 + tslib: 2.8.1 + + react-use@17.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@types/js-cookie': 2.2.7 + '@xobotyi/scrollbar-width': 1.9.5 + copy-to-clipboard: 3.3.3 + fast-deep-equal: 3.1.3 + fast-shallow-equal: 1.0.0 + js-cookie: 2.2.1 + nano-css: 5.6.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-universal-interface: 0.6.2(react@18.3.1)(tslib@2.8.1) + resize-observer-polyfill: 1.5.1 + screenfull: 5.2.0 + set-harmonic-interval: 1.0.1 + throttle-debounce: 3.0.1 + ts-easing: 0.2.0 + tslib: 2.8.1 + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + reflect.getprototypeof@1.0.10: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + get-proto: 1.0.1 + which-builtin-type: 1.2.1 + + regexp.prototype.flags@1.5.4: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-errors: 1.3.0 + get-proto: 1.0.1 + gopd: 1.2.0 + set-function-name: 2.0.2 + + resize-observer-polyfill@1.5.1: {} + + resolve-from@4.0.0: {} + + resolve@1.22.10: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + resolve@2.0.0-next.5: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + reusify@1.1.0: {} + + robust-predicates@3.0.2: {} + + rollup@4.44.1: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.44.1 + '@rollup/rollup-android-arm64': 4.44.1 + '@rollup/rollup-darwin-arm64': 4.44.1 + '@rollup/rollup-darwin-x64': 4.44.1 + '@rollup/rollup-freebsd-arm64': 4.44.1 + '@rollup/rollup-freebsd-x64': 4.44.1 + '@rollup/rollup-linux-arm-gnueabihf': 4.44.1 + '@rollup/rollup-linux-arm-musleabihf': 4.44.1 + '@rollup/rollup-linux-arm64-gnu': 4.44.1 + '@rollup/rollup-linux-arm64-musl': 4.44.1 + '@rollup/rollup-linux-loongarch64-gnu': 4.44.1 + '@rollup/rollup-linux-powerpc64le-gnu': 4.44.1 + '@rollup/rollup-linux-riscv64-gnu': 4.44.1 + '@rollup/rollup-linux-riscv64-musl': 4.44.1 + '@rollup/rollup-linux-s390x-gnu': 4.44.1 + '@rollup/rollup-linux-x64-gnu': 4.44.1 + '@rollup/rollup-linux-x64-musl': 4.44.1 + '@rollup/rollup-win32-arm64-msvc': 4.44.1 + '@rollup/rollup-win32-ia32-msvc': 4.44.1 + '@rollup/rollup-win32-x64-msvc': 4.44.1 + fsevents: 2.3.3 + + roughjs@4.6.6: + dependencies: + hachure-fill: 0.5.2 + path-data-parser: 0.1.0 + points-on-curve: 0.2.0 + points-on-path: 0.2.1 + + rtl-css-js@1.16.1: + dependencies: + '@babel/runtime': 7.27.6 + + run-parallel@1.2.0: + dependencies: + queue-microtask: 1.2.3 + + rw@1.3.3: {} + + safe-array-concat@1.1.3: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + get-intrinsic: 1.3.0 + has-symbols: 1.1.0 + isarray: 2.0.5 + + safe-push-apply@1.0.0: + dependencies: + es-errors: 1.3.0 + isarray: 2.0.5 + + safe-regex-test@1.1.0: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-regex: 1.2.1 + + safer-buffer@2.1.2: {} + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + screenfull@5.2.0: {} + + semver@6.3.1: {} + + semver@7.7.2: {} + + set-cookie-parser@2.7.1: {} + + set-function-length@1.2.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + function-bind: 1.1.2 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-property-descriptors: 1.0.2 + + set-function-name@2.0.2: + dependencies: + define-data-property: 1.1.4 + es-errors: 1.3.0 + functions-have-names: 1.2.3 + has-property-descriptors: 1.0.2 + + set-harmonic-interval@1.0.1: {} + + set-proto@1.0.0: + dependencies: + dunder-proto: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.5.6: {} + + source-map@0.5.7: {} + + source-map@0.6.1: {} + + stack-generator@2.0.10: + dependencies: + stackframe: 1.3.4 + + stackframe@1.3.4: {} + + stacktrace-gps@3.1.2: + dependencies: + source-map: 0.5.6 + stackframe: 1.3.4 + + stacktrace-js@2.0.2: + dependencies: + error-stack-parser: 2.1.4 + stack-generator: 2.0.10 + stacktrace-gps: 3.1.2 + + stop-iteration-iterator@1.1.0: + dependencies: + es-errors: 1.3.0 + internal-slot: 1.1.0 + + string.prototype.matchall@4.0.12: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + get-intrinsic: 1.3.0 + gopd: 1.2.0 + has-symbols: 1.1.0 + internal-slot: 1.1.0 + regexp.prototype.flags: 1.5.4 + set-function-name: 2.0.2 + side-channel: 1.1.0 + + string.prototype.repeat@1.0.0: + dependencies: + define-properties: 1.2.1 + es-abstract: 1.24.0 + + string.prototype.trim@1.2.10: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-data-property: 1.1.4 + define-properties: 1.2.1 + es-abstract: 1.24.0 + es-object-atoms: 1.1.1 + has-property-descriptors: 1.0.2 + + string.prototype.trimend@1.0.9: + dependencies: + call-bind: 1.0.8 + call-bound: 1.0.4 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + string.prototype.trimstart@1.0.8: + dependencies: + call-bind: 1.0.8 + define-properties: 1.2.1 + es-object-atoms: 1.1.1 + + strip-json-comments@3.1.1: {} + + stylis@4.2.0: {} + + stylis@4.3.6: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + supports-preserve-symlinks-flag@1.0.0: {} + + synckit@0.11.8: + dependencies: + '@pkgr/core': 0.2.7 + + tailwind-merge@3.3.1: {} + + tailwindcss@4.1.11: {} + + tapable@2.2.2: {} + + tar@7.4.3: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.0.2 + mkdirp: 3.0.1 + yallist: 5.0.0 + + terser@5.43.1: + dependencies: + '@jridgewell/source-map': 0.3.10 + acorn: 8.15.0 + commander: 2.20.3 + source-map-support: 0.5.21 + + textarea-caret@3.1.0: {} + + throttle-debounce@3.0.1: {} + + tinycolor2@1.6.0: {} + + tinyexec@1.0.1: {} + + tinyglobby@0.2.14: + dependencies: + fdir: 6.4.6(picomatch@4.0.2) + picomatch: 4.0.2 + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toggle-selection@1.0.6: {} + + ts-api-utils@2.1.0(typescript@5.8.3): + dependencies: + typescript: 5.8.3 + + ts-dedent@2.2.0: {} + + ts-easing@0.2.0: {} + + ts-error@1.0.6: {} + + tslib@2.8.1: {} + + tw-animate-css@1.3.4: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + + typed-array-byte-length@1.0.3: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + + typed-array-byte-offset@1.0.4: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + has-proto: 1.2.0 + is-typed-array: 1.1.15 + reflect.getprototypeof: 1.0.10 + + typed-array-length@1.0.7: + dependencies: + call-bind: 1.0.8 + for-each: 0.3.5 + gopd: 1.2.0 + is-typed-array: 1.1.15 + possible-typed-array-names: 1.1.0 + reflect.getprototypeof: 1.0.10 + + typescript-eslint@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3): + dependencies: + '@typescript-eslint/eslint-plugin': 8.35.1(@typescript-eslint/parser@8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3))(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/parser': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + '@typescript-eslint/utils': 8.35.1(eslint@9.30.1(jiti@2.4.2))(typescript@5.8.3) + eslint: 9.30.1(jiti@2.4.2) + typescript: 5.8.3 + transitivePeerDependencies: + - supports-color + + typescript@5.8.3: {} + + ufo@1.6.1: {} + + unbox-primitive@1.1.0: + dependencies: + call-bound: 1.0.4 + has-bigints: 1.1.0 + has-symbols: 1.1.0 + which-boxed-primitive: 1.1.1 + + undici-types@7.8.0: {} + + update-browserslist-db@1.1.3(browserslist@4.25.1): + dependencies: + browserslist: 4.25.1 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + use-callback-ref@1.3.3(@types/react@18.3.23)(react@18.3.1): + dependencies: + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.23 + + use-sidecar@1.1.3(@types/react@18.3.23)(react@18.3.1): + dependencies: + detect-node-es: 1.1.0 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.23 + + use-sync-external-store@1.5.0(react@18.3.1): + dependencies: + react: 18.3.1 + + uuid@11.1.0: {} + + vite-code-inspector-plugin@0.18.3: + dependencies: + code-inspector-core: 0.18.3 + transitivePeerDependencies: + - supports-color + + vite@7.0.1(@types/node@24.0.10)(jiti@2.4.2)(lightningcss@1.30.1)(terser@5.43.1): + dependencies: + esbuild: 0.25.5 + fdir: 6.4.6(picomatch@4.0.2) + picomatch: 4.0.2 + postcss: 8.5.6 + rollup: 4.44.1 + tinyglobby: 0.2.14 + optionalDependencies: + '@types/node': 24.0.10 + fsevents: 2.3.3 + jiti: 2.4.2 + lightningcss: 1.30.1 + terser: 5.43.1 + + void-elements@3.1.0: {} + + vscode-jsonrpc@8.2.0: {} + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-textdocument@1.0.12: {} + + vscode-languageserver-types@3.17.5: {} + + vscode-languageserver@9.0.1: + dependencies: + vscode-languageserver-protocol: 3.17.5 + + vscode-uri@3.0.8: {} + + webpack-code-inspector-plugin@0.18.3: + dependencies: + code-inspector-core: 0.18.3 + transitivePeerDependencies: + - supports-color + + which-boxed-primitive@1.1.1: + dependencies: + is-bigint: 1.1.0 + is-boolean-object: 1.2.2 + is-number-object: 1.1.1 + is-string: 1.1.1 + is-symbol: 1.1.1 + + which-builtin-type@1.2.1: + dependencies: + call-bound: 1.0.4 + function.prototype.name: 1.1.8 + has-tostringtag: 1.0.2 + is-async-function: 2.1.1 + is-date-object: 1.1.0 + is-finalizationregistry: 1.1.1 + is-generator-function: 1.1.0 + is-regex: 1.2.1 + is-weakref: 1.1.1 + isarray: 2.0.5 + which-boxed-primitive: 1.1.1 + which-collection: 1.0.2 + which-typed-array: 1.1.19 + + which-collection@1.0.2: + dependencies: + is-map: 2.0.3 + is-set: 2.0.3 + is-weakmap: 2.0.2 + is-weakset: 2.0.4 + + which-typed-array@1.1.19: + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.8 + call-bound: 1.0.4 + for-each: 0.3.5 + get-proto: 1.0.1 + gopd: 1.2.0 + has-tostringtag: 1.0.2 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + ws@8.18.3: {} + + yallist@3.1.1: {} + + yallist@5.0.0: {} + + yaml@1.10.2: {} + + yocto-queue@0.1.0: {} diff --git a/web/public/android-chrome-192x192.png b/web/public/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..778cf7561dfda5c0b67fe5c262f72615a8b9b387 GIT binary patch literal 25014 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 literal 0 HcmV?d00001 diff --git a/web/public/android-chrome-512x512.png b/web/public/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..5fd83fff786a2429e661256547cd1639cc6bc1e6 GIT binary patch literal 141510 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

Cn9*Gz%uKqRFe?NC_Gbasl-$I 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

BagoUxOv5zm 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°8UHN}EY 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%0DbR1WCpSo}l5h#TQI9MAqBI*4?$kr_yTzn)OfNO^ zx@Hxoa+}U9pRg~0hpCjlhCsOBEUO3}k+>Z6Hry-&LJ}kQ*ZuDNJdWiK3ZA)tSwuO* z6iTbK00Qd2;b+z{d9QEslkJcH_?gw8vmI4Cy*)gpJ1+_fseS^RR6?IK_Qf7K89!`1 z{{AR^CMQiiU#wftQlyl&UiZI!e8n}B{^TFcwUKyA#t&0wj=Wz)h(!!bj4u$jShwHw zs<+7BgL{yYSR^70#k0=Mxjsc`hwkl`c@N=>B{ zh-Mbu=I+!7-;FT;Zb=dwPD1HJfr5wfKI>x$hK7~Y2DJd#=A1xG0?D<;wMF_^mn4}O zm|$cyWFfYX(%%jVXoiDAYN-y=6|NeQCGz`~UfE5hEOKR@!cy_n1d6sG^QgOU^B?jl z((5*(woXZ;DPN*}2I2aDbNCax41s}<4D}czaH|+9c$kzsSm!|&`kz!qhOgF${dJz= zZbr5o)cl@!q-H^3#01k7B_fQB!M%Mfu(5iqQ3@Jb)kDs}eq7)qmG5ma~#bpC{x z=!p7w$3K)@t}+^RG?=RYH_L~vkxr&?JPDW)P|=7Q`!U3OV46mxLP|oEb|M5%BQ1rqpLag9o=kxK(87hCCSoLO5WR*M` z9N+b+x~u5B{JN2~ldUwGORcU;I#E7^-`pbIPxg6aud_sqB&z#YgQ-7@2^7!-Ss2b# zmAvNAhUGHL;^g&HzwZjle)#cAJ>C9=mgaf>SFF{Ae*1x*7DW<@eE(z0feXJcKBo>U#NqXoT2*B7htx1Yn1~V9NJnvpx~g38 z6q(luHXFa97?Gp;fZaGl;!=?2&xW!Iah?T!3^tQ!aNzeR*@(L`#;!3EDU`Cj{lsYK zBzCIxy?6s(3gyLrh>VZyJ&IYh6AK+2B?x^E4ooj%5g7wLz`eie)hbu5WEYJ}+tS4aYk&Z|gV6*)rPhok+rLO{!^ zYQ;E$r-V}XD+$5*{DP1#W^o{xwd)VF_LRu7-HeeE&-{01sjB2U^-R;L(N?j^3TJBh zTt`ENN=eN-vncyN^S_D_6F1BaN0B&uIu&rU{*xCA_%xoP_}72&1EsziF5Qw%0plDY`bp(d zN1jm>`b^_F9mA~vKRj}T%!Md6pev4jIiESdh`(EeAP%}YKiEpa^n-$VbO$Zlo3N;f zoix~a*XD06HtwGw43RJP(SJsh7X%M<0+%NzLVvDIP^R59_5`8{SHi#*+& zBvtY6Ou34KM9+hM&Hb;pSy43s?xZ{%jZCOix{!5v2tq;}5E6k; zO+zvdMJ@1LL9x5L5RyQkB>c3oJd1xEgs}NVFW)uRACik>0gwqCNs--%Eo^&`n)H!H zgCar;Aasaj#qQZolgtgh_}fj(mnTUdz{3u=FW>hAU)l43@Yf7>_yuOnV_d_%f(Xx_ zbS-olM@(uWu6Rdv;a&op#ZX4CsDt}%+1$h?QPd!OtP><3QMkfbGd?rfqYY8|qYlHO zM}2(|;$}k~Y7b?&^xO_X2t8fhJ!oud z%;~SKr>{o7ubP)?i?nxpe@^B(V^Uclb|)whBWTRum55uTX+K{7OwygqvW8Dr+oHaV z*yFp58;8s9xX%>+*}jqxf68T!0dT}tbsW>(?amM-cC8ElMNMFTPUNl7RTl!v9l}XA zzU7+p=Ga|we>j8gxbirx)Bc42{~foRG+_~JEx(k0YMz>41Z>?h-<{PdVM>S;I=|Qs zW5qbV?3c9kK5I=~`$nYyh0UvMB{+7qP{}_5VZmb-GyL#BMt-pqE081#f1vVsq3pYG z=X?Qlx!P$5dF*A1&{cooxPy$RgofTU$Yv&|iiFk^q37-0SZcF^sgnUqBYzyiVyzJ( zO5(?_a^uzdwP+jO1!*v=; z!@CW#KM5SWHeV`e56WlZUs2Ek`E>PL{j>NkM0OO~06YRN;_s3f$5zLMoj`lx^h@N6; zOXP`LyzNyp3YVQYbo6CPkEx3RAnep)kpQOTLd=ba?X0nyy9GK6+XE!N!|YvCn-keWHl+@A$UoCBsC%C021}=e=4A zs!Qhht02S*udCQda_jS<$lN!=+87Q^3>|R+^AF$` zt;}lE-&rl`^#6CTXS!mtsJ^v2UWQQ{dlNDuqL%@MDhSaG_Uq-F)-JDqnx2}#ukkNbmcMGE*DBV)89~3qWV&nnu)n>Ds8MNM-v>H@N zGDkP1nH?mi5$zWV#jO*~WaWyGO++cpRfVUSlbDY!s#o43qw#Sm}A#H5@yK1LFNFqEmA1-Rj^VcHL0cJ1xqV?4?Y!7n#E$J(qV$SlkJU?O2 zTLCuscFR)?bGOCI`S`=wMg2J*u817%e>_JVmGd1R6|wgarFl@D_)Hc|1-zBc5IwI$ zPSQ=rJCqrU)%I0|8iYMZ3I@cHl|pn{982XuJFas!#(g0XXZsmva*q+a>PVA&3H9F& zn>CZ^iTyEKH#7(tISN^CuM2Jbr@Ul+JY;%`wSUBhqY3BRyNjZi%73+?g{Qgd@g=%* zduLXvfwp2&q-c(-7l2Ge3;Ur)*-QRg;*6&z#JDOXm zH25iG9e>$HLFS#^f{uXy4yDM(IG+&h%^XN=hK1fMm&ry948o=9a`uZ6=1$vEICla3 zFA&?)``TIkp*-Ab;Np$+mw@Ub5p1Y*5E$!4+M&!Fj*}SzYy(zZmi*2ZKc6c;OraBf zJfsLX6_HB&G9PajBWqwoP^ww$3VG}5jC$#mz%^Cvy&>bZXIs|EM9h;02h8v2FvY5n z?&QacHc`MgAPqUrh_}>X;_sMemUC_9xugP;Z;I zJGlHEf{QyvjwG!`A}a(oQNj{9jEUDh>V2y234fa}t}dfi#_Pb_Ye693V|UVQ(E~or z3cw0ND?=)w?ooY^+dzSDuzwLKbU-hWv~9EgYqP?yg-%|^(6IS*UR>(4E41`F(qzA1 zv0SB8nI4LF+1xjWd4Ha99)ammK_=+kurE%_>wTjGgA0^x9wJ+Sw4rFBR`jUy%OU81IZ7dv>SB{?`6*nh zk5Qz;nCqUmI_Zsma zkwXgyLH4XIs(80s_`A|!>H$u4%?KdmHy~uhCmh&iO}l!veP&^JoN&87qscy)8Esp^C5_S z?cw>(uJ%f)jC1UWc$-RK?3j z9OS>~JRTEl>$Nb1m6B*$)`TR+fxnO{o0dOayx`ggjS(a+kChG&f=6E|vP5G~R!%Gv zThD&i#wPa{wf*!R1KeH2JuyEHUz{iLpiY@ty1uJ+twjg&{}#H=z6GHeIam77O0n0( z{9I{6IqUwVS)w7ek(-33q=t+Fz2iCrAij!0P%-nu@8x^Za<2E&f~Va6bDfnnXRil{ z{+D?DJ&llgdi1&3zDQ_8y=ml+LS-ro@QqIfw9gqU=smk_s~_C82gro2lwgFlsM5c3 z{HJ6lgNe?6$G@a|g66+%Dtn^OY3?yxY+Xtv24n8n#l-%L^Gy}vl zBAUGRCYu3c7yDTFaD5Bu_Bi$mDs?rA@F3~|ZZ1HOEnAIHCI%4o&!scwbV#PC@QE0C z33?9|8b^QJ_SN{q_6C?ha*&S_1k?Jb_kEj<+&J1FYb^&$#I_k*lG(|}$}8JyvQStV zrz0sCz6C5J1dhQsUt*DwM`tw+1dgXnr z#)IsGXX&v4O6b=iUR$}=YEt1c4-NQA80Yxi`Cl5Vt7Qg4(W6fHSA8G$WbcmXjcDD- zzj4uN>kI2BDLZM-yQ;suDx3{{&7Az``C#^5G@ViecSQIB|iC>9io@lf`4c)`MG1E1_>M( zD|wOYhu4Wl%>r@6{_!h1y~#s?=x??mW*X`58!?{$<*q)3#8@|7*F`zNNnvSia;AgfQbgV+ZsRd$r1RddF&jx4rBS z{$pHH|G01Y``wOQ3nE^VRI^gUyK@Lnjy22MVnYM=z00fbu^WX9TE($L@&<#(K2B!| zZ-BoLenqvIj|-+VmIYVfa@YM`EHU7t)8{6y5uEDA(PvA?ffEBE2%`A~8MID+yc<(3_%3ECeR4Swr&*w)F zgh%b4!+3kt!S~LKE2Ti2ZH(BnD{?Aoe$YqGgbFaWlaDNGK3xxIqLTv>eZ&|i9K=>;Klp?Do`{zzVUP5Q1zsAE`~ zv(+z}+Gd2pdv(HI6+gDa){#j2Jy?RzY+wIr3dY8gI77y^3h<{D^`?U|pZE`c=Ki^* z4vj&qGh%y`d})tMEeSSAf=7K}2r@O^EMTK;Npzn<6s1ts5x`AQuQ*EJp*LadK|iGba)m~f z5TNvco#UsrO=Z98?+M+H*b|W&zrg3AzF&CSaClnD8_7Fve?;0go#BKYQNGWt{3G#} zct;OE#J;_byQ^v=$(KTk(m1JEfPD@R5=|w1Vw3SSV3&KZUW-VG^|`FV)` z#8?O5o{uGe#DcZ%Nqh)BfWuT4$>Ny?1#|Yjx!%)!_$mTL&OMqw7YmE}+ihLwgiXq2 zX<3=Jg`lBD&Oe=GquYKa)2vr8Q#6^**etq0DzDlJ%}20SS>Z!a7+`0#wnDA{|$GQFMA&p|C+D z?3X`Etwv>-9MMITwEs)xJPCceHT~4rFIGPD<=QKacE8kq3fUYR&@=+sA9m)tlT?R7 zpxfA<5NiBveIv=5UGdh?)P(4Y+wTB>SKq zkrO`?uNwDMI*T&`jXh|%(7mY2_Oih0gvqXTqcoAM5IKqm-5*y`Thrp}6z7DAZOhOP z;d-yPpCXSuVDji>>kcyAyB@VdH>@&HPBPcRdm3rq+_RJ~nml@g^8lZ_vptd5U3~3? zM`scg0r_pqzCdEmeZjw~YY5y2dJl^3CKT4l+Su{4 zz|{@SpwP4PA^E180jN}rMyB~}qlDk#^ou?SS)o7^hCfz!si=A@c^nlk6&JiGK1+VZ z+!@01koundIQjQ{6wcUgBs@r0&QOCrq<;9NVxxstZDrzXi26Jq3F{6}3ae0@i!x@q zg>Z1GAWFWlm!HiKDT#RR!q9VT;Vi8y8N_`9W%7*V@SS2~mICKFL4l| z^wwdAFPu$$Mv{Ah404)???ni$%RR%#UM%~^U*J!w@lTCZP$Nf|$7)D*m60iA&}hPc zA@!5V1dWihlOm?`@x}>HM;vgGU%i?Ivd+kpc5L>}Lo9=zPkOaH3Xz(*+ z137Vh#DW9*21Y}THyNB+cOO;bK~yri5jWJ1ubweEgsy1?Wr(||6k@UVKW#e}(U=c6 z98@%lvsZuFu2g+HS*(oAr4W-GU3z)4 zefrn5vt#9t#mgce0#@vn+S|{(A%(Gt2?BIGN|tXpe#f%|f)uazg-}%zW6!tAhv-F= z9k@aHn-0S8C9I2N+=ee-|6V9@x-eQQ0$Y6m8^ywXo*@ClG;b{k@5o(WvjliCDhH{5N-Y?_}I8p9fqhc?^*hw}O^^&B=6Lw>Tx6k#Dc>N+uXP ze*0c+D<^JBm!}9Pysp>Ks0d<18p|}JYKHSVb&=uwmBP5@_vBA32vwcCB`qZ*C1pmY z>nP1xujZ4>G+gq#U!<`N^v2S3zBcY<2^;>&!=8%1tb1;v){JyzwvbDrQv%T1EvjP8 zlIHzjnB%QltlnocM#YcRV0xDjUy^hMIqgSip^W0gsNi>)?Tf0V-)`JjjHf|S13Ye> zb2lHI)Z?@ofmpBpbxo+Izd+h^Xa%Z|?rkdkHL$Z@#5AGRxQBsm*tM~;AZn1A;yH3A zn#R+}<#tCP;U+4SsUqn*6mRp>-VV$nR8`_vR{O+0S77I<f1)*6aB){EXksjSH&?Ty`9*D+TD1j5 zUr=29@YAZ}3wirN>)YmXrt2|4kjPB(NR$D3pzmoHuwTTgiJ_DqdsUfEn?ivaI}Qhc=9&Mq*CU$0yLUvB*BQf+<=LFn{P9Gul3g39jIJPEygnghMVBK5B z+J-(W`^uo!a^2@#l-atXP~g;x^XnMugsMc&5TWYq@ojh$GZtylvWnjVl$As5hfvDK z8rT4;#r(O{Y?DFCD|&5rbVEU)rFMRekW3Pb8XAr{6p562Jp%~`@v`xmzn}VMVissN zONaiC{7eO#fQy3paFzAKJshUClh>&cPwh!UF{LCop6%obKK{Zb5$$@oKhn+K!EmZt z9TN|iL+Y}{qn3vzt(JxBwmqE7E60>8WY2Cx9?d5)X**Z6GcEu`~MboJthJL3TE%g*<5 zY@vReD;)nm!n@3^wlFL}g>eB#rv#B0$-Uk8{UH(f*>A2H66XLMyrs}_QT>D za~)3X5EhF*SoeHcE67H^zQc3d9xwlZ)ZL5KtNN$#^Ah3(v9me%uGu^MU`%YH`Ar;ID-gC9L-yWjG;C%gW!I9%PC@a!@%;aM0v`uyL) zPsFZ?2s;m0IDD|SG{J^;_H^v_c%@{lXx>o{nAt|umkidYRkDZu?!8;PgFZcBc@+BE zW$vV`RJoB0g0|=wv?DnMp+U7eL)AoYLZl%oa@@sGum0vIN zuq^mOd{;J_w<3Ue2Mhqtve6N)=}klRzpB8mfEW^1{VhH5^6)X~b_=5}-kU?Hxle?` zNx_PSI*l|z=3~LNIM}FmDwB>Vrbd#k4boh=wPSgHBsOJa;a%}FgNNH1Ueu&Q(9ExK z>?!Wb|5p7H!BLK|X#YfwcwND1^rgfKrDUK#*8E0dH~j_DUp+ELoD{uO2dA=Dx+?*e8RyT z#w;?t*i(dM)+7YInCu35B}T=vMM~F*9mj#b%VosW;3nbA`)m8aHdDI#tr2tS&h#z#M9#d(P-IE{K+jzV zVFJ_L?i%dNXW#Dc2Aza^-?9t-4+~&hK_e5AUHbASJutdHoEc{&!;n_1bYB1e!J^Bv ziQSY(AGE5r$)Wd=sqxX{3PdD4l&v#NBR0sJ0DY&uja9SQ0v32guakkq$JVM3v($AU zPWzyf`7PP;S?aq#xdxx(;po$tn1nZG{j<4!V$2Q6A~ib(g7oOBc=(9~e9hzK8WX=0L_Q@KnvJc8g|+Hl1JP*kOG488@K)<#2a~uyhHxPCXBW4~gn4-@-IiZiLq?{$5ZSDvS5Q{hvoG=Tk zK`xI;{IwPg^Nik@nN494d)gL!I>dpM)O{7@n!5V)O7cL(@4`MkA0UqZv3bmar+f>z zD;gse_5DB$NqSFyZ8$AzazpP?Wl)+Q<#l)8t&0I4OGN{k z=02zUNWmZP&I+Tg#%Bptr1o77Pwz6gAe)r9nZFr@vf&#BjhYc7G-8DA^AunX)vl)Q zFebm(PU8Q<-9*97tT?b~u4w!xZ$Ka1=4u23$0BC!~=O)}c0E^JkZg-2v`ZL3x zLODY|`~7w|@u`BaJL3Weep>!0HbFp39_((VPwTZ_&eJuA9+DXi<|$uw{1j|T++i~; zs6{vR(@rtbwU38ap~GcXwMFBHCuUOtTqSA=gZr<`<&(jgCiiy2b9uIZ9lz8tIsZD> zNx~gsCo%ZR>xwkN@MI&5%W#r_-~?{E5Q2Yyyk*|aaE0ll1Q9qCM}Zd?*|>W8-b2L$ zKZ>w`cZqO-Qb%*bwX0z6ebgeW3trL3y*exhq{XlgESo-89Ec}TEHju%S)0^TU!#}R zW{dbW$-#3D@zL{9=dkc;^WhS^XDP!fq$-kPZK`uxA#GtNg}aVZLRS2;bZyqRr1q0z z@92oIv~$0YNfgUHHmg6;+z8j7>Ncu89=za$*(iaIhGMcytZ|Qic^q$wJZ*n$jI=La zyG4@coRS@n2koO0b=kN>wwLPtu!`!>x_y9t(f*uo*g z1l;}lQzdMptGvT>@<<`R&-O5-FQ%8~?Ax*mllTOl3m+l{s62aTgL1tc5iLIF{WkNZ z|GwJ7;BYpkM{%}$Zf23|nP@V%>{=!O?=b< zpg^@6d!llE$h>TwwUdkq%Q#tQ7+z65bX}+_(*AkBQWZNA%n5)lfYLYBX+EnF`Yx_2m zb)G{6C`FDV+;k&=pS`!}U|5zU0otM9yu8_d@>PLv+gK=3U2wiGfCIm6=>v18)vB@% ze_kifjh2$$_AeY^k8V?cP#>_`(a+)3ccHz6OJUfE4J=L%DdOXkfE5R9U)jX;V)V|M zV{;lt5;40pZ@3%D*EsDI-5S823w>C=?UR=xQ2SROqGX$Ayx6u85SDspMR=gjsN_^6 zZFp%|oe+K?zxw_*H?n^oL7)A1e?q^;s2Fd@{kD;)eVnEJ_ein{hFx!ECad>({}ZK% z1VQ&odOW)WWuChdUgRqNtSF-l;!vNH>M?s&Q)|W?WpMturu zaC_X4aHV&gkO2lLi#$+rz5*MdRx9ModWE0m6-OZTo_Ig0sh%cF*sWLehBtRe8&6sW zG4|W7=iIY%{0|^qeDFj6v133&fzX z^Z*hg+flph2^}TdCLMwM>^V_5@WMHaeAtdC6%IlEbagTwaP&QQQA9WZgo=Z)bEieV zZtWivbYT_Yy)OkQ(8oxkk6(wTFN)hCo$R z>GMI1U1i?pAWY|{ZRn&fzm%BvGlR$OaNYavozJ=RKIE4XHX9=!oy>I`9vFI_-6nnD z6!)smp!mBkF~`sO z$TE87sW3{B?Nm2_e_3NQ{54{Lor$TWP$8Mal2Dv@NrDmzVL_-GpgLS{Ow6E`m}_!# zx%)`~Z)15Gf~lh>4Umv*ff$kn2WB!$GQGDAY-G1i8BfWF+z>kdfh+(w9O2pW2rE@z ziKP~EUD=}KHebH63(lkiNVasjW=yV7B%wTAhoBzpX{!EUD&(_b>Fj4l20$B1%{2*5 z9HIAnXcV9kKX*qb9uE?Zwi_ZOx-%w?7)2x2pri+f4QD2Xj?zal>jyKjOp0;I$RmV7 zfz$sFQD@oJRvT{XBuH?FA_0PHiWGAakmQY?(R~cxD+c;iWK+auEpK8xN~^- z9($ZWkS}XxJWK3F*wL}a!ik^W)PO=|7b67kGg7{Y8U4-Qo#$S(RXF)cbI%4 zFIv6vw`{aG<rX`CX_xH01SVry%*YW{kW za{1r-+sxl`s#;6ydR-x_{;T>Cza&PxK?Nx$e*pmGlNOKk)8W!X>Jw%8E3Z-t=fPP$ z&v34s-k5Cq2}24tx^MX?o-b|k6$-sqj8o_3eMXc?2l_Qu@n^jO?=Gd5nq0ZoNg`Q) z^7RZ2lobTMm%H7Ovcb|&UuWhmV$o)9!$;$KGnn#ZgkNkMwUKs!g5hxir~qc8&hCHm zm%|m0c^$Ve!PIi|NEJoor*(5aP5OyFBnqWrlG?eA`N&=$vEG`CF9@~+5V^B*7sXKL zW8LA)@5D(zshWGUZQ%j#O$%s1DI_L-iGDXYEO>psw~Awy@zGkuEaS#j;ZK&#y)iJ{ zak1Lx0tzUJT4y*CN@$>E=r3w>4;bbPULzrO^cvNl15du)6^_av6HD`fr6^Z5(1T=n zlEt

l$ks%y=(3+_`I-k)CFkj$>07-x~dc;8QE{r9K;5}VSpUgj&LS*;_#755WU z1-1-_vs*^0KU&w(&s8;Q$ zbxyUd!dV~Xt8$Hr^Cj$oFq0|Y%U2egoMxh@Q^6v1{&|V~j?;jPZ#XLb*+rT3Zuh=? zGqxtk!07<1_}=Yeyl8QyqG@<}=fcJ6KlWK4XmoFC65I%;ZXB)X#`9w~w9InZ1ZW?N zrOfhhE4vx{3tv7zpbY9Y(Z&4IIo0^xvk$bpLsv3ks+q3>a9seBx`j} zyks1tO&{+Zd^;KtfQNUvf$AIysFAok!``3O#Pv)5eq(WDo!l038u}MgdVoWT=!bdY z_$bG88}>{Y6;mjLi$Z$3&#RGf3|NL1CTmagyel+&8^%$fB%z5YHI18lY^Z?V5O#YD zCoZmlbz;F*jtUJ6>xy=OI-JTeNapDl zh7NNk(J+}CX%2Ue?l5u}oovkupZ%pt@g+N@x zC-V+9WB|rtrR;n|^5&_Wx8zC8cFOqBgU6yfO)=(FDQ?~h0F4lK{VnUoi0ZTdpNQiA4e$f z(t&c$2Xjr4v_ChZyMs}MGoOlAvRKep7)zHzunm;4>`7S8{;b51F$Jo~^gLKrLi8& zd*}1k>Z|TCS83=K4pJr%7wcS1wDq4}@g9Tr76_l^bCh>MgJwMUr^h&wpTA@VAea1k zjy;$rlt*6S$Cv{wv^oZLpMnKs*_Rok6>qa|h#n|YGs7sr02~w?3R4*~+SVc+xZckEKg#`cL>=Mlt%!5(gWlJlF}Bf*VpZ|{%ECD6YQt}*NF+PUuczxpm{ zuHU3ojxy`|^NssAx_?x*iw`AzCzIL(Z{;69{!c$Zn{xzdhc2l30znRur>kc@$z_cmvFZu~UNrR*8ZdvWwSZXM&bg zx>7-fWS?no;AoqPN#x1M_$SO2`0bZL5h*#i?I%&$l1OhdaWD3&n6fk});!rir!M1^ z|F>QGu8S%7%Qq!o%yOrN3=C-z$SXa8#c){tV z@_9*MC*w;zOOJ7YP1D}8i`loC<7oaBXG)ZN!j{f;Mj2eLD5)$Z$AjrM%0Q(SScq5R z6;LPSw6}BAJN0`b7BL=&O&|=#xC)rU#|w=Rwc^xJ3*vgB)P52d6Wg?B;iqjIvB0Eh zP@1`Y76&Le3#-ko$e);;bsMi;prLL`rSBI=CgJd#5zB^6a6vG z88^;(zy-G>`)HuRgWi$*TT&pje;E2@Wbtudk!m6m>@eUUyIfMC)~^j3avoSPbQl!A zmi6)HOOfB9Byyu^FUQpb^I*XyE;#PM@cj9&5H6r>;3MWUZ{c;RWc;!aZLtwhsym>C z!G?Ex@0`puuSD9E*CkroG-{kTpV7q>i5+bMKMqHdOmKn-foyH-eV^ei;D};g!NVDN zP0G!4opqP(_|$Se|FN<>LQ zd`x;2smD4I;)i{(<~~J2tI>b_N*`_ZKxaR!*>&ObbvRtG7Ajd$Rs221bA{MJ^-olT z9PK%TCHCZ0bjz#nNd^Q@dNB&;MFr;WKm=^e!YRrY$CDVGv$rG8?aF08(JfQz=A5(a z4m4x(KX0SfVzhRb!kE`ko_;fXTW<3H(%z3;^m=#_>gST1aRfWXzVrP1mJzdw(4THq z_Jdjr2j!is2q5$!Iy>KUR3vEO{FM5cfa-Q*B`QBbUw#?QjMC4J?nfdnMrQw?mB!%+xSNsE~gd!{A5tC#y4eN(J&@99MryQlqxpGY;e#a@I zVXnAf{5%!Wo`RGNq;dG;2D%sO`b&3jHXYazUlrJW+4xV{=lz&HbKl)aaos&(b@5}f z^{fJu6r%^46MNVrC)L!m=?|_t#XPi=_5|=1n5;c@9Ze(Y$Ll^Iy*RD?1llqC8Z^$N z!%aMoH~-B#xivW!e+*F3HHtc+IZ9-=`;~+yYvhL*H{6xU5?srVRYVU^G+KtG=bP4b zoFU9=M@NS3$%$@zPGl&lFp?8?5ZEtl)RXo$)49ib?Z{O|d-*x-xw$yTmnmd>pM`7v zk!zpF*CF+-b3`(Ar(Kahky+R1+s`%o$XCt_wkI4a%c=K^h7JucSdyB9jSqPc?unaT zBWdYQ?h2=-&TFLc*2E(dNMq_ukvI2joFK&A>I5 zI*>ZO2_ABFK*Yll9@&64`N#*$QHP8&WvNGdbknmm*I{qMHV4!nbxZp5V{1MumB}~> zYEhG+kVz!mZ($~28h!n-dz{S?eLNrU$s^;xW7q8ZN&52J|NA1WWO*Nlk3Dx2CO^kI zbYGqQ;n+Tw^+R3zlZa6B{}MnnkDBGwDm9Cu|EA^eVGQo^7>JrxEP8xC+)j4~8Whr^ zT<>F`17nig{n=XKvqZ{AOnC>vfGNX8mVSAPHhspCl31n^%LOUF;OnG}SWA{amr%?n zJ1VmBjB|Wwb+L@;dC23Fp_ByJg#Ghoy;6Jg`i_f`u=>M6ip)byGNVvM??bLlQp=Gg z!?p~{&qLS6w4sau{DB+DxKCUZLZ3q)D5#C;I%W+V6qJ$RyIp@tG z5)Lo}n5U)2Fj+KOrzg4tD#6W#9)flUC(IRwKfJWC1xNjJaICQygO$o?v9Uq;*O2%E zY)pE646f^&VkE zp?xw)I+ISS{i~t7WMK(+UBF*^G@aiz#;8a|30${JpW8U_{>94p@lIY>)c4hctrS$` zfcvYTd@INts8wFFCB*<-+``o8slMXO5tLRW2)1mzd4c&)y^Zs>=j%kyhfJ=N7?y#p zpu`yL>y>-me-=2=Q|Bh3S&5tEir2IxEB_KiT`>{_-0ww7reh^PbvHAE6S73@=x`(A zMp({L*0c4 zQcZbP2m2Tbe3#k_KZ4v?l<#~(V@at;AYQ7{4fSOOAedeIzaAh!X(V{9JUl)8z6C!Y zWv32HWNN}WSJs#mo$*cMP_1V8ENUTu)8b`kZ6ea>%ok63)~`5(Xm6jz3$l6@71+6{ zkfcxqkA=xcdcu!a2389YyDG0`0q!!4QmYQi(Kqxv8>?aAT5?`X^_F)niM+SyULzpA z(dI$qFFX&PXN)l1D0;(^&@$1&YSU_g;^U)jMj=BiayC zY9|hPj=C50no}ZohRH*QoOA?eHptwzJawEbv!6l1?%M+q@#kOh(j7rq;!DpCq!n8% zAqjA728(-sm;d%W#G_Q@xl*ejQ%0m+m?XaN_Vr=Y_Heq+uvi!#gSV_yKB}BKC>^-q;}=dA-UT4LC=zS>kPdQ3X~Y)B^o~7Z;On3(*Qg?JKC?$@ zf2C`7S{Q~_pj4nNMQKIE|NU1IdsF!Cyv``sXuUO8pJQ$@z3a%K-C(&Z+nH|@9E3m- z{O7iHMX3|o_IR{5_QPF)h7f;pMgBRd3~A1yOu_yl^WgPV{ zhOd2z<4qU8mVNqR9%=C&Cxwk0+HhOkwL<&uduCTJ_rnh%!&{R&r1h;8asd#fU=3gY ze8|2??9Z{>P+m?o=ztA%89G+N52u2zKCTk?SwRi_@PuKs9=%X{^|by798W|83!?+% zUx6CHehZ_{XJHS=*&RWWWwbDJP-GD364GeC0P>e-84Y}zUA>ozk7w^am5TM^T)Wp_sm1KUNW~uly}EJ)y^nt#;sr5-sC?0v%_LKdxum=!wCc zeLCGSju~KRvB`n%sK+^%OP%IlqrdxDYDu^L+U=?3lYAGnbotbNEhFP*e=Axp3p4B$ zjvvnh*TuHRZuTfye<`T~i^?pu%-yk9)8rw`@69?n5p2(=+7n9#g3A^KAMLoo(F$EG z8R!AvaOWGS`Z05@a?q%*`iwesbwYU%{fk_Wd!y0VOWK6u4eZ&0=M9Q}Mg3V*?&!Eo zs(%)XRlR0eibY>o{ec{TOLCu0S+b+F+S`b3Yce)UUn!@bf2>&)G(4@k7>#F!tWypO z5#?!Tortu}wlp)pN5@SfxQ1k1%7Hm8JxGc=Ct}kMyDsJ53yX*l-x|Ou9ST$LW`5@; zx7{YQgVOTUqA%9qJHil*+PA9h^Zq$?h-Mo&X^P-N=q6{s-QX})cBDQVi8LhZpca2Y86`Q?ANh; z+ne(*mK}y2BMS{ms6XS_tE*v=8fhg#@G%>nk1Gp6G_UbEd*!t2bbHKo2)`gpOhyTH zU77SzG~$j)ey1%zBXZV|n*446UHkhJF8e7uoZ5bpEk!9X9tIcQ*4TbF2{+x^n42EVd3F^91R>eryO$1P{t*tQ%+4}3N)LV0Ei zgQ=jamXpV^fiaB6#pl&>ivo(NN?SbYaW?1mr6w0HEBE8(TjPYYj?W8qHT}OOdt;1C zy8Wzpt7TQ`HhJ?N5Hny6>!mOVplN7U3Vq6YP`GouY;@a`b>|ZFRC~+J0N=`sH zIw^H`_?XP^J~UNACaXW1NOET@ty2K#StlUg_xDUAqeH$GJL4z&tj&|x%C?`p_n^h# zc5d<2sZm>1Yq~w4k>MDz3N9As%2ztEcV8BQlC6?7I@B;*$2`&!~QNwds>}Gb%j^}V)R64V8j}|${Na9*|NBs0UE=J z$|4jM4R;Zzr*yHQiAusFkhqZO!i$?ZR7qy(81h$w&YwQAKu3C`(qO1~=c2Ekk&5t5 zSY`Xa+2CDNGG%|g67QI1^}O0Azw7HShB}y~{la^8N%l%Oky(qa>c0K-I)HW~rn!=T zdhQ32_}+v^%FYl*?DJ*+Y0`+P;m_F@2A+WM)y~Sd4GO8{MX8Q6T2AiPnGteqK$%fE zA`0%1(3Ac0J&2k#a+rue-#b`iUJ$H;$G&8)GVseD8p7fz>9@yjc#2c0;)`F)vM zJQifxw|$?^_w1&h7gdb3lZS2F4rUqGBJdfrhsZ10&Cw9 z;`flplNp(KjJm3SA$tNaSQO9EC?XnO1;J)IR_P$!JMcM0JyOXi$gpwWIKg8RG>#3L z@Ft9LNH#sGf2vUFwSLa%kKYYD+>xs>0wCkdNqe4bebqS}9%lSl33e}i4m*(%yjf{< z%5J`jl~QJ+7Mb{rZYxALQh+N8A`3`(t!k#^NR1_2dxOW)AbrP;XG34-^nYG}#|@Ps zV!1>cK;ExH+8g{r{;9`Qrcgh%w(kS893&h7O7sE#5~wy*mI@Idtz_C0!|&!3J_Tec z@Ahlu+WdTQe6v1S87$6}iMLF=&}Oc{2UMG1-Ocw1xi-I8W4t(?RxlCeiQX~R#B~VK zsry8gb$5-q)?BT`FM@snl;wj%TDJIIBWG3Ya!c>9_k&LKyehJCF}U|VP>1kJFVw|o zxYSJM5T<~tiVhl$%rM#CffHl9nI`^aXIHu>L1JQ8*PIqp-L!6kXD0-qcrm?%zTUD* znhItAo+V1tPt7@>LJIoje$DdJF3V_gyl3dDrW8Q>a*=3f|_eyq=eGpeqw2!i%#4WVPygg(3U zKC{d}Z$jB+zmU;^9+WluwF@-Pi>GJkV?;#QmoT;;RH26T#Pg2Oqk^&o|UzuF44qQd-#A6w-C3+TCI0uG<1jFo$;D&z%O zV!Ntx6yrAHeQ3%hTdCeZ33bWn1cO=ppM~##$xpC`RHJ5-hm<*UjK3|kfsw%M(NL#n z>*DYlX6dA3NG^Pn+)&ShQ6Uz^-A!F9zb#Z>kQu(F+AxVn8Y|%AYM?K=deIQ`sge4O z>r=30U+P=_4b*jqpAc~!&BQamIm3Jx{W6!o4eu~Qwg%1_jo+*Hrt^4OxZSL^1^J3S z<_TD$Y*7*26x4QDTVkPIFQ4lQcoM)_4L>LyNt-?d#;EXhbykaegdMD9K;I_x?kwxZ zg2*l_bs>~?V80spujG2$QO^VnZh|=9!*`pKREb{XS{p;X?GHq~ezubX zQcQY-WftMd*+QrgPYXbQUlZO-ivLa+Ss!VZoXlV=wZtimP9zRj>x`}tsHj{q|978T z+74yxxHmPoURE%8QOx<;>hy(GaM%;|&1hUe36xB}d3;c+1L~xT<1Y}Ozo%VM&$U^zGln{rZCg%NL9A+HsmS963B$snp0nN>t_f zqir+gh=^{CKP!?FnNway_iU$tB{t4U^LiC^u9rA<7|wxo-3@C!e$ zm#rm#U9PDLuF;voRN7yyMh(f<-(B~Iv90OJk7h3YPJaI+({9J902qpCVC zX+9<_$o$8m>nYg^b|lH0*Zao9$8BiRFE>x3jvNxRuTykwB}#sA&11kDc`$Svl{3+; zsG@*#q~-d7>m{Uggd*T&v-GkqS(?u_ssGWm@~;=o=C!RpAQqoUG3%xa*D^dP#ni5% zwwX9+XiB`Fn$wjcm$U$DpzpL?yLA!GFe#)~HUAZw4HTJ_hkU9=>$Si+hPizliGSAD zHy^&Z>qeC(PkboTtvSi3IwAFCl%O?%0HUmu^1|d>^rD#Zfqw_169}tfMdHPFq|L1N zebZ~WKrr?k(qihoZ5a;$7@S|HN_m>9jep)j3g|^zBTPHzzxBcrF})7K`bf=ZS!zI; z`5cRU#4+Y=WpeFj6~8SiOz1UY-`~*2b#Y`@KfpC^o0@kVC88PeRmit0EFb%I$MQN@HBHbU86qqI&=Tjrxh@bDOGu+u@bP@{Hg54u2^;d<%ABNF8fN{A+ z)zm!;>DN{XU{Ga&qor^|tR_*_guzz}p&aUH1ai>JCIF@j6Fddjf$((uhchk5d1kQ` zv1}v|(bZvG;wZQ>VV~i+Dpq>5#jTdHUCv*0hqw-=s#+#cd8F9&EEv)hFJ|oI7AnOj zw62TN@zCOkNbgUgA?DYwiple1s{zVJ1qiRPp&e26{oCvv1B{iHT|FA2bF3ra+TKJ% zpfi}B7+4ChT)TZ`kX%1ftf3qfuo#)*z@Fv1mYtiHp_D~N`VbMU+4(AbdY7>2qyPrb zp4p$H`@<`t)URtB&FS*o`blA-P!~+r#a5U#r?nG8VC=r+>U1SuPb<|s+^8kohB#SN zk9yPQyZ0D!$l7%*418YhjlJ*W*^&*2gi6(Agoxr>kI~(1-nR>_Z>~*22q+w7F#o>M z7lno6oGPwgtT#KJjDF?BBNm-2G>iV9lj8b-^IhW$Khz0EU-Xgy^TGy0$^045Q+0oT z#c^G-uS}ZGs|a`*MxWNU)bI~2*}vowM-78n|Lw1wXxGN*n*{MT0tmdL#xZSgeXM(U zfZ~t>6!s`rEKF5VfjvG40`l$6)M~&4m#sloEs8W+h~7U-Zin`b0_0+0BcTjDn<0_T z4IwoUf)_EO=z;JfiC1}s@EqR?wMe(+nEvEH5E5sT6YO^5wkSjSHo0W`$Ye9GUe+i8 zL!-9O<~%z^4ZGmNad%fZbR8nycjvJfnIPRFrME4R^F~$WR8A&=86ei_q@#Y#v@#=4 zn2>Q%+9D%s<%HmY`qFr91lLcGZ6>O9N_`dRx2KfiZ#i>Ou_a$7$B_7wXii$nA+PRq zJpF)5KjUl;0n4BePDRQjMz`%1j-G3m&j(2_+q z58_|6b4-O94=iIHe4VkLD@{Xoq*AMu11m&-PD~N=cePPPqVV9f`y+~+_op2ySt~ei>irmvrV=9marp)(+p{dyrYmKv^sSo9BH2`N}Bf z0p0!Dsgw|LqTu5o2U)+{@I$g!@>KmRlkvDH2%8Ox;(dP)TShhYhe;18SRi!fH#52A zK>B^3ogV?-*0=gN*|-@k`1qzwnJBkL$ooNxVAe6|)WwJ9TTI22ubyKP?^2hk`3xu_ z3kv(Lb^S2c{Y3#@qM(_c>U>(||4#Wn>>BbdbEd6Ft2teU0dHj=N! zq%$`JYSZuz7N2wFgf&E5iT(~-uxJ(JDMkGf2#0bYw&$_8=m9ph7$ib z77Xh5)(OvDuF^LTXey=o^zFvxESa=-nkU3zZlN}k@TbB@8k@D~q?#i9o7<_$(NJoy z=id9z57pq2PcLr*++yRsl`fy=@?$g&hYtN!EyS;05K6hcZHwn8WZTAMitNYT9uslH zSvt9VLh}_G}YhhD!uI z-~(~__q-%T>#wmdDh@JSGy}TDI+Sk(R4HA{qj1Sle?EpsF@NmN)hXF6j)6eToM?pC ziE{km7qRwVXep89sl(gFfyEu!ZARk7>TK%@obO3@agOo?Jt0&{53P7kVjQj&)5n2% zareEsMqbdmr80LI)z*67Y`Jc%|ZQAlk-H& zU~=kM4X4Uz2hV7$m9<}&CM(S10humcS?&pk97;nTT$0HJ~Ham*0m3A?md$0PuC#6tU>Vi;4pqMp%UE=y5a>sj8}ZG>n!lq z@Vda==A}O}jJmxRNw0xBb2_^c+nshRO#MXGzYOiJ(S<#8_U@8mdUSJ$^JC9q9{KX2 zPvn1l3phTOjkrrV5~tH&%m8CLd%@_G6vKs0^_XlEQl5h3C;mlLH*w@wc;t!nizEse;lOp`H;CO%n8psw)c9^T=?z>MzIE71oN+G z;UmCRoge72IVwRheQVWTXa7(YG@;v*8t$jcn&|#jOcK4~sg=BFJ+mHkVZq8y!`;nl zVd#AT`o2c;?3ZF=xu2E>#cV_X4mm&*=)T$t?oDLYoo`HFg+dL9)dF znK*gcf%G&dVQ!h?Xi?6>8o(tXtB2uh2GiLm>W|fmk7Ds{)wgp@1mjP?U)GX}$HjmH z-2>YU9WuA)%s#53j+y*Y_{xUx&CU-6!3gIc35+fPJ$1j5z@5aM)CNZDzv{%Gs$*F- zigtCtacaFV|Ex)#s?cD_!j~4hMaS!|kO=dy30yCdqmkGO6U6T&O)IWy3{(SnG`yxD z*Fs6Hv10yOg``q$LA>>`5})NrC9)#CelMX8K5U?aq4r(mRQIHri5Lb0B}661(>zDv z=T$?-_@mCuUy>OKz{Oaau2j4Me! zsfq>(DW7depY3$R2M)lyyaHsE*NWU) z#b%=GuJ~5Ea>1~zCRoCf`+m_1q0)0SH-gV<8MNql!Te8WvmJ3Kd z?#J2k>f)@o4TGC@PBV|tkZvZV&c`_sRs&qi?nE%^HiY59P^tFVCg>R1??<5y8q+rZD-3z%LaBFkcG5Tf z?o!Np34z_7Ts9X$c0H?+=Pd6@w%shQHE-zRoBBdZ(<{GLe*~_2)yCyHWEU%qR z0&G*#FVA!7Rpf+C{B|&SFvqxlm)SMUVT6N^sjA({@RpudsC1z=IdV@gA=IC~P* z;885Qps}~(wa!zML13~m(!uw>Q6N3u5{7&q8D$J%0LmO-JuT)P2FUV{WAk^5V#gv} zfGY8Zn!$jZUUu!*Ve`_CTOQ4b@D2Mo zS_JKvZqFBP?!*|#3A#)vI&g=k;vwsvLxW%H27TTA_&L|1V{}tdF;4gbBtSPRe3>Uc zHfKJIcKQhp5^+~~wi}r%N=rdaW_$l6j{&V9{fCx4njoD#6~l)`OREgSG{ER{A=wKg z8IwyKGmI?5I`jEQiPJ&|vR@Ve7JS#29U1_dUW(T#|9Zw|31j>8W07F$(wQe0~qFdbAqD&SG_4smd^~q_I#sJP9+5Jq(R=K&z!<6dF6Oer{(}SPC_~j zebH`?J#8ntt0tp5gid03dwenZ^gl4{&EhgD6ApbdMZCpAu;GJ@i(EggyxG@WsY!z% zv0Pc3w7aQFhlm%^$1x|JiT(Z8ev^Ze6iO3EP_^`tY*F!?x@0yQ2wz$j>-bS;?+(c_=}4cEWf-2{{2^@tQ& z`xRiCjd7hN>8Z8$q{eS+m!CB=Iuww+(ff2jGknMpS6~gji#O<4l_1Fw@vM{Dv%==w z%b%ZB`1EW|t)xnB><{_lOD&Nv?ZcoB>Oq#1oAm8ekNR$pjJDK(dM6#MgO>Agja7Rh572$@o62yeMDk>cKht=a4 zunK`;#A(1b-jeWvRttS|a0;jtccl`4U+XaGm>w2e0dBRcJut_l{Dc9M{jrPzU z+X>iVp_Hn!gv@yBQ607wi6#=ZyIKrwtoVPkV-0^|BaMI_VI9zuYLX$WBs2t54-58A z;!sFosBtlb179n!c1vVrAP-CjO7W6^E1$`X&%YNxa5WK~{IB5gvwtkeKc$kJUJYn zgz_-74@niEmL&Y#jKTqDX>k~R@825#;VxT78Ll1SArGpYV`kxs;%V<5XxKh=zVoLp))S;g3sP zs9WjzZub zEmhE7{Hq5&00uDlW8y*12&EK%>i40)?G7jo3YQD$ciD6!l0IDeAgx3A*m+S>jeDW% z2G=wEPt5*Z)xh|VKcpA~9&acP13aDG?x~}?J&WK5trRclFb;}oqWn}vrP|<hU* zGeRosRWGswbqtPqG)y{!t}qr}Qvf0lBNRu64#$p3x7$@}C5<9SS4-B6`oJ;99(&UQ zoTLguHvahF0qzdZIl3dVjo+pHpsbqd5YMTxyFH7o5g9?yNfPbiH7H^Ss-OuYu0!sF zEdW+n37x~=_=%Z}(vxkZaSXFcZiiC^_%9NiP7C@claH-!2=C1_{e+)x5?On7&9Awe zysGhy6s<28TTRPO^^)Fl3XS^|nEFQjW{ZRSBkDzykgZ1A2OjlM z$=%>rtqoYb(qD#kHoYmd`QZ*PZwRVQll+&P`i+?B(eNJOULaJ$lAz>;uoh&K7nW#6 z7}Ca{E^a+S>*Kx6;3WIK6Zlid!vm0t7L%t)1`qp!=w?SuPQmk^ZRfYprGGinC2Yk9 zJ9{pz$#1_HG5q4ctJB1U$(+F6FjIn_6D@n~BOHqMGjo{9Ys_=$9j)&IT_j>??N<#m z0yw%^pb156M&N(!s{q9ZqhHXC8R=#rZB;$Ya8bHCwl1E=UYXBn!R(t-UlISi+q0A@$F20 zcJ!A2%;{MjdGg;*`O(YP>YI?+UxoL!`gk-s1vC+AfWppfs#}HnwYzUTjd_NPZspJ| zi-8YXoYFV=5r7hW@cy&iJi8QAt>PjwernaeeCX~dd2)`i*X~)U%Y)ihQvVwNSZ&Gu=L8O*iCYwN!dohnr|#K<^H75P5=0>lSt5-=mQUw-g`W+)-NW>?Tv+Du?GUR@ zwRoZ@89WlPU7mf$fi*=_3C?H*%VH)6giD@Nqt|#Sx3QPB62|m&heJMJu~8Y?zsop+ z)*V)-nkCrVa@DB;Fr&>4(qG3 z*OzVB#xb;=bQ+YWYg2ouGOj1{AEp6=Qlw?{UA0qF&qirTgIY=H+26?ni_&C^qnXlQ zpu9_hAsG1~_OzpVS}{OQ{!gwSS4~C&r;f}3;djyXh}+DoKZxB11^zx3heS;buLjI{ z{+3}D_o-CqpOrA+2g3mE)E6qf)JT76Y*0Gdl_B$YF*{-Gn6P!`=uJIwd8np|-Z)mf zQ5Qz2ugxaRh4n9J_tKycjs!(|9Jt} zlt+5l@%fwjd zZt_N@^CMs9>ZKx964^q~fHGfW_c%IpV)#jf>H+#(KU)J^v`%5#6Zcp*9#=l&IxrG4 zRvE27sl6tj>n0uV|3NlD;*4O3*ufhltm{AN@oB<8(Ly|)`O*3hCN5KdI^33GRfsnA zw?h11R_`1#p8UNMoR)pM9GC;c;mk0yw@4oM4b_4C?} z>jYI5T8yQ_X#XWZ!wgN}>CiBna2rm8OFmQt7pX6@zu&O`WH_f0%LblXMdra7BmC{0 zwM3A0Hv2f+ZtcnC9oIuz5=hz%g)}(=mC+;)uM+3yFSyhiOqg$)8t6|A5v>rTm8+;? z1^m6Sx(f{W5l3r8xV6dj?nx!Wfzm9MAKm`8U5&M6DhkZRt}J+ysiBUukd59@!i&60 z4Ly&9t33O7n_=XQ1UOEs;`b`K21`7e$_qS*rEt~yr}Lv{Vnrq31EJ$$NWDNusJEu3VPhl z*lz>XM{iR{fLobs@2_Zi*L6lp?FrcP$>TU;5}l*%V#RKcVOHDe!`N2B;0RBqFVraY zez(t2G_)l9;exC>nprwYsp4B0{%$sU05b_`+T!ERz?^#Ih#-X~hRnf=G6RJ|n}=;` z*EQOviU0s9Q%y`gCGiD?OHX4muO6T=Kx=B`paQ6v9l*0DV;2R`iYv;-zlE>KJTy_X zC84Pfxz(vjk@-Xk>!EHi?dl0|(Ef@};I1gu0FT%1)o(EG^5iv)^80?-8L2(Rl|e7E z3RXiZcyCuD9R(>5Z!0|eccA!GS4GZSwKBKg($3YuRDE$zvp@1`%OcPk&yItWRFLcV895=W+;63209nc=N{=c^)lIXsd>>OS$6S(DSK(ZDZ4M zG!rkC9;0#X`nzOG)2tA=_@gQz+XYBFj~7l)8HJRUZt2w;?t5leSP-zWvl1th`dn4; zQ)mtLNBQA0du{%skLCj^kEitQe~aeB9dLl~^@~=-`reg?r~a?=Xp8q=(dyetEX!F8&$ZPB!@1g{<5 z_&(lGuBp?2loeVynvyc`Q@;s~98;fUX(8EN#Q-?@y^_iEuL;{f*l|;zL?4ISiY8zbEzWK^n3cW&%y5%b898&S*SRd+X+ zp9)jL&lAuy<8pY$M&KY5=Ttzh++_4IoBj7Di*$WG9BxT-J}>-^Koq;Qb3a%C0IJot zH<(Gylm#pHe(x3FMOo0#DoAICst2Z9i^L!B3IUqGR2Z!amF$<7AdEiB8Ztyqkk&8W z*{~K7)&s|w<#bYJ;$CDKzYwwb5a2{vf_XBMS$R^es6#K5JYKXt_dQiVB9fF~3F(}^ zr<&8Y%8huq)8x^hM9thZtnF!gVFn3Km%HyTFyjfNr8zhg!6&;1knK)ahoV0<-!J#s zC5>guP(3ykg1*bn;Sc*I(9WneeR;P58_P=nu2A{t`geBeJJQ*``#F=&J{oK#%QxOK z6SI4z$q^tt5z20riDW7Qm<9i=NrnI-kB$F*%tQ-zq~`Y*p7-o~7C0uwu099dgcHIs za4Qt9k9!a1hRjjRfGRCfo1p@`d6pfrA-ys4 z_Ws^rLNMf&%^>hbaRF(G+Cf%-66Cg4IQzoUT+Xx4!xp7miTa6K>da@pTa2UKVv*@g z_(P*G-5L4Q6lZVL3b}Voecax2Vs#QyJH5Z^VtxFCm^FIwn^v1z*6)wFdf{ks(}+Po zi6qGza6+Oh_eP`oxIC>q%dWHKFCS46z>%MW7?$T$PH|20pei;XUHp{@ni?92W+gJ} zjWlTmd`i$Ql&fVN#RyVwpa!TpDSYXTXXTMC`C(Z3wv+bA|3?j8+j@2MLHT#;1`F8$ zMoxtLcRT=E(8cFI&8N1yz7$C0xo)K>E|frVquOE=bC7{-=vH<4_SW)S7sm9>xt@E$ z*Qvr$vkZ)1(c)t`k_QhuHD72MI^WNrm8kdDF^awG${#HZ94j@)Rruz$kkZ`T|6VPU zCUmrlDMds&+j1b-+ig~RRu`O4v#NyHgdvn_z(jKRPzV+WP|>zNd08pd+g)M3d_g`` zT}ePFiZp8rNVK!V`(lU*Y&!4x_KYJ#91A7X3BJ`*qX#_ zbUK>h(qlyS3;UJs&M z-%FH`*EB*kbT2x8vhK5I)xV-|KR7Z$_A$`Yci%)O89z?FGIYDGwcGDUQZx!%#^ilU z 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 literal 0 HcmV?d00001 diff --git a/web/public/full-logo.webp b/web/public/full-logo.webp new file mode 100644 index 0000000000000000000000000000000000000000..f654aae8acb9438aa69440ac323c0b3ba0f42e71 GIT binary patch literal 25152 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

dt7%BcXjNO|& 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 { + const { i18n } = useTranslation(); + const navigateTo = useNavigateTo(); + const [mode, setMode] = useState<"light" | "dark">("light"); + const workspaceProfile = workspaceStore.state.profile; + const userSetting = userStore.state.userSetting; + const workspaceGeneralSetting = workspaceStore.state.generalSetting; + + // Redirect to sign up page if no instance owner. + useEffect(() => { + if (!workspaceProfile.owner) { + navigateTo("/auth/signup"); + } + }, [workspaceProfile.owner]); + + useEffect(() => { + const darkMediaQuery = window.matchMedia("(prefers-color-scheme: dark)"); + const handleColorSchemeChange = (e: MediaQueryListEvent) => { + const mode = e.matches ? "dark" : "light"; + setMode(mode); + }; + + try { + darkMediaQuery.addEventListener("change", handleColorSchemeChange); + } catch (error) { + console.error("failed to initial color scheme listener", error); + } + }, []); + + useEffect(() => { + if (workspaceGeneralSetting.additionalStyle) { + const styleEl = document.createElement("style"); + styleEl.innerHTML = workspaceGeneralSetting.additionalStyle; + styleEl.setAttribute("type", "text/css"); + document.body.insertAdjacentElement("beforeend", styleEl); + } + }, [workspaceGeneralSetting.additionalStyle]); + + useEffect(() => { + if (workspaceGeneralSetting.additionalScript) { + const scriptEl = document.createElement("script"); + scriptEl.innerHTML = workspaceGeneralSetting.additionalScript; + document.head.appendChild(scriptEl); + } + }, [workspaceGeneralSetting.additionalScript]); + + // Dynamic update metadata with customized profile. + useEffect(() => { + if (!workspaceGeneralSetting.customProfile) { + return; + } + + document.title = workspaceGeneralSetting.customProfile.title; + const link = document.querySelector("link[rel~='icon']") as HTMLLinkElement; + link.href = workspaceGeneralSetting.customProfile.logoUrl || "/logo.webp"; + }, [workspaceGeneralSetting.customProfile]); + + useEffect(() => { + const currentLocale = workspaceStore.state.locale; + // This will trigger re-rendering of the whole app. + i18n.changeLanguage(currentLocale); + document.documentElement.setAttribute("lang", currentLocale); + if (["ar", "fa"].includes(currentLocale)) { + document.documentElement.setAttribute("dir", "rtl"); + } else { + document.documentElement.setAttribute("dir", "ltr"); + } + }, [workspaceStore.state.locale]); + + useEffect(() => { + let currentAppearance = workspaceStore.state.appearance as Appearance; + if (currentAppearance === "system") { + currentAppearance = getSystemColorScheme(); + } + setMode(currentAppearance); + }, [workspaceStore.state.appearance]); + + useEffect(() => { + const root = document.documentElement; + if (mode === "light") { + root.classList.remove("dark"); + } else if (mode === "dark") { + root.classList.add("dark"); + } + }, [mode]); + + useEffect(() => { + if (!userSetting) { + return; + } + + workspaceStore.state.setPartial({ + locale: userSetting.locale || workspaceStore.state.locale, + appearance: userSetting.appearance || workspaceStore.state.appearance, + }); + }, [userSetting?.locale, userSetting?.appearance]); + + // Load theme when user setting changes (user theme is already backfilled with workspace theme) + useEffect(() => { + if (userSetting?.theme) { + loadTheme(userSetting.theme); + } + }, [userSetting?.theme]); + + return ; +}); + +export default App; diff --git a/web/src/components/ActivityCalendar/ActivityCalendar.tsx b/web/src/components/ActivityCalendar/ActivityCalendar.tsx new file mode 100644 index 0000000..125e5f6 --- /dev/null +++ b/web/src/components/ActivityCalendar/ActivityCalendar.tsx @@ -0,0 +1,180 @@ +import dayjs from "dayjs"; +import { observer } from "mobx-react-lite"; +import { memo, useMemo } from "react"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { cn } from "@/lib/utils"; +import { workspaceStore } from "@/store"; +import type { ActivityCalendarProps, CalendarDay } from "@/types/statistics"; +import { useTranslate } from "@/utils/i18n"; + +const getCellOpacity = (ratio: number): string => { + if (ratio === 0) return ""; + if (ratio > 0.75) return "bg-destructive text-destructive-foreground"; + if (ratio > 0.5) return "bg-destructive/70 text-destructive-foreground"; + if (ratio > 0.25) return "bg-destructive/50 text-destructive-foreground"; + return "bg-destructive/30 text-destructive-foreground"; +}; + +const CalendarCell = memo( + ({ + dayInfo, + count, + maxCount, + isToday, + isSelected, + onClick, + tooltipText, + }: { + dayInfo: CalendarDay; + count: number; + maxCount: number; + isToday: boolean; + isSelected: boolean; + onClick?: () => void; + tooltipText: string; + }) => { + const cellContent = ( +

0 && "cursor-pointer hover:scale-110", + )} + onClick={count > 0 ? onClick : undefined} + > + {dayInfo.day} +
+ ); + + if (!dayInfo.isCurrentMonth) { + return ( +
+ {dayInfo.day} +
+ ); + } + + return ( + + + +
{cellContent}
+
+ +

{tooltipText}

+
+
+
+ ); + }, +); + +export const ActivityCalendar = memo( + observer((props: ActivityCalendarProps) => { + const t = useTranslate(); + const { month: monthStr, data, onClick } = props; + const weekStartDayOffset = workspaceStore.state.generalSetting.weekStartDayOffset; + + const { days, weekDays, maxCount } = useMemo(() => { + const yearValue = dayjs(monthStr).toDate().getFullYear(); + const monthValue = dayjs(monthStr).toDate().getMonth(); + const dayInMonth = new Date(yearValue, monthValue + 1, 0).getDate(); + const firstDay = (((new Date(yearValue, monthValue, 1).getDay() - weekStartDayOffset) % 7) + 7) % 7; + const lastDay = new Date(yearValue, monthValue, dayInMonth).getDay() - weekStartDayOffset; + const prevMonthDays = new Date(yearValue, monthValue, 0).getDate(); + + const WEEK_DAYS = [t("days.sun"), t("days.mon"), t("days.tue"), t("days.wed"), t("days.thu"), t("days.fri"), t("days.sat")]; + const weekDaysOrdered = WEEK_DAYS.slice(weekStartDayOffset).concat(WEEK_DAYS.slice(0, weekStartDayOffset)); + + const daysArray: CalendarDay[] = []; + + // Previous month's days + for (let i = firstDay - 1; i >= 0; i--) { + daysArray.push({ day: prevMonthDays - i, isCurrentMonth: false }); + } + + // Current month's days + for (let i = 1; i <= dayInMonth; i++) { + const date = dayjs(`${yearValue}-${monthValue + 1}-${i}`).format("YYYY-MM-DD"); + daysArray.push({ day: i, isCurrentMonth: true, date }); + } + + // Next month's days + for (let i = 1; i < 7 - lastDay; i++) { + daysArray.push({ day: i, isCurrentMonth: false }); + } + + const maxCountValue = Math.max(...Object.values(data), 1); + + return { + year: yearValue, + month: monthValue, + days: daysArray, + weekDays: weekDaysOrdered, + maxCount: maxCountValue, + }; + }, [monthStr, data, weekStartDayOffset, t]); + + const today = useMemo(() => dayjs().format("YYYY-MM-DD"), []); + const selectedDateFormatted = useMemo(() => dayjs(props.selectedDate).format("YYYY-MM-DD"), [props.selectedDate]); + + return ( +
+ {weekDays.map((day, index) => ( +
+ {day} +
+ ))} + {days.map((dayInfo, index) => { + if (!dayInfo.isCurrentMonth) { + return ( + + ); + } + + const date = dayInfo.date!; + const count = data[date] || 0; + const isToday = today === date; + const isSelected = selectedDateFormatted === date; + const tooltipText = + count === 0 + ? date + : t("memo.count-memos-in-date", { + count: count, + memos: count === 1 ? t("common.memo") : t("common.memos"), + date: date, + }).toLowerCase(); + + return ( + onClick?.(date)} + tooltipText={tooltipText} + /> + ); + })} +
+ ); + }), +); + +ActivityCalendar.displayName = "ActivityCalendar"; diff --git a/web/src/components/ActivityCalendar/index.ts b/web/src/components/ActivityCalendar/index.ts new file mode 100644 index 0000000..925dcc7 --- /dev/null +++ b/web/src/components/ActivityCalendar/index.ts @@ -0,0 +1 @@ +export { ActivityCalendar as default } from "./ActivityCalendar"; diff --git a/web/src/components/AppearanceSelect.tsx b/web/src/components/AppearanceSelect.tsx new file mode 100644 index 0000000..5c67985 --- /dev/null +++ b/web/src/components/AppearanceSelect.tsx @@ -0,0 +1,51 @@ +import { SunIcon, MoonIcon, SmileIcon } from "lucide-react"; +import { FC } from "react"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { useTranslate } from "@/utils/i18n"; + +interface Props { + value: Appearance; + onChange: (appearance: Appearance) => void; +} + +const appearanceList = ["system", "light", "dark"] as const; + +const AppearanceSelect: FC = (props: Props) => { + const { onChange, value } = props; + const t = useTranslate(); + + const getPrefixIcon = (appearance: Appearance) => { + const className = "w-4 h-auto"; + if (appearance === "light") { + return ; + } else if (appearance === "dark") { + return ; + } else { + return ; + } + }; + + const handleSelectChange = async (appearance: Appearance) => { + onChange(appearance); + }; + + return ( + + ); +}; + +export default AppearanceSelect; diff --git a/web/src/components/AttachmentIcon.tsx b/web/src/components/AttachmentIcon.tsx new file mode 100644 index 0000000..1469f64 --- /dev/null +++ b/web/src/components/AttachmentIcon.tsx @@ -0,0 +1,108 @@ +import { + BinaryIcon, + BookIcon, + FileArchiveIcon, + FileAudioIcon, + FileEditIcon, + FileIcon, + FileTextIcon, + FileVideo2Icon, + SheetIcon, +} from "lucide-react"; +import React, { useState } from "react"; +import { cn } from "@/lib/utils"; +import { Attachment } from "@/types/proto/api/v1/attachment_service"; +import { getAttachmentType, getAttachmentUrl } from "@/utils/attachment"; +import { PreviewImageDialog } from "./PreviewImageDialog"; +import SquareDiv from "./kit/SquareDiv"; + +interface Props { + attachment: Attachment; + className?: string; + strokeWidth?: number; +} + +const AttachmentIcon = (props: Props) => { + const { attachment } = props; + const [previewImage, setPreviewImage] = useState<{ open: boolean; urls: string[]; index: number }>({ + open: false, + urls: [], + index: 0, + }); + const resourceType = getAttachmentType(attachment); + const attachmentUrl = getAttachmentUrl(attachment); + const className = cn("w-full h-auto", props.className); + const strokeWidth = props.strokeWidth; + + const previewResource = () => { + window.open(attachmentUrl); + }; + + const handleImageClick = () => { + setPreviewImage({ open: true, urls: [attachmentUrl], index: 0 }); + }; + + if (resourceType === "image/*") { + return ( + <> + + { + // Fallback to original image if thumbnail fails + const target = e.target as HTMLImageElement; + if (target.src.includes("?thumbnail=true")) { + console.warn("Thumbnail failed, falling back to original image:", attachmentUrl); + target.src = attachmentUrl; + } + }} + decoding="async" + loading="lazy" + /> + + + setPreviewImage((prev) => ({ ...prev, open }))} + imgUrls={previewImage.urls} + initialIndex={previewImage.index} + /> + + ); + } + + const getAttachmentIcon = () => { + switch (resourceType) { + case "video/*": + return ; + case "audio/*": + return ; + case "text/*": + return ; + case "application/epub+zip": + return ; + case "application/pdf": + return ; + case "application/msword": + return ; + case "application/msexcel": + return ; + case "application/zip": + return ; + case "application/x-java-archive": + return ; + default: + return ; + } + }; + + return ( +
+ {getAttachmentIcon()} +
+ ); +}; + +export default React.memo(AttachmentIcon); diff --git a/web/src/components/AuthFooter.tsx b/web/src/components/AuthFooter.tsx new file mode 100644 index 0000000..2a449bb --- /dev/null +++ b/web/src/components/AuthFooter.tsx @@ -0,0 +1,28 @@ +import { observer } from "mobx-react-lite"; +import { cn } from "@/lib/utils"; +import { workspaceStore } from "@/store"; +import AppearanceSelect from "./AppearanceSelect"; +import LocaleSelect from "./LocaleSelect"; + +interface Props { + className?: string; +} + +const AuthFooter = observer(({ className }: Props) => { + const handleLocaleSelectChange = (locale: Locale) => { + workspaceStore.state.setPartial({ locale }); + }; + + const handleAppearanceSelectChange = (appearance: Appearance) => { + workspaceStore.state.setPartial({ appearance }); + }; + + return ( +
+ + +
+ ); +}); + +export default AuthFooter; diff --git a/web/src/components/BrandBanner.tsx b/web/src/components/BrandBanner.tsx new file mode 100644 index 0000000..4746fef --- /dev/null +++ b/web/src/components/BrandBanner.tsx @@ -0,0 +1,27 @@ +import { observer } from "mobx-react-lite"; +import { cn } from "@/lib/utils"; +import { workspaceStore } from "@/store"; +import UserAvatar from "./UserAvatar"; + +interface Props { + className?: string; + collapsed?: boolean; +} + +const BrandBanner = observer((props: Props) => { + const { collapsed } = props; + const workspaceGeneralSetting = workspaceStore.state.generalSetting; + const title = workspaceGeneralSetting.customProfile?.title || "Memos"; + const avatarUrl = workspaceGeneralSetting.customProfile?.logoUrl || "/full-logo.webp"; + + return ( +
+
+ + {!collapsed && {title}} +
+
+ ); +}); + +export default BrandBanner; diff --git a/web/src/components/ChangeMemberPasswordDialog.tsx b/web/src/components/ChangeMemberPasswordDialog.tsx new file mode 100644 index 0000000..c4537f6 --- /dev/null +++ b/web/src/components/ChangeMemberPasswordDialog.tsx @@ -0,0 +1,115 @@ +import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { userStore } from "@/store"; +import { User } from "@/types/proto/api/v1/user_service"; +import { useTranslate } from "@/utils/i18n"; + +interface ChangeMemberPasswordDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + user?: User; + onSuccess?: () => void; +} + +export function ChangeMemberPasswordDialog({ open, onOpenChange, user, onSuccess }: ChangeMemberPasswordDialogProps) { + const t = useTranslate(); + const [newPassword, setNewPassword] = useState(""); + const [newPasswordAgain, setNewPasswordAgain] = useState(""); + + useEffect(() => { + // do nth + }, []); + + const handleCloseBtnClick = () => { + onOpenChange(false); + }; + + const handleNewPasswordChanged = (e: React.ChangeEvent) => { + const text = e.target.value as string; + setNewPassword(text); + }; + + const handleNewPasswordAgainChanged = (e: React.ChangeEvent) => { + const text = e.target.value as string; + setNewPasswordAgain(text); + }; + + const handleSaveBtnClick = async () => { + if (!user) return; + + if (newPassword === "" || newPasswordAgain === "") { + toast.error(t("message.fill-all")); + return; + } + + if (newPassword !== newPasswordAgain) { + toast.error(t("message.new-password-not-match")); + setNewPasswordAgain(""); + return; + } + + try { + await userStore.updateUser( + { + name: user.name, + password: newPassword, + }, + ["password"], + ); + toast(t("message.password-changed")); + onSuccess?.(); + onOpenChange(false); + } catch (error: any) { + console.error(error); + toast.error(error.details); + } + }; + + if (!user) return null; + + return ( + + + + + {t("setting.account-section.change-password")} ({user.displayName}) + + +
+
+ + +
+
+ + +
+
+ + + + +
+
+ ); +} + +export default ChangeMemberPasswordDialog; diff --git a/web/src/components/CreateAccessTokenDialog.tsx b/web/src/components/CreateAccessTokenDialog.tsx new file mode 100644 index 0000000..0c139c1 --- /dev/null +++ b/web/src/components/CreateAccessTokenDialog.tsx @@ -0,0 +1,139 @@ +import React, { useState } from "react"; +import { toast } from "react-hot-toast"; +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { userServiceClient } from "@/grpcweb"; +import useCurrentUser from "@/hooks/useCurrentUser"; +import useLoading from "@/hooks/useLoading"; +import { useTranslate } from "@/utils/i18n"; + +interface CreateAccessTokenDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + onSuccess: () => void; +} + +interface State { + description: string; + expiration: number; +} + +export function CreateAccessTokenDialog({ open, onOpenChange, onSuccess }: CreateAccessTokenDialogProps) { + const t = useTranslate(); + const currentUser = useCurrentUser(); + const [state, setState] = useState({ + description: "", + expiration: 3600 * 8, + }); + const requestState = useLoading(false); + + const expirationOptions = [ + { + label: t("setting.access-token-section.create-dialog.duration-8h"), + value: 3600 * 8, + }, + { + label: t("setting.access-token-section.create-dialog.duration-1m"), + value: 3600 * 24 * 30, + }, + { + label: t("setting.access-token-section.create-dialog.duration-never"), + value: 0, + }, + ]; + + const setPartialState = (partialState: Partial) => { + setState({ + ...state, + ...partialState, + }); + }; + + const handleDescriptionInputChange = (e: React.ChangeEvent) => { + setPartialState({ + description: e.target.value, + }); + }; + + const handleRoleInputChange = (value: string) => { + setPartialState({ + expiration: Number(value), + }); + }; + + const handleSaveBtnClick = async () => { + if (!state.description) { + toast.error(t("message.description-is-required")); + return; + } + + try { + requestState.setLoading(); + await userServiceClient.createUserAccessToken({ + parent: currentUser.name, + accessToken: { + description: state.description, + expiresAt: state.expiration ? new Date(Date.now() + state.expiration * 1000) : undefined, + }, + }); + + requestState.setFinish(); + onSuccess(); + onOpenChange(false); + } catch (error: any) { + toast.error(error.details); + console.error(error); + requestState.setError(); + } + }; + + return ( + + + + {t("setting.access-token-section.create-dialog.create-access-token")} + +
+
+ + +
+
+ + + {expirationOptions.map((option) => ( +
+ + +
+ ))} +
+
+
+ + + + +
+
+ ); +} + +export default CreateAccessTokenDialog; diff --git a/web/src/components/CreateIdentityProviderDialog.tsx b/web/src/components/CreateIdentityProviderDialog.tsx new file mode 100644 index 0000000..2d2b2e1 --- /dev/null +++ b/web/src/components/CreateIdentityProviderDialog.tsx @@ -0,0 +1,433 @@ +import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Separator } from "@/components/ui/separator"; +import { identityProviderServiceClient } from "@/grpcweb"; +import { absolutifyLink } from "@/helpers/utils"; +import { FieldMapping, IdentityProvider, IdentityProvider_Type, OAuth2Config } from "@/types/proto/api/v1/idp_service"; +import { useTranslate } from "@/utils/i18n"; + +const templateList: IdentityProvider[] = [ + { + name: "", + title: "GitHub", + type: IdentityProvider_Type.OAUTH2, + identifierFilter: "", + config: { + oauth2Config: { + clientId: "", + clientSecret: "", + authUrl: "https://github.com/login/oauth/authorize", + tokenUrl: "https://github.com/login/oauth/access_token", + userInfoUrl: "https://api.github.com/user", + scopes: ["read:user"], + fieldMapping: FieldMapping.fromPartial({ + identifier: "login", + displayName: "name", + email: "email", + }), + }, + }, + }, + { + name: "", + title: "GitLab", + type: IdentityProvider_Type.OAUTH2, + identifierFilter: "", + config: { + oauth2Config: { + clientId: "", + clientSecret: "", + authUrl: "https://gitlab.com/oauth/authorize", + tokenUrl: "https://gitlab.com/oauth/token", + userInfoUrl: "https://gitlab.com/oauth/userinfo", + scopes: ["openid"], + fieldMapping: FieldMapping.fromPartial({ + identifier: "name", + displayName: "name", + email: "email", + }), + }, + }, + }, + { + name: "", + title: "Google", + type: IdentityProvider_Type.OAUTH2, + identifierFilter: "", + config: { + oauth2Config: { + clientId: "", + clientSecret: "", + authUrl: "https://accounts.google.com/o/oauth2/v2/auth", + tokenUrl: "https://oauth2.googleapis.com/token", + userInfoUrl: "https://www.googleapis.com/oauth2/v2/userinfo", + scopes: ["https://www.googleapis.com/auth/userinfo.email", "https://www.googleapis.com/auth/userinfo.profile"], + fieldMapping: FieldMapping.fromPartial({ + identifier: "email", + displayName: "name", + email: "email", + }), + }, + }, + }, + { + name: "", + title: "Custom", + type: IdentityProvider_Type.OAUTH2, + identifierFilter: "", + config: { + oauth2Config: { + clientId: "", + clientSecret: "", + authUrl: "", + tokenUrl: "", + userInfoUrl: "", + scopes: [], + fieldMapping: FieldMapping.fromPartial({ + identifier: "", + displayName: "", + email: "", + }), + }, + }, + }, +]; + +interface CreateIdentityProviderDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + identityProvider?: IdentityProvider; + onSuccess?: () => void; +} + +export function CreateIdentityProviderDialog({ open, onOpenChange, identityProvider, onSuccess }: CreateIdentityProviderDialogProps) { + const t = useTranslate(); + const identityProviderTypes = [...new Set(templateList.map((t) => t.type))]; + const [basicInfo, setBasicInfo] = useState({ + title: "", + identifierFilter: "", + }); + const [type, setType] = useState(IdentityProvider_Type.OAUTH2); + const [oauth2Config, setOAuth2Config] = useState({ + clientId: "", + clientSecret: "", + authUrl: "", + tokenUrl: "", + userInfoUrl: "", + scopes: [], + fieldMapping: FieldMapping.fromPartial({ + identifier: "", + displayName: "", + email: "", + }), + }); + const [oauth2Scopes, setOAuth2Scopes] = useState(""); + const [selectedTemplate, setSelectedTemplate] = useState("GitHub"); + const isCreating = identityProvider === undefined; + + useEffect(() => { + if (identityProvider) { + setBasicInfo({ + title: identityProvider.title, + identifierFilter: identityProvider.identifierFilter, + }); + setType(identityProvider.type); + if (identityProvider.type === IdentityProvider_Type.OAUTH2) { + const oauth2Config = OAuth2Config.fromPartial(identityProvider.config?.oauth2Config || {}); + setOAuth2Config(oauth2Config); + setOAuth2Scopes(oauth2Config.scopes.join(" ")); + } + } + }, []); + + useEffect(() => { + if (!isCreating) { + return; + } + + const template = templateList.find((t) => t.title === selectedTemplate); + if (template) { + setBasicInfo({ + title: template.title, + identifierFilter: template.identifierFilter, + }); + setType(template.type); + if (template.type === IdentityProvider_Type.OAUTH2) { + const oauth2Config = OAuth2Config.fromPartial(template.config?.oauth2Config || {}); + setOAuth2Config(oauth2Config); + setOAuth2Scopes(oauth2Config.scopes.join(" ")); + } + } + }, [selectedTemplate]); + + const handleCloseBtnClick = () => { + onOpenChange(false); + }; + + const allowConfirmAction = () => { + if (basicInfo.title === "") { + return false; + } + if (type === "OAUTH2") { + if ( + oauth2Config.clientId === "" || + oauth2Config.authUrl === "" || + oauth2Config.tokenUrl === "" || + oauth2Config.userInfoUrl === "" || + oauth2Scopes === "" || + oauth2Config.fieldMapping?.identifier === "" + ) { + return false; + } + if (isCreating) { + if (oauth2Config.clientSecret === "") { + return false; + } + } + } + + return true; + }; + + const handleConfirmBtnClick = async () => { + try { + if (isCreating) { + await identityProviderServiceClient.createIdentityProvider({ + identityProvider: { + ...basicInfo, + type: type, + config: { + oauth2Config: { + ...oauth2Config, + scopes: oauth2Scopes.split(" "), + }, + }, + }, + }); + toast.success(t("setting.sso-section.sso-created", { name: basicInfo.title })); + } else { + await identityProviderServiceClient.updateIdentityProvider({ + identityProvider: { + ...basicInfo, + name: identityProvider!.name, + type: type, + config: { + oauth2Config: { + ...oauth2Config, + scopes: oauth2Scopes.split(" "), + }, + }, + }, + updateMask: ["title", "identifier_filter", "config"], + }); + toast.success(t("setting.sso-section.sso-updated", { name: basicInfo.title })); + } + } catch (error: any) { + toast.error(error.details); + console.error(error); + } + onSuccess?.(); + onOpenChange(false); + }; + + const setPartialOAuth2Config = (state: Partial) => { + setOAuth2Config({ + ...oauth2Config, + ...state, + }); + }; + + return ( + + + + {t(isCreating ? "setting.sso-section.create-sso" : "setting.sso-section.update-sso")} + +
+ {isCreating && ( + <> +

{t("common.type")}

+ +

{t("setting.sso-section.template")}

+ + + + )} +

+ {t("common.name")} + * +

+ + setBasicInfo({ + ...basicInfo, + title: e.target.value, + }) + } + /> +

{t("setting.sso-section.identifier-filter")}

+ + setBasicInfo({ + ...basicInfo, + identifierFilter: e.target.value, + }) + } + /> + + {type === "OAUTH2" && ( + <> + {isCreating && ( +

+ {t("setting.sso-section.redirect-url")}: {absolutifyLink("/auth/callback")} +

+ )} +

+ {t("setting.sso-section.client-id")} + * +

+ setPartialOAuth2Config({ clientId: e.target.value })} + /> +

+ {t("setting.sso-section.client-secret")} + * +

+ setPartialOAuth2Config({ clientSecret: e.target.value })} + /> +

+ {t("setting.sso-section.authorization-endpoint")} + * +

+ setPartialOAuth2Config({ authUrl: e.target.value })} + /> +

+ {t("setting.sso-section.token-endpoint")} + * +

+ setPartialOAuth2Config({ tokenUrl: e.target.value })} + /> +

+ {t("setting.sso-section.user-endpoint")} + * +

+ setPartialOAuth2Config({ userInfoUrl: e.target.value })} + /> +

+ {t("setting.sso-section.scopes")} + * +

+ setOAuth2Scopes(e.target.value)} + /> + +

+ {t("setting.sso-section.identifier")} + * +

+ + setPartialOAuth2Config({ fieldMapping: { ...oauth2Config.fieldMapping, identifier: e.target.value } as FieldMapping }) + } + /> +

{t("setting.sso-section.display-name")}

+ + setPartialOAuth2Config({ fieldMapping: { ...oauth2Config.fieldMapping, displayName: e.target.value } as FieldMapping }) + } + /> +

{t("common.email")}

+ + setPartialOAuth2Config({ fieldMapping: { ...oauth2Config.fieldMapping, email: e.target.value } as FieldMapping }) + } + /> +

Avatar URL

+ + setPartialOAuth2Config({ fieldMapping: { ...oauth2Config.fieldMapping, avatarUrl: e.target.value } as FieldMapping }) + } + /> + + )} +
+ + + + +
+
+ ); +} + +export default CreateIdentityProviderDialog; diff --git a/web/src/components/CreateShortcutDialog.tsx b/web/src/components/CreateShortcutDialog.tsx new file mode 100644 index 0000000..92e3daa --- /dev/null +++ b/web/src/components/CreateShortcutDialog.tsx @@ -0,0 +1,141 @@ +import React, { useState } from "react"; +import { toast } from "react-hot-toast"; +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { shortcutServiceClient } from "@/grpcweb"; +import useCurrentUser from "@/hooks/useCurrentUser"; +import useLoading from "@/hooks/useLoading"; +import { userStore } from "@/store"; +import { Shortcut } from "@/types/proto/api/v1/shortcut_service"; +import { useTranslate } from "@/utils/i18n"; + +interface CreateShortcutDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + shortcut?: Shortcut; + onSuccess?: () => void; +} + +export function CreateShortcutDialog({ open, onOpenChange, shortcut: initialShortcut, onSuccess }: CreateShortcutDialogProps) { + const t = useTranslate(); + const user = useCurrentUser(); + const [shortcut, setShortcut] = useState({ + name: initialShortcut?.name || "", + title: initialShortcut?.title || "", + filter: initialShortcut?.filter || "", + }); + const requestState = useLoading(false); + const isCreating = !initialShortcut; + + const onShortcutTitleChange = (e: React.ChangeEvent) => { + setShortcut({ ...shortcut, title: e.target.value }); + }; + + const onShortcutFilterChange = (e: React.ChangeEvent) => { + setShortcut({ ...shortcut, filter: e.target.value }); + }; + + const handleConfirm = async () => { + if (!shortcut.title || !shortcut.filter) { + toast.error("Title and filter cannot be empty"); + return; + } + + try { + requestState.setLoading(); + if (isCreating) { + await shortcutServiceClient.createShortcut({ + parent: user.name, + shortcut: { + name: "", // Will be set by server + title: shortcut.title, + filter: shortcut.filter, + }, + }); + toast.success("Create shortcut successfully"); + } else { + await shortcutServiceClient.updateShortcut({ + shortcut: { + ...shortcut, + name: initialShortcut!.name, // Keep the original resource name + }, + updateMask: ["title", "filter"], + }); + toast.success("Update shortcut successfully"); + } + // Refresh shortcuts. + await userStore.fetchShortcuts(); + requestState.setFinish(); + onSuccess?.(); + onOpenChange(false); + } catch (error: any) { + console.error(error); + toast.error(error.details); + requestState.setError(); + } + }; + + return ( + + + + {`${isCreating ? t("common.create") : t("common.edit")} ${t("common.shortcuts")}`} + +
+
+ + +
+
+ + + +
+ ); +}); + +export default Editor; diff --git a/web/src/components/MemoEditor/RelationListView.tsx b/web/src/components/MemoEditor/RelationListView.tsx new file mode 100644 index 0000000..76c5e4a --- /dev/null +++ b/web/src/components/MemoEditor/RelationListView.tsx @@ -0,0 +1,55 @@ +import { LinkIcon, XIcon } from "lucide-react"; +import { observer } from "mobx-react-lite"; +import { useEffect, useState } from "react"; +import { memoStore } from "@/store"; +import { Memo, MemoRelation, MemoRelation_Type } from "@/types/proto/api/v1/memo_service"; + +interface Props { + relationList: MemoRelation[]; + setRelationList: (relationList: MemoRelation[]) => void; +} + +const RelationListView = observer((props: Props) => { + const { relationList, setRelationList } = props; + const [referencingMemoList, setReferencingMemoList] = useState([]); + + useEffect(() => { + (async () => { + const requests = relationList + .filter((relation) => relation.type === MemoRelation_Type.REFERENCE) + .map(async (relation) => { + return await memoStore.getOrFetchMemoByName(relation.relatedMemo!.name, { skipStore: true }); + }); + const list = await Promise.all(requests); + setReferencingMemoList(list); + })(); + }, [relationList]); + + const handleDeleteRelation = async (memo: Memo) => { + setRelationList(relationList.filter((relation) => relation.relatedMemo?.name !== memo.name)); + }; + + return ( + <> + {referencingMemoList.length > 0 && ( +
+ {referencingMemoList.map((memo) => { + return ( +
handleDeleteRelation(memo)} + > + + {memo.snippet} + +
+ ); + })} +
+ )} + + ); +}); + +export default RelationListView; diff --git a/web/src/components/MemoEditor/SortableItem.tsx b/web/src/components/MemoEditor/SortableItem.tsx new file mode 100644 index 0000000..e8f3cd6 --- /dev/null +++ b/web/src/components/MemoEditor/SortableItem.tsx @@ -0,0 +1,25 @@ +import { useSortable } from "@dnd-kit/sortable"; +import { CSS } from "@dnd-kit/utilities"; + +interface Props { + id: string; + className: string; + children: React.ReactNode; +} + +const SortableItem: React.FC = ({ id, className, children }: Props) => { + const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id }); + + const style = { + transform: CSS.Transform.toString(transform), + transition, + }; + + return ( +
+ {children} +
+ ); +}; + +export default SortableItem; diff --git a/web/src/components/MemoEditor/handlers.ts b/web/src/components/MemoEditor/handlers.ts new file mode 100644 index 0000000..2e0c2bb --- /dev/null +++ b/web/src/components/MemoEditor/handlers.ts @@ -0,0 +1,52 @@ +import { EditorRefActions } from "./Editor"; + +export const handleEditorKeydownWithMarkdownShortcuts = (event: React.KeyboardEvent, editorRef: EditorRefActions) => { + if (event.key === "b") { + const boldDelimiter = "**"; + event.preventDefault(); + styleHighlightedText(editorRef, boldDelimiter); + } else if (event.key === "i") { + const italicsDelimiter = "*"; + event.preventDefault(); + styleHighlightedText(editorRef, italicsDelimiter); + } else if (event.key === "k") { + event.preventDefault(); + hyperlinkHighlightedText(editorRef); + } +}; + +export const hyperlinkHighlightedText = (editor: EditorRefActions, url?: string) => { + const cursorPosition = editor.getCursorPosition(); + const selectedContent = editor.getSelectedContent(); + const blankURL = "url"; + + // If the selected content looks like a URL and no URL is provided, + // create a link with empty text and the URL + const urlRegex = /^(https?:\/\/[^\s]+)$/; + if (!url && urlRegex.test(selectedContent.trim())) { + editor.insertText(`[](${selectedContent})`); + editor.setCursorPosition(cursorPosition + 1, cursorPosition + 1); + } else { + url = url ?? blankURL; + + editor.insertText(`[${selectedContent}](${url})`); + + if (url === blankURL) { + const newCursorStart = cursorPosition + selectedContent.length + 3; + editor.setCursorPosition(newCursorStart, newCursorStart + url.length); + } + } +}; + +const styleHighlightedText = (editor: EditorRefActions, delimiter: string) => { + const cursorPosition = editor.getCursorPosition(); + const selectedContent = editor.getSelectedContent(); + if (selectedContent.startsWith(delimiter) && selectedContent.endsWith(delimiter)) { + editor.insertText(selectedContent.slice(delimiter.length, -delimiter.length)); + const newContentLength = selectedContent.length - delimiter.length * 2; + editor.setCursorPosition(cursorPosition, cursorPosition + newContentLength); + } else { + editor.insertText(`${delimiter}${selectedContent}${delimiter}`); + editor.setCursorPosition(cursorPosition + delimiter.length, cursorPosition + delimiter.length + selectedContent.length); + } +}; diff --git a/web/src/components/MemoEditor/index.tsx b/web/src/components/MemoEditor/index.tsx new file mode 100644 index 0000000..b7bc6ae --- /dev/null +++ b/web/src/components/MemoEditor/index.tsx @@ -0,0 +1,582 @@ +import copy from "copy-to-clipboard"; +import { isEqual } from "lodash-es"; +import { LoaderIcon, SendIcon } from "lucide-react"; +import { observer } from "mobx-react-lite"; +import React, { useEffect, useMemo, useRef, useState } from "react"; +import { toast } from "react-hot-toast"; +import { useTranslation } from "react-i18next"; +import useLocalStorage from "react-use/lib/useLocalStorage"; +import { Button } from "@/components/ui/button"; +import { memoServiceClient } from "@/grpcweb"; +import { TAB_SPACE_WIDTH } from "@/helpers/consts"; +import { isValidUrl } from "@/helpers/utils"; +import useAsyncEffect from "@/hooks/useAsyncEffect"; +import useCurrentUser from "@/hooks/useCurrentUser"; +import { cn } from "@/lib/utils"; +import { memoStore, attachmentStore, userStore, workspaceStore } from "@/store"; +import { extractMemoIdFromName } from "@/store/common"; +import { Attachment } from "@/types/proto/api/v1/attachment_service"; +import { Location, Memo, MemoRelation, MemoRelation_Type, Visibility } from "@/types/proto/api/v1/memo_service"; +import { UserSetting } from "@/types/proto/api/v1/user_service"; +import { useTranslate } from "@/utils/i18n"; +import { convertVisibilityFromString } from "@/utils/memo"; +import DateTimeInput from "../DateTimeInput"; +import AddMemoRelationPopover from "./ActionButton/AddMemoRelationPopover"; +import LocationSelector from "./ActionButton/LocationSelector"; +import MarkdownMenu from "./ActionButton/MarkdownMenu"; +import TagSelector from "./ActionButton/TagSelector"; +import UploadAttachmentButton from "./ActionButton/UploadAttachmentButton"; +import VisibilitySelector from "./ActionButton/VisibilitySelector"; +import AttachmentListView from "./AttachmentListView"; +import Editor, { EditorRefActions } from "./Editor"; +import RelationListView from "./RelationListView"; +import { handleEditorKeydownWithMarkdownShortcuts, hyperlinkHighlightedText } from "./handlers"; +import { MemoEditorContext } from "./types"; + +export interface Props { + className?: string; + cacheKey?: string; + placeholder?: string; + // The name of the memo to be edited. + memoName?: string; + // The name of the parent memo if the memo is a comment. + parentMemoName?: string; + autoFocus?: boolean; + onConfirm?: (memoName: string) => void; + onCancel?: () => void; +} + +interface State { + memoVisibility: Visibility; + attachmentList: Attachment[]; + relationList: MemoRelation[]; + location: Location | undefined; + isUploadingAttachment: boolean; + isRequesting: boolean; + isComposing: boolean; + isDraggingFile: boolean; +} + +const MemoEditor = observer((props: Props) => { + const { className, cacheKey, memoName, parentMemoName, autoFocus, onConfirm, onCancel } = props; + const t = useTranslate(); + const { i18n } = useTranslation(); + const currentUser = useCurrentUser(); + const [state, setState] = useState({ + memoVisibility: Visibility.PRIVATE, + attachmentList: [], + relationList: [], + location: undefined, + isUploadingAttachment: false, + isRequesting: false, + isComposing: false, + isDraggingFile: false, + }); + const [createTime, setCreateTime] = useState(); + const [updateTime, setUpdateTime] = useState(); + const [hasContent, setHasContent] = useState(false); + const [isVisibilitySelectorOpen, setIsVisibilitySelectorOpen] = useState(false); + const editorRef = useRef(null); + const userSetting = userStore.state.userSetting as UserSetting; + const contentCacheKey = `${currentUser.name}-${cacheKey || ""}`; + const [contentCache, setContentCache] = useLocalStorage(contentCacheKey, ""); + const referenceRelations = memoName + ? state.relationList.filter( + (relation) => + relation.memo?.name === memoName && relation.relatedMemo?.name !== memoName && relation.type === MemoRelation_Type.REFERENCE, + ) + : state.relationList.filter((relation) => relation.type === MemoRelation_Type.REFERENCE); + const workspaceMemoRelatedSetting = workspaceStore.state.memoRelatedSetting; + + useEffect(() => { + editorRef.current?.setContent(contentCache || ""); + }, []); + + useEffect(() => { + if (autoFocus) { + handleEditorFocus(); + } + }, [autoFocus]); + + useAsyncEffect(async () => { + let visibility = convertVisibilityFromString(userSetting.memoVisibility); + if (workspaceMemoRelatedSetting.disallowPublicVisibility && visibility === Visibility.PUBLIC) { + visibility = Visibility.PROTECTED; + } + if (parentMemoName) { + const parentMemo = await memoStore.getOrFetchMemoByName(parentMemoName); + visibility = parentMemo.visibility; + } + setState((prevState) => ({ + ...prevState, + memoVisibility: convertVisibilityFromString(visibility), + })); + }, [parentMemoName, userSetting.memoVisibility, workspaceMemoRelatedSetting.disallowPublicVisibility]); + + useAsyncEffect(async () => { + if (!memoName) { + return; + } + + const memo = await memoStore.getOrFetchMemoByName(memoName); + if (memo) { + handleEditorFocus(); + setCreateTime(memo.createTime); + setUpdateTime(memo.updateTime); + setState((prevState) => ({ + ...prevState, + memoVisibility: memo.visibility, + attachmentList: memo.attachments, + relationList: memo.relations, + location: memo.location, + })); + if (!contentCache) { + editorRef.current?.setContent(memo.content ?? ""); + } + } + }, [memoName]); + + const handleCompositionStart = () => { + setState((prevState) => ({ + ...prevState, + isComposing: true, + })); + }; + + const handleCompositionEnd = () => { + setState((prevState) => ({ + ...prevState, + isComposing: false, + })); + }; + + const handleKeyDown = (event: React.KeyboardEvent) => { + if (!editorRef.current) { + return; + } + + const isMetaKey = event.ctrlKey || event.metaKey; + if (isMetaKey) { + if (event.key === "Enter") { + handleSaveBtnClick(); + return; + } + if (!workspaceMemoRelatedSetting.disableMarkdownShortcuts) { + handleEditorKeydownWithMarkdownShortcuts(event, editorRef.current); + } + } + if (event.key === "Tab" && !state.isComposing) { + event.preventDefault(); + const tabSpace = " ".repeat(TAB_SPACE_WIDTH); + const cursorPosition = editorRef.current.getCursorPosition(); + const selectedContent = editorRef.current.getSelectedContent(); + editorRef.current.insertText(tabSpace); + if (selectedContent) { + editorRef.current.setCursorPosition(cursorPosition + TAB_SPACE_WIDTH); + } + return; + } + }; + + const handleMemoVisibilityChange = (visibility: Visibility) => { + setState((prevState) => ({ + ...prevState, + memoVisibility: visibility, + })); + }; + + const handleSetAttachmentList = (attachmentList: Attachment[]) => { + setState((prevState) => ({ + ...prevState, + attachmentList, + })); + }; + + const handleSetRelationList = (relationList: MemoRelation[]) => { + setState((prevState) => ({ + ...prevState, + relationList, + })); + }; + + const handleUploadResource = async (file: File) => { + setState((state) => { + return { + ...state, + isUploadingAttachment: true, + }; + }); + + const { name: filename, size, type } = file; + const buffer = new Uint8Array(await file.arrayBuffer()); + + try { + const attachment = await attachmentStore.createAttachment({ + attachment: Attachment.fromPartial({ + filename, + size, + type, + content: buffer, + }), + attachmentId: "", + }); + setState((state) => { + return { + ...state, + isUploadingAttachment: false, + }; + }); + return attachment; + } catch (error: any) { + console.error(error); + toast.error(error.details); + setState((state) => { + return { + ...state, + isUploadingAttachment: false, + }; + }); + } + }; + + const uploadMultiFiles = async (files: FileList) => { + const uploadedAttachmentList: Attachment[] = []; + for (const file of files) { + const attachment = await handleUploadResource(file); + if (attachment) { + uploadedAttachmentList.push(attachment); + if (memoName) { + await attachmentStore.updateAttachment({ + attachment: Attachment.fromPartial({ + name: attachment.name, + memo: memoName, + }), + updateMask: ["memo"], + }); + } + } + } + if (uploadedAttachmentList.length > 0) { + setState((prevState) => ({ + ...prevState, + attachmentList: [...prevState.attachmentList, ...uploadedAttachmentList], + })); + } + }; + + const handleDropEvent = async (event: React.DragEvent) => { + if (event.dataTransfer && event.dataTransfer.files.length > 0) { + event.preventDefault(); + setState((prevState) => ({ + ...prevState, + isDraggingFile: false, + })); + + await uploadMultiFiles(event.dataTransfer.files); + } + }; + + const handleDragOver = (event: React.DragEvent) => { + if (event.dataTransfer && event.dataTransfer.types.includes("Files")) { + event.preventDefault(); + event.dataTransfer.dropEffect = "copy"; + if (!state.isDraggingFile) { + setState((prevState) => ({ + ...prevState, + isDraggingFile: true, + })); + } + } + }; + + const handleDragLeave = (event: React.DragEvent) => { + event.preventDefault(); + setState((prevState) => ({ + ...prevState, + isDraggingFile: false, + })); + }; + + const handlePasteEvent = async (event: React.ClipboardEvent) => { + if (event.clipboardData && event.clipboardData.files.length > 0) { + event.preventDefault(); + await uploadMultiFiles(event.clipboardData.files); + } else if ( + editorRef.current != null && + editorRef.current.getSelectedContent().length != 0 && + isValidUrl(event.clipboardData.getData("Text")) + ) { + event.preventDefault(); + hyperlinkHighlightedText(editorRef.current, event.clipboardData.getData("Text")); + } + }; + + const handleContentChange = (content: string) => { + setHasContent(content !== ""); + if (content !== "") { + setContentCache(content); + } else { + localStorage.removeItem(contentCacheKey); + } + }; + + const handleSaveBtnClick = async () => { + if (state.isRequesting) { + return; + } + + setState((state) => { + return { + ...state, + isRequesting: true, + }; + }); + const content = editorRef.current?.getContent() ?? ""; + try { + // Update memo. + if (memoName) { + const prevMemo = await memoStore.getOrFetchMemoByName(memoName); + if (prevMemo) { + const updateMask = new Set(); + const memoPatch: Partial = { + name: prevMemo.name, + content, + }; + if (!isEqual(content, prevMemo.content)) { + updateMask.add("content"); + memoPatch.content = content; + } + if (!isEqual(state.memoVisibility, prevMemo.visibility)) { + updateMask.add("visibility"); + memoPatch.visibility = state.memoVisibility; + } + if (!isEqual(state.attachmentList, prevMemo.attachments)) { + updateMask.add("attachments"); + memoPatch.attachments = state.attachmentList; + } + if (!isEqual(state.relationList, prevMemo.relations)) { + updateMask.add("relations"); + memoPatch.relations = state.relationList; + } + if (!isEqual(state.location, prevMemo.location)) { + updateMask.add("location"); + memoPatch.location = state.location; + } + if (["content", "attachments", "relations", "location"].some((key) => updateMask.has(key))) { + updateMask.add("update_time"); + } + if (createTime && !isEqual(createTime, prevMemo.createTime)) { + updateMask.add("create_time"); + memoPatch.createTime = createTime; + } + if (updateTime && !isEqual(updateTime, prevMemo.updateTime)) { + updateMask.add("update_time"); + memoPatch.updateTime = updateTime; + } + if (updateMask.size === 0) { + toast.error(t("editor.no-changes-detected")); + if (onCancel) { + onCancel(); + } + return; + } + const memo = await memoStore.updateMemo(memoPatch, Array.from(updateMask)); + if (onConfirm) { + onConfirm(memo.name); + } + } + } else { + // Create memo or memo comment. + const request = !parentMemoName + ? memoStore.createMemo({ + memo: Memo.fromPartial({ + content, + visibility: state.memoVisibility, + attachments: state.attachmentList, + relations: state.relationList, + location: state.location, + }), + // Optional fields can be omitted + memoId: "", + validateOnly: false, + requestId: "", + }) + : memoServiceClient + .createMemoComment({ + name: parentMemoName, + comment: { + content, + visibility: state.memoVisibility, + attachments: state.attachmentList, + relations: state.relationList, + location: state.location, + }, + }) + .then((memo) => memo); + const memo = await request; + if (onConfirm) { + onConfirm(memo.name); + } + } + editorRef.current?.setContent(""); + } catch (error: any) { + console.error(error); + toast.error(error.details); + } + + localStorage.removeItem(contentCacheKey); + setState((state) => { + return { + ...state, + isRequesting: false, + attachmentList: [], + relationList: [], + location: undefined, + isDraggingFile: false, + }; + }); + }; + + const handleCancelBtnClick = () => { + localStorage.removeItem(contentCacheKey); + + if (onCancel) { + onCancel(); + } + }; + + const handleEditorFocus = () => { + editorRef.current?.focus(); + }; + + const editorConfig = useMemo( + () => ({ + className: "", + initialContent: "", + placeholder: props.placeholder ?? t("editor.any-thoughts"), + onContentChange: handleContentChange, + onPaste: handlePasteEvent, + }), + [i18n.language], + ); + + const allowSave = (hasContent || state.attachmentList.length > 0) && !state.isUploadingAttachment && !state.isRequesting; + + return ( + { + setState((prevState) => ({ + ...prevState, + attachmentList, + })); + }, + setRelationList: (relationList: MemoRelation[]) => { + setState((prevState) => ({ + ...prevState, + relationList, + })); + }, + memoName, + }} + > +
+ + + +
e.stopPropagation()}> +
+ + + + + + setState((prevState) => ({ + ...prevState, + location, + })) + } + /> +
+
+ {props.onCancel && ( + + )} + +
+
+
e.stopPropagation()} + onMouseDown={(e) => e.stopPropagation()} + > + +
+
+ + {/* Show memo metadata if memoName is provided */} + {memoName && ( +
+
+ {!isEqual(createTime, updateTime) && updateTime && ( + <> + Updated + + + )} + {createTime && ( + <> + Created + + + )} + ID + { + copy(extractMemoIdFromName(memoName)); + toast.success(t("message.copied")); + }} + > + {extractMemoIdFromName(memoName)} + +
+
+ )} +
+ ); +}); + +export default MemoEditor; diff --git a/web/src/components/MemoEditor/types/context.ts b/web/src/components/MemoEditor/types/context.ts new file mode 100644 index 0000000..8c2d81d --- /dev/null +++ b/web/src/components/MemoEditor/types/context.ts @@ -0,0 +1,18 @@ +import { createContext } from "react"; +import { Attachment } from "@/types/proto/api/v1/attachment_service"; +import { MemoRelation } from "@/types/proto/api/v1/memo_service"; + +interface Context { + attachmentList: Attachment[]; + relationList: MemoRelation[]; + setAttachmentList: (attachmentList: Attachment[]) => void; + setRelationList: (relationList: MemoRelation[]) => void; + memoName?: string; +} + +export const MemoEditorContext = createContext({ + attachmentList: [], + relationList: [], + setAttachmentList: () => {}, + setRelationList: () => {}, +}); diff --git a/web/src/components/MemoEditor/types/index.ts b/web/src/components/MemoEditor/types/index.ts new file mode 100644 index 0000000..2edd280 --- /dev/null +++ b/web/src/components/MemoEditor/types/index.ts @@ -0,0 +1 @@ +export * from "./context"; diff --git a/web/src/components/MemoFilters.tsx b/web/src/components/MemoFilters.tsx new file mode 100644 index 0000000..8699f6d --- /dev/null +++ b/web/src/components/MemoFilters.tsx @@ -0,0 +1,80 @@ +import { isEqual } from "lodash-es"; +import { CalendarIcon, CheckCircleIcon, CodeIcon, EyeIcon, HashIcon, LinkIcon, BookmarkIcon, SearchIcon, XIcon } from "lucide-react"; +import { observer } from "mobx-react-lite"; +import { useEffect } from "react"; +import { useSearchParams } from "react-router-dom"; +import { memoFilterStore } from "@/store"; +import { FilterFactor, getMemoFilterKey, MemoFilter, stringifyFilters } from "@/store/memoFilter"; +import { useTranslate } from "@/utils/i18n"; + +const MemoFilters = observer(() => { + const t = useTranslate(); + const [, setSearchParams] = useSearchParams(); + const filters = memoFilterStore.filters; + + useEffect(() => { + const searchParams = new URLSearchParams(); + if (filters.length > 0) { + searchParams.set("filter", stringifyFilters(filters)); + } + setSearchParams(searchParams); + }, [filters]); + + const getFilterDisplayText = (filter: MemoFilter) => { + if (filter.value) { + return filter.value; + } + if (filter.factor.startsWith("property.")) { + const factorLabel = filter.factor.replace("property.", ""); + switch (factorLabel) { + case "hasLink": + return t("filters.has-link"); + case "hasCode": + return t("filters.has-code"); + case "hasTaskList": + return t("filters.has-task-list"); + default: + return factorLabel; + } + } + return filter.factor; + }; + + if (filters.length === 0) { + return undefined; + } + + return ( +
+ {filters.map((filter: MemoFilter) => ( +
memoFilterStore.removeFilter((f: MemoFilter) => isEqual(f, filter))} + > + + {getFilterDisplayText(filter)} + +
+ ))} +
+ ); +}); + +const FactorIcon = ({ factor, className }: { factor: FilterFactor; className?: string }) => { + const iconMap = { + tagSearch: , + visibility: , + contentSearch: , + displayTime: , + pinned: , + "property.hasLink": , + "property.hasTaskList": , + "property.hasCode": , + }; + return iconMap[factor as keyof typeof iconMap] || <>; +}; + +export default MemoFilters; diff --git a/web/src/components/MemoLocationView.tsx b/web/src/components/MemoLocationView.tsx new file mode 100644 index 0000000..e2f39c0 --- /dev/null +++ b/web/src/components/MemoLocationView.tsx @@ -0,0 +1,35 @@ +import { LatLng } from "leaflet"; +import { MapPinIcon } from "lucide-react"; +import { useState } from "react"; +import { Location } from "@/types/proto/api/v1/memo_service"; +import LeafletMap from "./LeafletMap"; +import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; + +interface Props { + location: Location; +} + +const MemoLocationView: React.FC = (props: Props) => { + const { location } = props; + const [popoverOpen, setPopoverOpen] = useState(false); + + return ( + + +

+ + + {location.placeholder ? location.placeholder : `[${location.latitude}, ${location.longitude}]`} + +

+
+ +
+ +
+
+
+ ); +}; + +export default MemoLocationView; diff --git a/web/src/components/MemoReactionListView.tsx b/web/src/components/MemoReactionListView.tsx new file mode 100644 index 0000000..124e385 --- /dev/null +++ b/web/src/components/MemoReactionListView.tsx @@ -0,0 +1,49 @@ +import { uniq } from "lodash-es"; +import { observer } from "mobx-react-lite"; +import { memo, useEffect, useState } from "react"; +import useCurrentUser from "@/hooks/useCurrentUser"; +import { userStore } from "@/store"; +import { State } from "@/types/proto/api/v1/common"; +import { Memo } from "@/types/proto/api/v1/memo_service"; +import { Reaction } from "@/types/proto/api/v1/memo_service"; +import { User } from "@/types/proto/api/v1/user_service"; +import ReactionSelector from "./ReactionSelector"; +import ReactionView from "./ReactionView"; + +interface Props { + memo: Memo; + reactions: Reaction[]; +} + +const MemoReactionListView = observer((props: Props) => { + const { memo, reactions } = props; + const currentUser = useCurrentUser(); + const [reactionGroup, setReactionGroup] = useState>(new Map()); + const readonly = memo.state === State.ARCHIVED; + + useEffect(() => { + (async () => { + const reactionGroup = new Map(); + for (const reaction of reactions) { + const user = await userStore.getOrFetchUserByName(reaction.creator); + const users = reactionGroup.get(reaction.reactionType) || []; + users.push(user); + reactionGroup.set(reaction.reactionType, uniq(users)); + } + setReactionGroup(reactionGroup); + })(); + }, [reactions]); + + return ( + reactions.length > 0 && ( +
+ {Array.from(reactionGroup).map(([reactionType, users]) => { + return ; + })} + {!readonly && currentUser && } +
+ ) + ); +}); + +export default memo(MemoReactionListView); diff --git a/web/src/components/MemoRelationForceGraph/MemoRelationForceGraph.tsx b/web/src/components/MemoRelationForceGraph/MemoRelationForceGraph.tsx new file mode 100644 index 0000000..da816f8 --- /dev/null +++ b/web/src/components/MemoRelationForceGraph/MemoRelationForceGraph.tsx @@ -0,0 +1,81 @@ +import { useEffect, useRef, useState } from "react"; +import ForceGraph2D, { ForceGraphMethods, LinkObject, NodeObject } from "react-force-graph-2d"; +import useNavigateTo from "@/hooks/useNavigateTo"; +import { cn } from "@/lib/utils"; +import { extractMemoIdFromName } from "@/store/common"; +import { Memo, MemoRelation_Type } from "@/types/proto/api/v1/memo_service"; +import { LinkType, NodeType } from "./types"; +import { convertMemoRelationsToGraphData } from "./utils"; + +interface Props { + memo: Memo; + className?: string; + parentPage?: string; +} + +const MAIN_NODE_COLOR = "#14b8a6"; +const DEFAULT_NODE_COLOR = "#a1a1aa"; + +const MemoRelationForceGraph = ({ className, memo, parentPage }: Props) => { + const navigateTo = useNavigateTo(); + const [mode, setMode] = useState<"light" | "dark">("light"); + const containerRef = useRef(null); + const graphRef = useRef, LinkObject> | undefined>(undefined); + const [graphSize, setGraphSize] = useState({ width: 0, height: 0 }); + + // Simple dark mode detection + useEffect(() => { + const updateMode = () => { + const isDark = document.documentElement.classList.contains("dark"); + setMode(isDark ? "dark" : "light"); + }; + + updateMode(); + + // Watch for changes to the dark class + const observer = new MutationObserver(updateMode); + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["class"], + }); + + return () => observer.disconnect(); + }, []); + + useEffect(() => { + if (!containerRef.current) return; + setGraphSize(containerRef.current.getBoundingClientRect()); + }, []); + + const onNodeClick = (node: NodeObject) => { + if (node.memo.name === memo.name) return; + navigateTo(`/${memo.name}`, { + state: { + from: parentPage, + }, + }); + }; + + return ( +
+ (node.id === memo.name ? MAIN_NODE_COLOR : DEFAULT_NODE_COLOR)} + nodeRelSize={3} + nodeLabel={(node) => extractMemoIdFromName(node.memo.name).slice(0, 6).toLowerCase()} + linkColor={() => (mode === "light" ? "#e4e4e7" : "#3f3f46")} + graphData={convertMemoRelationsToGraphData(memo.relations.filter((r) => r.type === MemoRelation_Type.REFERENCE))} + onNodeClick={onNodeClick} + linkDirectionalArrowLength={3} + linkDirectionalArrowRelPos={1} + linkCurvature={0.25} + /> +
+ ); +}; + +export default MemoRelationForceGraph; diff --git a/web/src/components/MemoRelationForceGraph/index.ts b/web/src/components/MemoRelationForceGraph/index.ts new file mode 100644 index 0000000..f89b461 --- /dev/null +++ b/web/src/components/MemoRelationForceGraph/index.ts @@ -0,0 +1,5 @@ +import MemoRelationForceGraph from "./MemoRelationForceGraph"; + +export * from "./utils"; + +export default MemoRelationForceGraph; diff --git a/web/src/components/MemoRelationForceGraph/types.ts b/web/src/components/MemoRelationForceGraph/types.ts new file mode 100644 index 0000000..3860ef4 --- /dev/null +++ b/web/src/components/MemoRelationForceGraph/types.ts @@ -0,0 +1,10 @@ +import { MemoRelation_Memo } from "@/types/proto/api/v1/memo_service"; + +export interface NodeType { + memo: MemoRelation_Memo; +} + +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +export interface LinkType { + // ...add more additional properties relevant to the link here. +} diff --git a/web/src/components/MemoRelationForceGraph/utils.ts b/web/src/components/MemoRelationForceGraph/utils.ts new file mode 100644 index 0000000..610da5e --- /dev/null +++ b/web/src/components/MemoRelationForceGraph/utils.ts @@ -0,0 +1,36 @@ +import { GraphData, LinkObject, NodeObject } from "react-force-graph-2d"; +import { MemoRelation, MemoRelation_Memo } from "@/types/proto/api/v1/memo_service"; +import { LinkType, NodeType } from "./types"; + +export const convertMemoRelationsToGraphData = (memoRelations: MemoRelation[]): GraphData => { + const nodesMap = new Map>(); + const links: LinkObject[] = []; + + // Iterate through memoRelations to populate nodes and links. + memoRelations.forEach((relation) => { + const memo = relation.memo as MemoRelation_Memo; + const relatedMemo = relation.relatedMemo as MemoRelation_Memo; + + // Add memo node if not already present. + if (!nodesMap.has(memo.name)) { + nodesMap.set(memo.name, { id: memo.name, memo }); + } + + // Add related_memo node if not already present. + if (!nodesMap.has(relatedMemo.name)) { + nodesMap.set(relatedMemo.name, { id: relatedMemo.name, memo: relatedMemo }); + } + + // Create link between memo and relatedMemo. + links.push({ + source: memo.name, + target: relatedMemo.name, + type: relation.type, // Include the type of relation as a property of the link. + }); + }); + + return { + nodes: Array.from(nodesMap.values()), + links, + }; +}; diff --git a/web/src/components/MemoRelationListView.tsx b/web/src/components/MemoRelationListView.tsx new file mode 100644 index 0000000..53dd3b8 --- /dev/null +++ b/web/src/components/MemoRelationListView.tsx @@ -0,0 +1,110 @@ +import { LinkIcon, MilestoneIcon } from "lucide-react"; +import { memo, useState } from "react"; +import { Link } from "react-router-dom"; +import { cn } from "@/lib/utils"; +import { extractMemoIdFromName } from "@/store/common"; +import { Memo, MemoRelation } from "@/types/proto/api/v1/memo_service"; +import { useTranslate } from "@/utils/i18n"; + +interface Props { + memo: Memo; + relations: MemoRelation[]; + parentPage?: string; +} + +const MemoRelationListView = (props: Props) => { + const t = useTranslate(); + const { memo, relations: relationList, parentPage } = props; + const referencingMemoList = relationList + .filter((relation) => relation.memo?.name === memo.name && relation.relatedMemo?.name !== memo.name) + .map((relation) => relation.relatedMemo!); + const referencedMemoList = relationList + .filter((relation) => relation.memo?.name !== memo.name && relation.relatedMemo?.name === memo.name) + .map((relation) => relation.memo!); + const [selectedTab, setSelectedTab] = useState<"referencing" | "referenced">( + referencingMemoList.length === 0 ? "referenced" : "referencing", + ); + + if (referencingMemoList.length + referencedMemoList.length === 0) { + return null; + } + + return ( +
+
+ {referencingMemoList.length > 0 && ( + + )} + {referencedMemoList.length > 0 && ( + + )} +
+ {selectedTab === "referencing" && referencingMemoList.length > 0 && ( +
+ {referencingMemoList.map((memo) => { + return ( + + + {extractMemoIdFromName(memo.name).slice(0, 6)} + + {memo.snippet} + + ); + })} +
+ )} + {selectedTab === "referenced" && referencedMemoList.length > 0 && ( +
+ {referencedMemoList.map((memo) => { + return ( + + + {extractMemoIdFromName(memo.name).slice(0, 6)} + + {memo.snippet} + + ); + })} +
+ )} +
+ ); +}; + +export default memo(MemoRelationListView); diff --git a/web/src/components/MemoResource.tsx b/web/src/components/MemoResource.tsx new file mode 100644 index 0000000..e69de29 diff --git a/web/src/components/MemoView.tsx b/web/src/components/MemoView.tsx new file mode 100644 index 0000000..6c5f0c9 --- /dev/null +++ b/web/src/components/MemoView.tsx @@ -0,0 +1,275 @@ +import { BookmarkIcon, EyeOffIcon, MessageCircleMoreIcon } from "lucide-react"; +import { observer } from "mobx-react-lite"; +import { memo, useCallback, useState } from "react"; +import { Link, useLocation } from "react-router-dom"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import useAsyncEffect from "@/hooks/useAsyncEffect"; +import useCurrentUser from "@/hooks/useCurrentUser"; +import useNavigateTo from "@/hooks/useNavigateTo"; +import { cn } from "@/lib/utils"; +import { memoStore, userStore, workspaceStore } from "@/store"; +import { State } from "@/types/proto/api/v1/common"; +import { Memo, MemoRelation_Type, Visibility } from "@/types/proto/api/v1/memo_service"; +import { useTranslate } from "@/utils/i18n"; +import { convertVisibilityToString } from "@/utils/memo"; +import { isSuperUser } from "@/utils/user"; +import MemoActionMenu from "./MemoActionMenu"; +import MemoAttachmentListView from "./MemoAttachmentListView"; +import MemoContent from "./MemoContent"; +import MemoEditor from "./MemoEditor"; +import MemoLocationView from "./MemoLocationView"; +import MemoReactionistView from "./MemoReactionListView"; +import MemoRelationListView from "./MemoRelationListView"; +import { PreviewImageDialog } from "./PreviewImageDialog"; +import ReactionSelector from "./ReactionSelector"; +import UserAvatar from "./UserAvatar"; +import VisibilityIcon from "./VisibilityIcon"; + +interface Props { + memo: Memo; + compact?: boolean; + showCreator?: boolean; + showVisibility?: boolean; + showPinned?: boolean; + showNsfwContent?: boolean; + className?: string; + parentPage?: string; +} + +const MemoView: React.FC = observer((props: Props) => { + const { memo, className } = props; + const t = useTranslate(); + const location = useLocation(); + const navigateTo = useNavigateTo(); + const currentUser = useCurrentUser(); + const user = useCurrentUser(); + const [showEditor, setShowEditor] = useState(false); + const [creator, setCreator] = useState(userStore.getUserByName(memo.creator)); + const [showNSFWContent, setShowNSFWContent] = useState(props.showNsfwContent); + const [previewImage, setPreviewImage] = useState<{ open: boolean; urls: string[]; index: number }>({ + open: false, + urls: [], + index: 0, + }); + const workspaceMemoRelatedSetting = workspaceStore.state.memoRelatedSetting; + const referencedMemos = memo.relations.filter((relation) => relation.type === MemoRelation_Type.REFERENCE); + const commentAmount = memo.relations.filter( + (relation) => relation.type === MemoRelation_Type.COMMENT && relation.relatedMemo?.name === memo.name, + ).length; + const relativeTimeFormat = Date.now() - memo.displayTime!.getTime() > 1000 * 60 * 60 * 24 ? "datetime" : "auto"; + const isArchived = memo.state === State.ARCHIVED; + const readonly = memo.creator !== user?.name && !isSuperUser(user); + const isInMemoDetailPage = location.pathname.startsWith(`/${memo.name}`); + const parentPage = props.parentPage || location.pathname; + const nsfw = + workspaceMemoRelatedSetting.enableBlurNsfwContent && + memo.tags?.some((tag) => workspaceMemoRelatedSetting.nsfwTags.some((nsfwTag) => tag === nsfwTag || tag.startsWith(`${nsfwTag}/`))); + + // Initial related data: creator. + useAsyncEffect(async () => { + const user = await userStore.getOrFetchUserByName(memo.creator); + setCreator(user); + }, []); + + const handleGotoMemoDetailPage = useCallback(() => { + navigateTo(`/${memo.name}`, { + state: { + from: parentPage, + }, + }); + }, [memo.name, parentPage]); + + const handleMemoContentClick = useCallback(async (e: React.MouseEvent) => { + const targetEl = e.target as HTMLElement; + + if (targetEl.tagName === "IMG") { + const imgUrl = targetEl.getAttribute("src"); + if (imgUrl) { + setPreviewImage({ open: true, urls: [imgUrl], index: 0 }); + } + } + }, []); + + const handleMemoContentDoubleClick = useCallback(async (e: React.MouseEvent) => { + if (readonly) { + return; + } + + if (workspaceMemoRelatedSetting.enableDoubleClickEdit) { + e.preventDefault(); + setShowEditor(true); + } + }, []); + + const onEditorConfirm = () => { + setShowEditor(false); + userStore.setStatsStateId(); + }; + + const onPinIconClick = async () => { + if (memo.pinned) { + await memoStore.updateMemo( + { + name: memo.name, + pinned: false, + }, + ["pinned"], + ); + } + }; + + const displayTime = isArchived ? ( + memo.displayTime?.toLocaleString() + ) : ( + + ); + + return showEditor ? ( + setShowEditor(false)} + /> + ) : ( +
+
+
+ {props.showCreator && creator ? ( +
+ + + +
+ + {creator.displayName || creator.username} + +
+ {displayTime} +
+
+
+ ) : ( +
+ {displayTime} +
+ )} +
+
+
+ {props.showVisibility && memo.visibility !== Visibility.PRIVATE && ( + + + + + + + {t(`memo.visibility.${convertVisibilityToString(memo.visibility).toLowerCase()}` as any)} + + )} + {currentUser && !isArchived && } +
+ {!isInMemoDetailPage && (workspaceMemoRelatedSetting.enableComment || commentAmount > 0) && ( + + + {commentAmount > 0 && {commentAmount}} + + )} + {props.showPinned && memo.pinned && ( + + + + + + + + +

{t("common.unpin")}

+
+
+
+ )} + {nsfw && showNSFWContent && ( + + setShowNSFWContent(false)} /> + + )} + setShowEditor(true)} /> +
+
+
+ + {memo.location && } + + + +
+ {nsfw && !showNSFWContent && ( + <> +
+ + + )} + + setPreviewImage((prev) => ({ ...prev, open }))} + imgUrls={previewImage.urls} + initialIndex={previewImage.index} + /> +
+ ); +}); + +export default memo(MemoView); diff --git a/web/src/components/MobileHeader.tsx b/web/src/components/MobileHeader.tsx new file mode 100644 index 0000000..6cbd7b9 --- /dev/null +++ b/web/src/components/MobileHeader.tsx @@ -0,0 +1,30 @@ +import useWindowScroll from "react-use/lib/useWindowScroll"; +import useResponsiveWidth from "@/hooks/useResponsiveWidth"; +import { cn } from "@/lib/utils"; +import NavigationDrawer from "./NavigationDrawer"; + +interface Props { + className?: string; + children?: React.ReactNode; +} + +const MobileHeader = (props: Props) => { + const { className, children } = props; + const { sm } = useResponsiveWidth(); + const { y: offsetTop } = useWindowScroll(); + + return ( +
0 && "shadow-md", + className, + )} + > +
{!sm && }
+
{children}
+
+ ); +}; + +export default MobileHeader; diff --git a/web/src/components/Navigation.tsx b/web/src/components/Navigation.tsx new file mode 100644 index 0000000..37e927e --- /dev/null +++ b/web/src/components/Navigation.tsx @@ -0,0 +1,123 @@ +import { EarthIcon, LibraryIcon, PaperclipIcon, UserCircleIcon, DownloadIcon } from "lucide-react"; +import { observer } from "mobx-react-lite"; +import { useEffect } from "react"; +import { NavLink } from "react-router-dom"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import useCurrentUser from "@/hooks/useCurrentUser"; +import { cn } from "@/lib/utils"; +import { Routes } from "@/router"; +import { userStore } from "@/store"; +import { useTranslate } from "@/utils/i18n"; +import BrandBanner from "./BrandBanner"; +import UserBanner from "./UserBanner"; + +interface NavLinkItem { + id: string; + path: string; + title: string; + icon: React.ReactNode; +} + +interface Props { + collapsed?: boolean; + className?: string; +} + +const Navigation = observer((props: Props) => { + const { collapsed, className } = props; + const t = useTranslate(); + const currentUser = useCurrentUser(); + + useEffect(() => { + if (!currentUser) { + return; + } + + userStore.fetchInboxes(); + }, []); + + const homeNavLink: NavLinkItem = { + id: "header-memos", + path: Routes.ROOT, + title: t("common.memos"), + icon: , + }; + const exploreNavLink: NavLinkItem = { + id: "header-explore", + path: Routes.EXPLORE, + title: t("common.explore"), + icon: , + }; + const attachmentsNavLink: NavLinkItem = { + id: "header-attachments", + path: Routes.ATTACHMENTS, + title: t("common.attachments"), + icon: , + }; + const exportImportNavLink: NavLinkItem = { + id: "header-export-import", + path: Routes.EXPORT_IMPORT, + title: "Export/Import", + icon: , + }; + const signInNavLink: NavLinkItem = { + id: "header-auth", + path: Routes.AUTH, + title: t("common.sign-in"), + icon: , + }; + + const navLinks: NavLinkItem[] = currentUser + ? [homeNavLink, exploreNavLink, attachmentsNavLink, exportImportNavLink] + : [exploreNavLink, signInNavLink]; + + return ( +
+
+ + + + {navLinks.map((navLink) => ( + + cn( + "px-2 py-2 rounded-2xl border flex flex-row items-center text-lg text-sidebar-foreground transition-colors", + collapsed ? "" : "w-full px-4", + isActive + ? "bg-sidebar-accent text-sidebar-accent-foreground border-sidebar-accent-border drop-shadow" + : "border-transparent hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:border-sidebar-accent-border opacity-80", + ) + } + key={navLink.id} + to={navLink.path} + id={navLink.id} + viewTransition + > + {props.collapsed ? ( + + + +
{navLink.icon}
+
+ +

{navLink.title}

+
+
+
+ ) : ( + navLink.icon + )} + {!props.collapsed && {navLink.title}} +
+ ))} +
+ {currentUser && ( +
+ +
+ )} +
+ ); +}); + +export default Navigation; \ No newline at end of file diff --git a/web/src/components/NavigationDrawer.tsx b/web/src/components/NavigationDrawer.tsx new file mode 100644 index 0000000..0cee7e3 --- /dev/null +++ b/web/src/components/NavigationDrawer.tsx @@ -0,0 +1,39 @@ +import { observer } from "mobx-react-lite"; +import { useEffect, useState } from "react"; +import { useLocation } from "react-router-dom"; +import { Button } from "@/components/ui/button"; +import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet"; +import { workspaceStore } from "@/store"; +import Navigation from "./Navigation"; +import UserAvatar from "./UserAvatar"; + +const NavigationDrawer = observer(() => { + const location = useLocation(); + const [open, setOpen] = useState(false); + const workspaceGeneralSetting = workspaceStore.state.generalSetting; + const title = workspaceGeneralSetting.customProfile?.title || "Memos"; + const avatarUrl = workspaceGeneralSetting.customProfile?.logoUrl || "/full-logo.webp"; + + useEffect(() => { + setOpen(false); + }, [location.pathname]); + + return ( + + + + + + + + + + + + ); +}); + +export default NavigationDrawer; diff --git a/web/src/components/PagedMemoList/PagedMemoList.tsx b/web/src/components/PagedMemoList/PagedMemoList.tsx new file mode 100644 index 0000000..30db9a4 --- /dev/null +++ b/web/src/components/PagedMemoList/PagedMemoList.tsx @@ -0,0 +1,230 @@ +import { ArrowUpIcon, LoaderIcon } from "lucide-react"; +import { observer } from "mobx-react-lite"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { matchPath } from "react-router-dom"; +import PullToRefresh from "react-simple-pull-to-refresh"; +import { Button } from "@/components/ui/button"; +import { DEFAULT_LIST_MEMOS_PAGE_SIZE } from "@/helpers/consts"; +import useResponsiveWidth from "@/hooks/useResponsiveWidth"; +import { Routes } from "@/router"; +import { memoStore, viewStore } from "@/store"; +import { State } from "@/types/proto/api/v1/common"; +import { Memo } from "@/types/proto/api/v1/memo_service"; +import { useTranslate } from "@/utils/i18n"; +import Empty from "../Empty"; +import MasonryView from "../MasonryView"; +import MemoEditor from "../MemoEditor"; + +interface Props { + renderer: (memo: Memo) => JSX.Element; + listSort?: (list: Memo[]) => Memo[]; + owner?: string; + state?: State; + orderBy?: string; + filter?: string; + oldFilter?: string; + pageSize?: number; +} + +const PagedMemoList = observer((props: Props) => { + const t = useTranslate(); + const { md } = useResponsiveWidth(); + + // Simplified state management - separate state variables for clarity + const [isRequesting, setIsRequesting] = useState(true); + const [nextPageToken, setNextPageToken] = useState(""); + + // Ref to manage auto-fetch timeout to prevent memory leaks + const autoFetchTimeoutRef = useRef(null); + + // Apply custom sorting if provided, otherwise use store memos directly + const sortedMemoList = props.listSort ? props.listSort(memoStore.state.memos) : memoStore.state.memos; + + // Show memo editor only on the root route + const showMemoEditor = Boolean(matchPath(Routes.ROOT, window.location.pathname)); + + // Fetch more memos with pagination support + const fetchMoreMemos = async (pageToken: string) => { + setIsRequesting(true); + + try { + const response = await memoStore.fetchMemos({ + parent: props.owner || "", + state: props.state || State.NORMAL, + orderBy: props.orderBy || "display_time desc", + filter: props.filter || "", + oldFilter: props.oldFilter || "", + pageSize: props.pageSize || DEFAULT_LIST_MEMOS_PAGE_SIZE, + pageToken, + }); + + setNextPageToken(response?.nextPageToken || ""); + } finally { + setIsRequesting(false); + } + }; + + // Helper function to check if page has enough content to be scrollable + const isPageScrollable = () => { + const documentHeight = Math.max(document.body.scrollHeight, document.documentElement.scrollHeight); + return documentHeight > window.innerHeight + 100; // 100px buffer for safe measure + }; + + // Auto-fetch more content if page isn't scrollable and more data is available + const checkAndFetchIfNeeded = useCallback(async () => { + // Clear any pending auto-fetch timeout + if (autoFetchTimeoutRef.current) { + clearTimeout(autoFetchTimeoutRef.current); + } + + // Wait for DOM to update before checking scrollability + await new Promise((resolve) => setTimeout(resolve, 200)); + + // Only fetch if: page isn't scrollable, we have more data, not currently loading, and have memos + const shouldFetch = !isPageScrollable() && nextPageToken && !isRequesting && sortedMemoList.length > 0; + + if (shouldFetch) { + await fetchMoreMemos(nextPageToken); + + // Schedule another check with delay to prevent rapid successive calls + autoFetchTimeoutRef.current = window.setTimeout(() => { + checkAndFetchIfNeeded(); + }, 500); + } + }, [nextPageToken, isRequesting, sortedMemoList.length]); + + // Refresh the entire memo list from the beginning + const refreshList = async () => { + memoStore.state.updateStateId(); + setNextPageToken(""); + await fetchMoreMemos(""); + }; + + // Initial load and reload when props change + useEffect(() => { + refreshList(); + }, [props.owner, props.state, props.orderBy, props.filter, props.oldFilter, props.pageSize]); + + // Auto-fetch more content when list changes and page isn't full + useEffect(() => { + if (!isRequesting && sortedMemoList.length > 0) { + checkAndFetchIfNeeded(); + } + }, [sortedMemoList.length, isRequesting, nextPageToken, checkAndFetchIfNeeded]); + + // Cleanup timeout on component unmount + useEffect(() => { + return () => { + if (autoFetchTimeoutRef.current) { + clearTimeout(autoFetchTimeoutRef.current); + } + }; + }, []); + + // Infinite scroll: fetch more when user scrolls near bottom + useEffect(() => { + if (!nextPageToken) return; + + const handleScroll = () => { + const nearBottom = window.innerHeight + window.scrollY >= document.body.offsetHeight - 300; + if (nearBottom && !isRequesting) { + fetchMoreMemos(nextPageToken); + } + }; + + window.addEventListener("scroll", handleScroll); + return () => window.removeEventListener("scroll", handleScroll); + }, [nextPageToken, isRequesting]); + + const children = ( +
+ : undefined} + listMode={viewStore.state.layout === "LIST"} + /> + + {/* Loading indicator */} + {isRequesting && ( +
+ +
+ )} + + {/* Empty state or back-to-top button */} + {!isRequesting && ( + <> + {!nextPageToken && sortedMemoList.length === 0 ? ( +
+ +

{t("message.no-data")}

+
+ ) : ( +
+ +
+ )} + + )} +
+ ); + + if (md) { + return children; + } + + return ( + refreshList()} + pullingContent={ +
+ +
+ } + refreshingContent={ +
+ +
+ } + > + {children} +
+ ); +}); + +const BackToTop = () => { + const t = useTranslate(); + const [isVisible, setIsVisible] = useState(false); + + useEffect(() => { + const handleScroll = () => { + const shouldShow = window.scrollY > 400; + setIsVisible(shouldShow); + }; + + window.addEventListener("scroll", handleScroll); + return () => window.removeEventListener("scroll", handleScroll); + }, []); + + const scrollToTop = () => { + window.scrollTo({ + top: 0, + behavior: "smooth", + }); + }; + + // Don't render if not visible + if (!isVisible) { + return null; + } + + return ( + + ); +}; + +export default PagedMemoList; diff --git a/web/src/components/PagedMemoList/index.ts b/web/src/components/PagedMemoList/index.ts new file mode 100644 index 0000000..fa75bee --- /dev/null +++ b/web/src/components/PagedMemoList/index.ts @@ -0,0 +1,3 @@ +import PagedMemoList from "./PagedMemoList"; + +export default PagedMemoList; diff --git a/web/src/components/PasswordSignInForm.tsx b/web/src/components/PasswordSignInForm.tsx new file mode 100644 index 0000000..af4f045 --- /dev/null +++ b/web/src/components/PasswordSignInForm.tsx @@ -0,0 +1,104 @@ +import { LoaderIcon } from "lucide-react"; +import { observer } from "mobx-react-lite"; +import { ClientError } from "nice-grpc-web"; +import { useState } from "react"; +import { toast } from "react-hot-toast"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { authServiceClient } from "@/grpcweb"; +import useLoading from "@/hooks/useLoading"; +import useNavigateTo from "@/hooks/useNavigateTo"; +import { workspaceStore } from "@/store"; +import { initialUserStore } from "@/store/user"; +import { useTranslate } from "@/utils/i18n"; + +const PasswordSignInForm = observer(() => { + const t = useTranslate(); + const navigateTo = useNavigateTo(); + const actionBtnLoadingState = useLoading(false); + const [username, setUsername] = useState(workspaceStore.state.profile.mode === "demo" ? "yourselfhosted" : ""); + const [password, setPassword] = useState(workspaceStore.state.profile.mode === "demo" ? "yourselfhosted" : ""); + + const handleUsernameInputChanged = (e: React.ChangeEvent) => { + const text = e.target.value as string; + setUsername(text); + }; + + const handlePasswordInputChanged = (e: React.ChangeEvent) => { + const text = e.target.value as string; + setPassword(text); + }; + + const handleFormSubmit = (e: React.FormEvent) => { + e.preventDefault(); + handleSignInButtonClick(); + }; + + const handleSignInButtonClick = async () => { + if (username === "" || password === "") { + return; + } + + if (actionBtnLoadingState.isLoading) { + return; + } + + try { + actionBtnLoadingState.setLoading(); + await authServiceClient.createSession({ + passwordCredentials: { username, password }, + }); + await initialUserStore(); + navigateTo("/"); + } catch (error: any) { + console.error(error); + toast.error((error as ClientError).details || "Failed to sign in."); + } + actionBtnLoadingState.setFinish(); + }; + + return ( +
+
+
+ {t("common.username")} + +
+
+ {t("common.password")} + +
+
+
+ +
+
+ ); +}); + +export default PasswordSignInForm; diff --git a/web/src/components/PreviewImageDialog.tsx b/web/src/components/PreviewImageDialog.tsx new file mode 100644 index 0000000..01ac346 --- /dev/null +++ b/web/src/components/PreviewImageDialog.tsx @@ -0,0 +1,93 @@ +import { X } from "lucide-react"; +import React, { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; + +interface PreviewImageDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + imgUrls: string[]; + initialIndex?: number; +} + +export function PreviewImageDialog({ open, onOpenChange, imgUrls, initialIndex = 0 }: PreviewImageDialogProps) { + const [currentIndex, setCurrentIndex] = useState(initialIndex); + + // Update current index when initialIndex prop changes + useEffect(() => { + setCurrentIndex(initialIndex); + }, [initialIndex]); + + // Handle keyboard navigation + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (!open) return; + + switch (event.key) { + case "Escape": + onOpenChange(false); + break; + default: + break; + } + }; + + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [open, onOpenChange]); + + const handleClose = () => { + onOpenChange(false); + }; + + // Prevent closing when clicking on the image + const handleImageClick = (event: React.MouseEvent) => { + event.stopPropagation(); + }; + + // Return early if no images provided + if (!imgUrls.length) return null; + + // Ensure currentIndex is within bounds + const safeIndex = Math.max(0, Math.min(currentIndex, imgUrls.length - 1)); + + return ( + + + {/* Close button */} +
+ +
+ + {/* Image container */} +
+ {`Preview +
+ + {/* Screen reader description */} +
+ Image preview dialog. Press Escape to close or click outside the image. +
+
+
+ ); +} diff --git a/web/src/components/ReactionSelector.tsx b/web/src/components/ReactionSelector.tsx new file mode 100644 index 0000000..3698d47 --- /dev/null +++ b/web/src/components/ReactionSelector.tsx @@ -0,0 +1,93 @@ +import { SmilePlusIcon } from "lucide-react"; +import { observer } from "mobx-react-lite"; +import { useRef, useState } from "react"; +import useClickAway from "react-use/lib/useClickAway"; +import { memoServiceClient } from "@/grpcweb"; +import useCurrentUser from "@/hooks/useCurrentUser"; +import { cn } from "@/lib/utils"; +import { memoStore, workspaceStore } from "@/store"; +import { Memo } from "@/types/proto/api/v1/memo_service"; +import { Popover, PopoverContent, PopoverTrigger } from "./ui/popover"; + +interface Props { + memo: Memo; + className?: string; +} + +const ReactionSelector = observer((props: Props) => { + const { memo, className } = props; + const currentUser = useCurrentUser(); + const [open, setOpen] = useState(false); + const containerRef = useRef(null); + const workspaceMemoRelatedSetting = workspaceStore.state.memoRelatedSetting; + + useClickAway(containerRef, () => { + setOpen(false); + }); + + const hasReacted = (reactionType: string) => { + return memo.reactions.some((r) => r.reactionType === reactionType && r.creator === currentUser?.name); + }; + + const handleReactionClick = async (reactionType: string) => { + try { + if (hasReacted(reactionType)) { + const reactions = memo.reactions.filter( + (reaction) => reaction.reactionType === reactionType && reaction.creator === currentUser.name, + ); + for (const reaction of reactions) { + await memoServiceClient.deleteMemoReaction({ name: reaction.name }); + } + } else { + await memoServiceClient.upsertMemoReaction({ + name: memo.name, + reaction: { + contentId: memo.name, + reactionType: reactionType, + }, + }); + } + await memoStore.getOrFetchMemoByName(memo.name, { skipCache: true }); + } catch { + // skip error. + } + setOpen(false); + }; + + return ( + + + + + + + +
+
+ {workspaceMemoRelatedSetting.reactions.map((reactionType) => { + return ( + handleReactionClick(reactionType)} + > + {reactionType} + + ); + })} +
+
+
+
+ ); +}); + +export default ReactionSelector; diff --git a/web/src/components/ReactionView.tsx b/web/src/components/ReactionView.tsx new file mode 100644 index 0000000..c97f4b7 --- /dev/null +++ b/web/src/components/ReactionView.tsx @@ -0,0 +1,92 @@ +import { observer } from "mobx-react-lite"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { memoServiceClient } from "@/grpcweb"; +import useCurrentUser from "@/hooks/useCurrentUser"; +import { cn } from "@/lib/utils"; +import { memoStore } from "@/store"; +import { State } from "@/types/proto/api/v1/common"; +import { Memo } from "@/types/proto/api/v1/memo_service"; +import { User } from "@/types/proto/api/v1/user_service"; + +interface Props { + memo: Memo; + reactionType: string; + users: User[]; +} + +const stringifyUsers = (users: User[], reactionType: string): string => { + if (users.length === 0) { + return ""; + } + if (users.length < 5) { + return users.map((user) => user.displayName || user.username).join(", ") + " reacted with " + reactionType.toLowerCase(); + } + return ( + `${users + .slice(0, 4) + .map((user) => user.displayName || user.username) + .join(", ")} and ${users.length - 4} more reacted with ` + reactionType.toLowerCase() + ); +}; + +const ReactionView = observer((props: Props) => { + const { memo, reactionType, users } = props; + const currentUser = useCurrentUser(); + const hasReaction = users.some((user) => currentUser && user.username === currentUser.username); + const readonly = memo.state === State.ARCHIVED; + + const handleReactionClick = async () => { + if (!currentUser || readonly) { + return; + } + + const index = users.findIndex((user) => user.username === currentUser.username); + try { + if (index === -1) { + await memoServiceClient.upsertMemoReaction({ + name: memo.name, + reaction: { + contentId: memo.name, + reactionType, + }, + }); + } else { + const reactions = memo.reactions.filter( + (reaction) => reaction.reactionType === reactionType && reaction.creator === currentUser.name, + ); + for (const reaction of reactions) { + await memoServiceClient.deleteMemoReaction({ name: reaction.name }); + } + } + } catch { + // Skip error. + } + await memoStore.getOrFetchMemoByName(memo.name, { skipCache: true }); + }; + + return ( + + + +
+ {reactionType} + {users.length} +
+
+ +

{stringifyUsers(users, reactionType)}

+
+
+
+ ); +}); + +export default ReactionView; diff --git a/web/src/components/RenameTagDialog.tsx b/web/src/components/RenameTagDialog.tsx new file mode 100644 index 0000000..7fe7e75 --- /dev/null +++ b/web/src/components/RenameTagDialog.tsx @@ -0,0 +1,89 @@ +import React, { useState } from "react"; +import { toast } from "react-hot-toast"; +import { Button } from "@/components/ui/button"; +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { memoServiceClient } from "@/grpcweb"; +import useLoading from "@/hooks/useLoading"; +import { useTranslate } from "@/utils/i18n"; + +interface RenameTagDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + tag: string; + onSuccess?: () => void; +} + +export function RenameTagDialog({ open, onOpenChange, tag, onSuccess }: RenameTagDialogProps) { + const t = useTranslate(); + const [newName, setNewName] = useState(tag); + const requestState = useLoading(false); + + const handleTagNameInputChange = (e: React.ChangeEvent) => { + setNewName(e.target.value.trim()); + }; + + const handleConfirm = async () => { + if (!newName || newName.includes(" ")) { + toast.error(t("tag.rename-error-empty")); + return; + } + if (newName === tag) { + toast.error(t("tag.rename-error-repeat")); + return; + } + + try { + requestState.setLoading(); + await memoServiceClient.renameMemoTag({ + parent: "memos/-", + oldTag: tag, + newTag: newName, + }); + toast.success(t("tag.rename-success")); + requestState.setFinish(); + onSuccess?.(); + onOpenChange(false); + } catch (error: any) { + console.error(error); + toast.error(error.details); + requestState.setError(); + } + }; + + return ( + + + + {t("tag.rename-tag")} + +
+
+ + +
+
+ + +
+
+
    +
  • {t("tag.rename-tip")}
  • +
+
+
+ + + + +
+
+ ); +} + +export default RenameTagDialog; diff --git a/web/src/components/RequiredBadge.tsx b/web/src/components/RequiredBadge.tsx new file mode 100644 index 0000000..a021954 --- /dev/null +++ b/web/src/components/RequiredBadge.tsx @@ -0,0 +1,11 @@ +interface Props { + className?: string; +} + +const RequiredBadge: React.FC = (props: Props) => { + const { className } = props; + + return *; +}; + +export default RequiredBadge; diff --git a/web/src/components/SearchBar.tsx b/web/src/components/SearchBar.tsx new file mode 100644 index 0000000..8c8349f --- /dev/null +++ b/web/src/components/SearchBar.tsx @@ -0,0 +1,49 @@ +import { SearchIcon } from "lucide-react"; +import { observer } from "mobx-react-lite"; +import { useState } from "react"; +import { cn } from "@/lib/utils"; +import { memoFilterStore } from "@/store"; +import { useTranslate } from "@/utils/i18n"; +import MemoDisplaySettingMenu from "./MemoDisplaySettingMenu"; + +const SearchBar = observer(() => { + const t = useTranslate(); + const [queryText, setQueryText] = useState(""); + + const onTextChange = (event: React.FormEvent) => { + setQueryText(event.currentTarget.value); + }; + + const onKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter") { + e.preventDefault(); + const trimmedText = queryText.trim(); + if (trimmedText !== "") { + const words = trimmedText.split(/\s+/); + words.forEach((word) => { + memoFilterStore.addFilter({ + factor: "contentSearch", + value: word, + }); + }); + setQueryText(""); + } + } + }; + + return ( +
+ + + +
+ ); +}); + +export default SearchBar; diff --git a/web/src/components/Settings/AccessTokenSection.tsx b/web/src/components/Settings/AccessTokenSection.tsx new file mode 100644 index 0000000..5b19057 --- /dev/null +++ b/web/src/components/Settings/AccessTokenSection.tsx @@ -0,0 +1,143 @@ +import copy from "copy-to-clipboard"; +import { ClipboardIcon, TrashIcon } from "lucide-react"; +import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; +import { Button } from "@/components/ui/button"; +import { userServiceClient } from "@/grpcweb"; +import useCurrentUser from "@/hooks/useCurrentUser"; +import { useDialog } from "@/hooks/useDialog"; +import { UserAccessToken } from "@/types/proto/api/v1/user_service"; +import { useTranslate } from "@/utils/i18n"; +import CreateAccessTokenDialog from "../CreateAccessTokenDialog"; +import LearnMore from "../LearnMore"; + +const listAccessTokens = async (parent: string) => { + const { accessTokens } = await userServiceClient.listUserAccessTokens({ parent }); + return accessTokens.sort((a, b) => (b.issuedAt?.getTime() ?? 0) - (a.issuedAt?.getTime() ?? 0)); +}; + +const AccessTokenSection = () => { + const t = useTranslate(); + const currentUser = useCurrentUser(); + const [userAccessTokens, setUserAccessTokens] = useState([]); + const createTokenDialog = useDialog(); + + useEffect(() => { + listAccessTokens(currentUser.name).then((accessTokens) => { + setUserAccessTokens(accessTokens); + }); + }, []); + + const handleCreateAccessTokenDialogConfirm = async () => { + const accessTokens = await listAccessTokens(currentUser.name); + setUserAccessTokens(accessTokens); + }; + + const handleCreateToken = () => { + createTokenDialog.open(); + }; + + const copyAccessToken = (accessToken: string) => { + copy(accessToken); + toast.success(t("setting.access-token-section.access-token-copied-to-clipboard")); + }; + + const handleDeleteAccessToken = async (userAccessToken: UserAccessToken) => { + const formatedAccessToken = getFormatedAccessToken(userAccessToken.accessToken); + const confirmed = window.confirm(t("setting.access-token-section.access-token-deletion", { accessToken: formatedAccessToken })); + if (confirmed) { + await userServiceClient.deleteUserAccessToken({ name: userAccessToken.name }); + setUserAccessTokens(userAccessTokens.filter((token) => token.accessToken !== userAccessToken.accessToken)); + } + }; + + const getFormatedAccessToken = (accessToken: string) => { + return `${accessToken.slice(0, 4)}****${accessToken.slice(-4)}`; + }; + + return ( +
+
+
+
+

+ {t("setting.access-token-section.title")} + +

+

{t("setting.access-token-section.description")}

+
+
+ +
+
+
+
+
+ + + + + + + + + + + + {userAccessTokens.map((userAccessToken) => ( + + + + + + + + ))} + +
+ {t("setting.access-token-section.token")} + + {t("common.description")} + + {t("setting.access-token-section.create-dialog.created-at")} + + {t("setting.access-token-section.create-dialog.expires-at")} + + {t("common.delete")} +
+ {getFormatedAccessToken(userAccessToken.accessToken)} + + {userAccessToken.description} + {userAccessToken.issuedAt?.toLocaleString()} + + {userAccessToken.expiresAt?.toLocaleString() ?? t("setting.access-token-section.create-dialog.duration-never")} + + +
+
+
+
+
+ + {/* Create Access Token Dialog */} + +
+ ); +}; + +export default AccessTokenSection; diff --git a/web/src/components/Settings/MemberSection.tsx b/web/src/components/Settings/MemberSection.tsx new file mode 100644 index 0000000..7734063 --- /dev/null +++ b/web/src/components/Settings/MemberSection.tsx @@ -0,0 +1,176 @@ +import { sortBy } from "lodash-es"; +import { MoreVerticalIcon, PlusIcon } from "lucide-react"; +import { observer } from "mobx-react-lite"; +import React, { useEffect, useState } from "react"; +import { Button } from "@/components/ui/button"; +import { userServiceClient } from "@/grpcweb"; +import useCurrentUser from "@/hooks/useCurrentUser"; +import { useDialog } from "@/hooks/useDialog"; +import { userStore } from "@/store"; +import { State } from "@/types/proto/api/v1/common"; +import { User, User_Role } from "@/types/proto/api/v1/user_service"; +import { useTranslate } from "@/utils/i18n"; +import CreateUserDialog from "../CreateUserDialog"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../ui/dropdown-menu"; + +const MemberSection = observer(() => { + const t = useTranslate(); + const currentUser = useCurrentUser(); + const [users, setUsers] = useState([]); + const createDialog = useDialog(); + const editDialog = useDialog(); + const [editingUser, setEditingUser] = useState(); + const sortedUsers = sortBy(users, "id"); + + useEffect(() => { + fetchUsers(); + }, []); + + const fetchUsers = async () => { + const users = await userStore.fetchUsers(); + setUsers(users); + }; + + const stringifyUserRole = (role: User_Role) => { + if (role === User_Role.HOST) { + return "Host"; + } else if (role === User_Role.ADMIN) { + return t("setting.member-section.admin"); + } else { + return t("setting.member-section.user"); + } + }; + + const handleCreateUser = () => { + setEditingUser(undefined); + createDialog.open(); + }; + + const handleEditUser = (user: User) => { + setEditingUser(user); + editDialog.open(); + }; + + const handleArchiveUserClick = async (user: User) => { + const confirmed = window.confirm(t("setting.member-section.archive-warning", { username: user.displayName })); + if (confirmed) { + await userServiceClient.updateUser({ + user: { + name: user.name, + state: State.ARCHIVED, + }, + updateMask: ["state"], + }); + fetchUsers(); + } + }; + + const handleRestoreUserClick = async (user: User) => { + await userServiceClient.updateUser({ + user: { + name: user.name, + state: State.NORMAL, + }, + updateMask: ["state"], + }); + fetchUsers(); + }; + + const handleDeleteUserClick = async (user: User) => { + const confirmed = window.confirm(t("setting.member-section.delete-warning", { username: user.displayName })); + if (confirmed) { + await userStore.deleteUser(user.name); + fetchUsers(); + } + }; + + return ( +
+
+

{t("setting.member-section.create-a-member")}

+ +
+
+
{t("setting.member-list")}
+
+
+
+ + + + + + + + + + + + {sortedUsers.map((user) => ( + + + + + + + + ))} + +
+ {t("common.username")} + + {t("common.role")} + + {t("common.nickname")} + + {t("common.email")} +
+ {user.username} + {user.state === State.ARCHIVED && "(Archived)"} + {stringifyUserRole(user.role)}{user.displayName}{user.email} + {currentUser?.name === user.name ? ( + {t("common.yourself")} + ) : ( + + + + + + handleEditUser(user)}>{t("common.update")} + {user.state === State.NORMAL ? ( + handleArchiveUserClick(user)}> + {t("setting.member-section.archive-member")} + + ) : ( + <> + handleRestoreUserClick(user)}>{t("common.restore")} + handleDeleteUserClick(user)} + className="text-destructive focus:text-destructive" + > + {t("setting.member-section.delete-member")} + + + )} + + + )} +
+
+
+ + {/* Create User Dialog */} + + + {/* Edit User Dialog */} + +
+ ); +}); + +export default MemberSection; diff --git a/web/src/components/Settings/MemoRelatedSettings.tsx b/web/src/components/Settings/MemoRelatedSettings.tsx new file mode 100644 index 0000000..6bc2e8e --- /dev/null +++ b/web/src/components/Settings/MemoRelatedSettings.tsx @@ -0,0 +1,187 @@ +import { isEqual, uniq } from "lodash-es"; +import { CheckIcon, X } from "lucide-react"; +import { observer } from "mobx-react-lite"; +import { useState } from "react"; +import { toast } from "react-hot-toast"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Switch } from "@/components/ui/switch"; +import { workspaceStore } from "@/store"; +import { workspaceSettingNamePrefix } from "@/store/common"; +import { WorkspaceSettingKey } from "@/store/workspace"; +import { WorkspaceMemoRelatedSetting } from "@/types/proto/api/v1/workspace_service"; +import { useTranslate } from "@/utils/i18n"; + +const MemoRelatedSettings = observer(() => { + const t = useTranslate(); + const [originalSetting, setOriginalSetting] = useState(workspaceStore.state.memoRelatedSetting); + const [memoRelatedSetting, setMemoRelatedSetting] = useState(originalSetting); + const [editingReaction, setEditingReaction] = useState(""); + const [editingNsfwTag, setEditingNsfwTag] = useState(""); + + const updatePartialSetting = (partial: Partial) => { + const newWorkspaceMemoRelatedSetting = WorkspaceMemoRelatedSetting.fromPartial({ + ...memoRelatedSetting, + ...partial, + }); + setMemoRelatedSetting(newWorkspaceMemoRelatedSetting); + }; + + const upsertReaction = () => { + if (!editingReaction) { + return; + } + + updatePartialSetting({ reactions: uniq([...memoRelatedSetting.reactions, editingReaction.trim()]) }); + setEditingReaction(""); + }; + + const upsertNsfwTags = () => { + if (!editingNsfwTag) { + return; + } + + updatePartialSetting({ nsfwTags: uniq([...memoRelatedSetting.nsfwTags, editingNsfwTag.trim()]) }); + setEditingNsfwTag(""); + }; + + const updateSetting = async () => { + if (memoRelatedSetting.reactions.length === 0) { + toast.error("Reactions must not be empty."); + return; + } + + try { + await workspaceStore.upsertWorkspaceSetting({ + name: `${workspaceSettingNamePrefix}${WorkspaceSettingKey.MEMO_RELATED}`, + memoRelatedSetting, + }); + setOriginalSetting(memoRelatedSetting); + toast.success(t("message.update-succeed")); + } catch (error: any) { + toast.error(error.details); + console.error(error); + } + }; + + return ( +
+

{t("setting.memo-related-settings.title")}

+
+ {t("setting.system-section.disable-public-memos")} + updatePartialSetting({ disallowPublicVisibility: checked })} + /> +
+
+ {t("setting.system-section.display-with-updated-time")} + updatePartialSetting({ displayWithUpdateTime: checked })} + /> +
+
+ {t("setting.memo-related-settings.enable-link-preview")} + updatePartialSetting({ enableLinkPreview: checked })} + /> +
+
+ {t("setting.memo-related-settings.enable-memo-comments")} + updatePartialSetting({ enableComment: checked })} + /> +
+
+ {t("setting.system-section.enable-double-click-to-edit")} + updatePartialSetting({ enableDoubleClickEdit: checked })} + /> +
+
+ {t("setting.system-section.disable-markdown-shortcuts-in-editor")} + updatePartialSetting({ disableMarkdownShortcuts: checked })} + /> +
+
+ {t("setting.memo-related-settings.content-lenght-limit")} + updatePartialSetting({ contentLengthLimit: Number(event.target.value) })} + /> +
+
+ {t("setting.memo-related-settings.reactions")} +
+ {memoRelatedSetting.reactions.map((reactionType) => { + return ( + + {reactionType} + updatePartialSetting({ reactions: memoRelatedSetting.reactions.filter((r) => r !== reactionType) })} + /> + + ); + })} +
+ setEditingReaction(event.target.value.trim())} + /> + upsertReaction()} /> +
+
+
+
+
+ {t("setting.memo-related-settings.enable-blur-nsfw-content")} + updatePartialSetting({ enableBlurNsfwContent: checked })} + /> +
+
+ {memoRelatedSetting.nsfwTags.map((nsfwTag) => { + return ( + + {nsfwTag} + updatePartialSetting({ nsfwTags: memoRelatedSetting.nsfwTags.filter((r) => r !== nsfwTag) })} + /> + + ); + })} +
+ setEditingNsfwTag(event.target.value.trim())} + /> + upsertNsfwTags()} /> +
+
+
+
+ +
+
+ ); +}); + +export default MemoRelatedSettings; diff --git a/web/src/components/Settings/MyAccountSection.tsx b/web/src/components/Settings/MyAccountSection.tsx new file mode 100644 index 0000000..a9fce54 --- /dev/null +++ b/web/src/components/Settings/MyAccountSection.tsx @@ -0,0 +1,69 @@ +import { MoreVerticalIcon, PenLineIcon } from "lucide-react"; +import { Button } from "@/components/ui/button"; +import useCurrentUser from "@/hooks/useCurrentUser"; +import { useDialog } from "@/hooks/useDialog"; +import { useTranslate } from "@/utils/i18n"; +import ChangeMemberPasswordDialog from "../ChangeMemberPasswordDialog"; +import UpdateAccountDialog from "../UpdateAccountDialog"; +import UserAvatar from "../UserAvatar"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../ui/dropdown-menu"; +import AccessTokenSection from "./AccessTokenSection"; +import UserSessionsSection from "./UserSessionsSection"; + +const MyAccountSection = () => { + const t = useTranslate(); + const user = useCurrentUser(); + const accountDialog = useDialog(); + const passwordDialog = useDialog(); + + const handleEditAccount = () => { + accountDialog.open(); + }; + + const handleChangePassword = () => { + passwordDialog.open(); + }; + + return ( +
+

{t("setting.account-section.title")}

+
+ +
+

+ {user.displayName} + ({user.username}) +

+

{user.description}

+
+
+
+ + + + + + + {t("setting.account-section.change-password")} + + +
+ + + + + {/* Update Account Dialog */} + + + {/* Change Password Dialog */} + +
+ ); +}; + +export default MyAccountSection; diff --git a/web/src/components/Settings/PreferencesSection.tsx b/web/src/components/Settings/PreferencesSection.tsx new file mode 100644 index 0000000..9937c2e --- /dev/null +++ b/web/src/components/Settings/PreferencesSection.tsx @@ -0,0 +1,84 @@ +import { observer } from "mobx-react-lite"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Separator } from "@/components/ui/separator"; +import { userStore } from "@/store"; +import { Visibility } from "@/types/proto/api/v1/memo_service"; +import { UserSetting } from "@/types/proto/api/v1/user_service"; +import { useTranslate } from "@/utils/i18n"; +import { convertVisibilityFromString, convertVisibilityToString } from "@/utils/memo"; +import AppearanceSelect from "../AppearanceSelect"; +import LocaleSelect from "../LocaleSelect"; +import ThemeSelector from "../ThemeSelector"; +import VisibilityIcon from "../VisibilityIcon"; +import WebhookSection from "./WebhookSection"; + +const PreferencesSection = observer(() => { + const t = useTranslate(); + const setting = userStore.state.userSetting as UserSetting; + + const handleLocaleSelectChange = async (locale: Locale) => { + await userStore.updateUserSetting({ locale }, ["locale"]); + }; + + const handleAppearanceSelectChange = async (appearance: Appearance) => { + await userStore.updateUserSetting({ appearance }, ["appearance"]); + }; + + const handleDefaultMemoVisibilityChanged = async (value: string) => { + await userStore.updateUserSetting({ memoVisibility: value }, ["memo_visibility"]); + }; + + const handleThemeChange = async (theme: string) => { + await userStore.updateUserSetting({ theme }, ["theme"]); + }; + + return ( +
+

{t("common.basic")}

+ +
+ {t("common.language")} + +
+ +
+ {t("setting.preference-section.apperance")} + +
+ +
+ {t("setting.preference-section.theme")} + +
+ +

{t("setting.preference")}

+ +
+ {t("setting.preference-section.default-memo-visibility")} + +
+ + + + +
+ ); +}); + +export default PreferencesSection; diff --git a/web/src/components/Settings/SSOSection.tsx b/web/src/components/Settings/SSOSection.tsx new file mode 100644 index 0000000..834776f --- /dev/null +++ b/web/src/components/Settings/SSOSection.tsx @@ -0,0 +1,122 @@ +import { MoreVerticalIcon } from "lucide-react"; +import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; +import { Link } from "react-router-dom"; +import { Button } from "@/components/ui/button"; +import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; +import { Separator } from "@/components/ui/separator"; +import { identityProviderServiceClient } from "@/grpcweb"; +import { IdentityProvider } from "@/types/proto/api/v1/idp_service"; +import { useTranslate } from "@/utils/i18n"; +import CreateIdentityProviderDialog from "../CreateIdentityProviderDialog"; +import LearnMore from "../LearnMore"; + +const SSOSection = () => { + const t = useTranslate(); + const [identityProviderList, setIdentityProviderList] = useState([]); + const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false); + const [editingIdentityProvider, setEditingIdentityProvider] = useState(); + + useEffect(() => { + fetchIdentityProviderList(); + }, []); + + const fetchIdentityProviderList = async () => { + const { identityProviders } = await identityProviderServiceClient.listIdentityProviders({}); + setIdentityProviderList(identityProviders); + }; + + const handleDeleteIdentityProvider = async (identityProvider: IdentityProvider) => { + const confirmed = window.confirm(t("setting.sso-section.confirm-delete", { name: identityProvider.title })); + if (confirmed) { + try { + await identityProviderServiceClient.deleteIdentityProvider({ name: identityProvider.name }); + } catch (error: any) { + console.error(error); + toast.error(error.details); + } + await fetchIdentityProviderList(); + } + }; + + const handleCreateIdentityProvider = () => { + setEditingIdentityProvider(undefined); + setIsCreateDialogOpen(true); + }; + + const handleEditIdentityProvider = (identityProvider: IdentityProvider) => { + setEditingIdentityProvider(identityProvider); + setIsCreateDialogOpen(true); + }; + + const handleDialogSuccess = async () => { + await fetchIdentityProviderList(); + setIsCreateDialogOpen(false); + setEditingIdentityProvider(undefined); + }; + + return ( +
+
+
+ {t("setting.sso-section.sso-list")} + +
+ +
+ + {identityProviderList.map((identityProvider) => ( +
+
+

+ {identityProvider.title} + ({identityProvider.type}) +

+
+
+ + + + + + handleEditIdentityProvider(identityProvider)}>{t("common.edit")} + handleDeleteIdentityProvider(identityProvider)}>{t("common.delete")} + + +
+
+ ))} + {identityProviderList.length === 0 && ( +
+

{t("setting.sso-section.no-sso-found")}

+
+ )} + +
+

{t("common.learn-more")}:

+
    +
  • + + {t("setting.sso-section.single-sign-on")} + +
  • +
+
+ +
+ ); +}; + +export default SSOSection; diff --git a/web/src/components/Settings/SectionMenuItem.tsx b/web/src/components/Settings/SectionMenuItem.tsx new file mode 100644 index 0000000..c40f304 --- /dev/null +++ b/web/src/components/Settings/SectionMenuItem.tsx @@ -0,0 +1,25 @@ +import { LucideIcon } from "lucide-react"; +import React from "react"; + +interface SettingMenuItemProps { + text: string; + icon: LucideIcon; + isSelected: boolean; + onClick: () => void; +} + +const SectionMenuItem: React.FC = ({ text, icon: IconComponent, isSelected, onClick }) => { + return ( +
+ + {text} +
+ ); +}; + +export default SectionMenuItem; diff --git a/web/src/components/Settings/StorageSection.tsx b/web/src/components/Settings/StorageSection.tsx new file mode 100644 index 0000000..c7529e2 --- /dev/null +++ b/web/src/components/Settings/StorageSection.tsx @@ -0,0 +1,252 @@ +import { isEqual } from "lodash-es"; +import { HelpCircleIcon } from "lucide-react"; +import { observer } from "mobx-react-lite"; +import React, { useEffect, useMemo, useState } from "react"; +import { toast } from "react-hot-toast"; +import { Link } from "react-router-dom"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { Separator } from "@/components/ui/separator"; +import { Switch } from "@/components/ui/switch"; +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { workspaceStore } from "@/store"; +import { workspaceSettingNamePrefix } from "@/store/common"; +import { WorkspaceSettingKey } from "@/store/workspace"; +import { + WorkspaceStorageSetting, + WorkspaceStorageSetting_S3Config, + WorkspaceStorageSetting_StorageType, +} from "@/types/proto/api/v1/workspace_service"; +import { useTranslate } from "@/utils/i18n"; + +const StorageSection = observer(() => { + const t = useTranslate(); + const [workspaceStorageSetting, setWorkspaceStorageSetting] = useState( + WorkspaceStorageSetting.fromPartial(workspaceStore.getWorkspaceSettingByKey(WorkspaceSettingKey.STORAGE)?.storageSetting || {}), + ); + + useEffect(() => { + setWorkspaceStorageSetting( + WorkspaceStorageSetting.fromPartial(workspaceStore.getWorkspaceSettingByKey(WorkspaceSettingKey.STORAGE)?.storageSetting || {}), + ); + }, [workspaceStore.getWorkspaceSettingByKey(WorkspaceSettingKey.STORAGE)]); + + const allowSaveStorageSetting = useMemo(() => { + if (workspaceStorageSetting.uploadSizeLimitMb <= 0) { + return false; + } + + const origin = WorkspaceStorageSetting.fromPartial( + workspaceStore.getWorkspaceSettingByKey(WorkspaceSettingKey.STORAGE)?.storageSetting || {}, + ); + if (workspaceStorageSetting.storageType === WorkspaceStorageSetting_StorageType.LOCAL) { + if (workspaceStorageSetting.filepathTemplate.length === 0) { + return false; + } + } else if (workspaceStorageSetting.storageType === WorkspaceStorageSetting_StorageType.S3) { + if ( + workspaceStorageSetting.s3Config?.accessKeyId.length === 0 || + workspaceStorageSetting.s3Config?.accessKeySecret.length === 0 || + workspaceStorageSetting.s3Config?.endpoint.length === 0 || + workspaceStorageSetting.s3Config?.region.length === 0 || + workspaceStorageSetting.s3Config?.bucket.length === 0 + ) { + return false; + } + } + return !isEqual(origin, workspaceStorageSetting); + }, [workspaceStorageSetting, workspaceStore.state]); + + const handleMaxUploadSizeChanged = async (event: React.FocusEvent) => { + let num = parseInt(event.target.value); + if (Number.isNaN(num)) { + num = 0; + } + const update: WorkspaceStorageSetting = { + ...workspaceStorageSetting, + uploadSizeLimitMb: num, + }; + setWorkspaceStorageSetting(update); + }; + + const handleFilepathTemplateChanged = async (event: React.FocusEvent) => { + const update: WorkspaceStorageSetting = { + ...workspaceStorageSetting, + filepathTemplate: event.target.value, + }; + setWorkspaceStorageSetting(update); + }; + + const handlePartialS3ConfigChanged = async (s3Config: Partial) => { + const update: WorkspaceStorageSetting = { + ...workspaceStorageSetting, + s3Config: WorkspaceStorageSetting_S3Config.fromPartial({ + ...workspaceStorageSetting.s3Config, + ...s3Config, + }), + }; + setWorkspaceStorageSetting(update); + }; + + const handleS3ConfigAccessKeyIdChanged = async (event: React.FocusEvent) => { + handlePartialS3ConfigChanged({ accessKeyId: event.target.value }); + }; + + const handleS3ConfigAccessKeySecretChanged = async (event: React.FocusEvent) => { + handlePartialS3ConfigChanged({ accessKeySecret: event.target.value }); + }; + + const handleS3ConfigEndpointChanged = async (event: React.FocusEvent) => { + handlePartialS3ConfigChanged({ endpoint: event.target.value }); + }; + + const handleS3ConfigRegionChanged = async (event: React.FocusEvent) => { + handlePartialS3ConfigChanged({ region: event.target.value }); + }; + + const handleS3ConfigBucketChanged = async (event: React.FocusEvent) => { + handlePartialS3ConfigChanged({ bucket: event.target.value }); + }; + + const handleS3ConfigUsePathStyleChanged = (event: React.ChangeEvent) => { + handlePartialS3ConfigChanged({ + usePathStyle: event.target.checked, + }); + }; + + const handleStorageTypeChanged = async (storageType: WorkspaceStorageSetting_StorageType) => { + const update: WorkspaceStorageSetting = { + ...workspaceStorageSetting, + storageType: storageType, + }; + setWorkspaceStorageSetting(update); + }; + + const saveWorkspaceStorageSetting = async () => { + await workspaceStore.upsertWorkspaceSetting({ + name: `${workspaceSettingNamePrefix}${WorkspaceSettingKey.STORAGE}`, + storageSetting: workspaceStorageSetting, + }); + toast.success("Updated"); + }; + + return ( +
+
{t("setting.storage-section.current-storage")}
+ { + handleStorageTypeChanged(parseInt(value) as unknown as WorkspaceStorageSetting_StorageType); + }} + className="flex flex-row gap-4" + > +
+ + +
+
+ + +
+
+ + +
+
+
+
+ {t("setting.system-section.max-upload-size")} + + + + + + +

{t("setting.system-section.max-upload-size-hint")}

+
+
+
+
+ +
+ {workspaceStorageSetting.storageType !== WorkspaceStorageSetting_StorageType.DATABASE && ( +
+ {t("setting.storage-section.filepath-template")} + +
+ )} + {workspaceStorageSetting.storageType === WorkspaceStorageSetting_StorageType.S3 && ( + <> +
+ Access key id + +
+
+ Access key secret + +
+
+ Endpoint + +
+
+ Region + +
+
+ Bucket + +
+
+ Use Path Style + handleS3ConfigUsePathStyleChanged({ target: { checked } } as any)} + /> +
+ + )} +
+ +
+ +
+

{t("common.learn-more")}:

+
    +
  • + + Docs - Local storage + +
  • +
  • + + Choosing a Storage for Your Resource: Database, S3 or Local Storage? + +
  • +
+
+
+ ); +}); + +export default StorageSection; diff --git a/web/src/components/Settings/UserSessionsSection.tsx b/web/src/components/Settings/UserSessionsSection.tsx new file mode 100644 index 0000000..9d6e47a --- /dev/null +++ b/web/src/components/Settings/UserSessionsSection.tsx @@ -0,0 +1,158 @@ +import { ClockIcon, MonitorIcon, SmartphoneIcon, TabletIcon, TrashIcon, WifiIcon } from "lucide-react"; +import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; +import { Button } from "@/components/ui/button"; +import { userServiceClient } from "@/grpcweb"; +import useCurrentUser from "@/hooks/useCurrentUser"; +import { UserSession } from "@/types/proto/api/v1/user_service"; +import { useTranslate } from "@/utils/i18n"; +import LearnMore from "../LearnMore"; + +const listUserSessions = async (parent: string) => { + const { sessions } = await userServiceClient.listUserSessions({ parent }); + return sessions.sort((a, b) => (b.lastAccessedTime?.getTime() ?? 0) - (a.lastAccessedTime?.getTime() ?? 0)); +}; + +const UserSessionsSection = () => { + const t = useTranslate(); + const currentUser = useCurrentUser(); + const [userSessions, setUserSessions] = useState([]); + + useEffect(() => { + listUserSessions(currentUser.name).then((sessions) => { + setUserSessions(sessions); + }); + }, []); + + const handleRevokeSession = async (userSession: UserSession) => { + const formattedSessionId = getFormattedSessionId(userSession.sessionId); + const confirmed = window.confirm(t("setting.user-sessions-section.session-revocation", { sessionId: formattedSessionId })); + if (confirmed) { + await userServiceClient.revokeUserSession({ name: userSession.name }); + setUserSessions(userSessions.filter((session) => session.sessionId !== userSession.sessionId)); + toast.success(t("setting.user-sessions-section.session-revoked")); + } + }; + + const getFormattedSessionId = (sessionId: string) => { + return `${sessionId.slice(0, 8)}...${sessionId.slice(-8)}`; + }; + + const getDeviceIcon = (deviceType: string) => { + switch (deviceType?.toLowerCase()) { + case "mobile": + return ; + case "tablet": + return ; + case "desktop": + default: + return ; + } + }; + + const formatDeviceInfo = (clientInfo: UserSession["clientInfo"]) => { + if (!clientInfo) return "Unknown Device"; + + const parts = []; + if (clientInfo.os) parts.push(clientInfo.os); + if (clientInfo.browser) parts.push(clientInfo.browser); + + return parts.length > 0 ? parts.join(" • ") : "Unknown Device"; + }; + + const isCurrentSession = (session: UserSession) => { + // A simple heuristic: the most recently accessed session is likely the current one + if (userSessions.length === 0) return false; + const mostRecent = userSessions[0]; + return session.sessionId === mostRecent.sessionId; + }; + + return ( +
+
+
+
+

+ {t("setting.user-sessions-section.title")} + +

+

{t("setting.user-sessions-section.description")}

+
+
+
+
+
+ + + + + + + + + + {userSessions.map((userSession) => ( + + + + + + ))} + +
+ {t("setting.user-sessions-section.device")} + + {t("setting.user-sessions-section.last-active")} + + {t("common.delete")} +
+
+ {getDeviceIcon(userSession.clientInfo?.deviceType || "")} +
+ + {formatDeviceInfo(userSession.clientInfo)} + {isCurrentSession(userSession) && ( + + + {t("setting.user-sessions-section.current")} + + )} + + {getFormattedSessionId(userSession.sessionId)} +
+
+
+
+ + {userSession.lastAccessedTime?.toLocaleString()} +
+
+ +
+ {userSessions.length === 0 && ( +
{t("setting.user-sessions-section.no-sessions")}
+ )} +
+
+
+
+
+ ); +}; + +export default UserSessionsSection; diff --git a/web/src/components/Settings/WebhookSection.tsx b/web/src/components/Settings/WebhookSection.tsx new file mode 100644 index 0000000..3a84788 --- /dev/null +++ b/web/src/components/Settings/WebhookSection.tsx @@ -0,0 +1,125 @@ +import { ExternalLinkIcon, TrashIcon } from "lucide-react"; +import { useEffect, useState } from "react"; +import { Link } from "react-router-dom"; +import { Button } from "@/components/ui/button"; +import { webhookServiceClient } from "@/grpcweb"; +import useCurrentUser from "@/hooks/useCurrentUser"; +import { Webhook } from "@/types/proto/api/v1/webhook_service"; +import { useTranslate } from "@/utils/i18n"; +import CreateWebhookDialog from "../CreateWebhookDialog"; + +const WebhookSection = () => { + const t = useTranslate(); + const currentUser = useCurrentUser(); + const [webhooks, setWebhooks] = useState([]); + const [isCreateWebhookDialogOpen, setIsCreateWebhookDialogOpen] = useState(false); + + const listWebhooks = async () => { + if (!currentUser) return []; + const { webhooks } = await webhookServiceClient.listWebhooks({ + parent: currentUser.name, + }); + return webhooks; + }; + + useEffect(() => { + listWebhooks().then((webhooks) => { + setWebhooks(webhooks); + }); + }, [currentUser]); + + const handleCreateWebhookDialogConfirm = async () => { + const webhooks = await listWebhooks(); + setWebhooks(webhooks); + setIsCreateWebhookDialogOpen(false); + }; + + const handleDeleteWebhook = async (webhook: Webhook) => { + const confirmed = window.confirm(`Are you sure to delete webhook \`${webhook.displayName}\`? You cannot undo this action.`); + if (confirmed) { + await webhookServiceClient.deleteWebhook({ name: webhook.name }); + setWebhooks(webhooks.filter((item) => item.name !== webhook.name)); + } + }; + + return ( +
+
+
+

{t("setting.webhook-section.title")}

+
+
+ +
+
+
+
+
+ + + + + + + + + + {webhooks.map((webhook) => ( + + + + + + ))} + + {webhooks.length === 0 && ( + + + + )} + +
+ {t("common.name")} + + {t("setting.webhook-section.url")} + + {t("common.delete")} +
{webhook.displayName} + {webhook.url} + + +
+ {t("setting.webhook-section.no-webhooks-found")} +
+
+
+
+
+ + {t("common.learn-more")} + + +
+ +
+ ); +}; + +export default WebhookSection; diff --git a/web/src/components/Settings/WorkspaceSection.tsx b/web/src/components/Settings/WorkspaceSection.tsx new file mode 100644 index 0000000..ddb393d --- /dev/null +++ b/web/src/components/Settings/WorkspaceSection.tsx @@ -0,0 +1,193 @@ +import { isEqual } from "lodash-es"; +import { ExternalLinkIcon } from "lucide-react"; +import { observer } from "mobx-react-lite"; +import { useEffect, useState } from "react"; +import { toast } from "react-hot-toast"; +import { Link } from "react-router-dom"; +import { Button } from "@/components/ui/button"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Separator } from "@/components/ui/separator"; +import { Switch } from "@/components/ui/switch"; +import { Textarea } from "@/components/ui/textarea"; +import { identityProviderServiceClient } from "@/grpcweb"; +import useDialog from "@/hooks/useDialog"; +import { workspaceStore } from "@/store"; +import { workspaceSettingNamePrefix } from "@/store/common"; +import { WorkspaceSettingKey } from "@/store/workspace"; +import { IdentityProvider } from "@/types/proto/api/v1/idp_service"; +import { WorkspaceGeneralSetting } from "@/types/proto/api/v1/workspace_service"; +import { useTranslate } from "@/utils/i18n"; +import ThemeSelector from "../ThemeSelector"; +import UpdateCustomizedProfileDialog from "../UpdateCustomizedProfileDialog"; + +const WorkspaceSection = observer(() => { + const t = useTranslate(); + const customizeDialog = useDialog(); + const originalSetting = WorkspaceGeneralSetting.fromPartial( + workspaceStore.getWorkspaceSettingByKey(WorkspaceSettingKey.GENERAL)?.generalSetting || {}, + ); + const [workspaceGeneralSetting, setWorkspaceGeneralSetting] = useState(originalSetting); + const [identityProviderList, setIdentityProviderList] = useState([]); + + useEffect(() => { + setWorkspaceGeneralSetting({ ...workspaceGeneralSetting, customProfile: originalSetting.customProfile }); + }, [workspaceStore.getWorkspaceSettingByKey(WorkspaceSettingKey.GENERAL)]); + + const handleUpdateCustomizedProfileButtonClick = () => { + customizeDialog.open(); + }; + + const updatePartialSetting = (partial: Partial) => { + setWorkspaceGeneralSetting( + WorkspaceGeneralSetting.fromPartial({ + ...workspaceGeneralSetting, + ...partial, + }), + ); + }; + + const handleSaveGeneralSetting = async () => { + try { + await workspaceStore.upsertWorkspaceSetting({ + name: `${workspaceSettingNamePrefix}${WorkspaceSettingKey.GENERAL}`, + generalSetting: workspaceGeneralSetting, + }); + } catch (error: any) { + toast.error(error.details); + console.error(error); + return; + } + toast.success(t("message.update-succeed")); + }; + + useEffect(() => { + fetchIdentityProviderList(); + }, []); + + const fetchIdentityProviderList = async () => { + const { identityProviders } = await identityProviderServiceClient.listIdentityProviders({}); + setIdentityProviderList(identityProviders); + }; + + return ( +
+

{t("common.basic")}

+
+
+ {t("setting.system-section.server-name")}:{" "} + {workspaceGeneralSetting.customProfile?.title || "Memos"} +
+ +
+ +

{t("setting.system-section.title")}

+
+ Theme + updatePartialSetting({ theme: value })} + className="min-w-fit" + /> +
+
+ {t("setting.system-section.additional-style")} +
+